import json import pynetbox import logging import argparse from uuid import uuid4 from os.path import join as join_path from flask import Flask, request from flask_restful import Resource, Api from werkzeug.exceptions import HTTPException from uncloud.common.shared import shared from uncloud.common import counters from uncloud.common.vm import VMStatus from uncloud.common.request import RequestEntry, RequestType from uncloud.api import schemas from uncloud.api.helper import generate_mac, mac2ipv6 from uncloud import UncloudException logger = logging.getLogger(__name__) app = Flask(__name__) api = Api(app) app.logger.handlers.clear() arg_parser = argparse.ArgumentParser('api', add_help=False) arg_parser.add_argument('--port', '-p') @app.errorhandler(Exception) def handle_exception(e): app.logger.error(e) # pass through HTTP errors if isinstance(e, HTTPException): return e # now you're handling non-HTTP exceptions only return {'message': 'Server Error'}, 500 class CreateVM(Resource): """API Request to Handle Creation of VM""" @staticmethod def post(): data = request.json validator = schemas.CreateVMSchema(data) if validator.is_valid(): vm_uuid = uuid4().hex vm_key = join_path(shared.settings['etcd']['vm_prefix'], vm_uuid) specs = { 'cpu': validator.specs['cpu'], 'ram': validator.specs['ram'], 'os-ssd': validator.specs['os-ssd'], 'hdd': validator.specs['hdd'], } macs = [generate_mac() for _ in range(len(data['network']))] tap_ids = [ counters.increment_etcd_counter( shared.etcd_client, shared.settings['etcd']['tap_counter'] ) for _ in range(len(data['network'])) ] vm_entry = { 'name': data['vm_name'], 'owner': data['name'], 'owner_realm': data['realm'], 'specs': specs, 'hostname': '', 'status': VMStatus.stopped, 'image_uuid': validator.image_uuid, 'log': [], 'vnc_socket': '', 'network': list(zip(data['network'], macs, tap_ids)), 'metadata': {'ssh-keys': []}, 'in_migration': False, } shared.etcd_client.put(vm_key, vm_entry, value_in_json=True) # Create ScheduleVM Request r = RequestEntry.from_scratch( type=RequestType.ScheduleVM, uuid=vm_uuid, request_prefix=shared.settings['etcd']['request_prefix'], ) shared.request_pool.put(r) return {'message': 'VM Creation Queued'}, 200 return validator.get_errors(), 400 class VmStatus(Resource): @staticmethod def post(): data = request.json validator = schemas.VMStatusSchema(data) if validator.is_valid(): vm = shared.vm_pool.get( join_path(shared.settings['etcd']['vm_prefix'], data['uuid']) ) vm_value = vm.value.copy() vm_value['ip'] = [] for network_mac_and_tap in vm.network: network_name, mac, tap = network_mac_and_tap network = shared.etcd_client.get( join_path( shared.settings['etcd']['network_prefix'], data['name'], network_name, ), value_in_json=True, ) ipv6_addr = ( network.value.get('ipv6').split('::')[0] + '::' ) vm_value['ip'].append(mac2ipv6(mac, ipv6_addr)) vm.value = vm_value return vm.value else: return validator.get_errors(), 400 class CreateImage(Resource): @staticmethod def post(): data = request.json validator = schemas.CreateImageSchema(data) if validator.is_valid(): file_entry = shared.etcd_client.get( join_path(shared.settings['etcd']['file_prefix'], data['uuid']) ) file_entry_value = json.loads(file_entry.value) image_entry_json = { 'status': 'TO_BE_CREATED', 'owner': file_entry_value['owner'], 'filename': file_entry_value['filename'], 'name': data['name'], 'store_name': data['image_store'], 'visibility': 'public', } shared.etcd_client.put( join_path( shared.settings['etcd']['image_prefix'], data['uuid'] ), json.dumps(image_entry_json), ) return {'message': 'Image queued for creation.'} return validator.get_errors(), 400 class ListPublicImages(Resource): @staticmethod def get(): images = shared.etcd_client.get_prefix( shared.settings['etcd']['image_prefix'], value_in_json=True ) r = {'images': []} for image in images: image_key = '{}:{}'.format( image.value['store_name'], image.value['name'] ) r['images'].append( {'name': image_key, 'status': image.value['status']} ) return r, 200 class VMAction(Resource): @staticmethod def post(): data = request.json validator = schemas.VmActionSchema(data) if validator.is_valid(): vm_entry = shared.vm_pool.get( join_path(shared.settings['etcd']['vm_prefix'], data['uuid']) ) action = data['action'] if action == 'start': action = 'schedule' if action == 'delete' and vm_entry.hostname == '': if shared.storage_handler.is_vm_image_exists( vm_entry.uuid ): r_status = shared.storage_handler.delete_vm_image( vm_entry.uuid ) if r_status: shared.etcd_client.client.delete(vm_entry.key) return {'message': 'VM successfully deleted'} else: logger.error( 'Some Error Occurred while deleting VM' ) return {'message': 'VM deletion unsuccessfull'} else: shared.etcd_client.client.delete(vm_entry.key) return {'message': 'VM successfully deleted'} r = RequestEntry.from_scratch( type='{}VM'.format(action.title()), uuid=data['uuid'], hostname=vm_entry.hostname, request_prefix=shared.settings['etcd']['request_prefix'], ) shared.request_pool.put(r) return ( {'message': 'VM {} Queued'.format(action.title())}, 200, ) else: return validator.get_errors(), 400 class VMMigration(Resource): @staticmethod def post(): data = request.json validator = schemas.VmMigrationSchema(data) if validator.is_valid(): vm = shared.vm_pool.get(data['uuid']) r = RequestEntry.from_scratch( type=RequestType.InitVMMigration, uuid=vm.uuid, hostname=join_path( shared.settings['etcd']['host_prefix'], validator.destination.value, ), request_prefix=shared.settings['etcd']['request_prefix'], ) shared.request_pool.put(r) return ( {'message': 'VM Migration Initialization Queued'}, 200, ) else: return validator.get_errors(), 400 class ListUserVM(Resource): @staticmethod def post(): data = request.json validator = schemas.OTPSchema(data) if validator.is_valid(): vms = shared.etcd_client.get_prefix( shared.settings['etcd']['vm_prefix'], value_in_json=True ) return_vms = [] user_vms = filter( lambda v: v.value['owner'] == data['name'], vms ) for vm in user_vms: return_vms.append( { 'name': vm.value['name'], 'vm_uuid': vm.key.split('/')[-1], 'specs': vm.value['specs'], 'status': vm.value['status'], 'hostname': vm.value['hostname'], 'vnc_socket': vm.value.get('vnc_socket', None), } ) if return_vms: return {'message': return_vms}, 200 return {'message': 'No VM found'}, 404 else: return validator.get_errors(), 400 class ListUserFiles(Resource): @staticmethod def post(): data = request.json validator = schemas.OTPSchema(data) if validator.is_valid(): files = shared.etcd_client.get_prefix( shared.settings['etcd']['file_prefix'], value_in_json=True ) return_files = [] user_files = [f for f in files if f.value['owner'] == data['name']] for file in user_files: file_uuid = file.key.split('/')[-1] file = file.value file['uuid'] = file_uuid file.pop('sha512sum', None) file.pop('owner', None) return_files.append(file) return {'message': return_files}, 200 else: return validator.get_errors(), 400 class CreateHost(Resource): @staticmethod def post(): data = request.json validator = schemas.CreateHostSchema(data) if validator.is_valid(): host_key = join_path( shared.settings['etcd']['host_prefix'], uuid4().hex ) host_entry = { 'specs': data['specs'], 'hostname': data['hostname'], 'status': 'DEAD', 'last_heartbeat': '', } shared.etcd_client.put( host_key, host_entry, value_in_json=True ) return {'message': 'Host Created'}, 200 return validator.get_errors(), 400 class ListHost(Resource): @staticmethod def get(): hosts = shared.host_pool.hosts r = { host.key: { 'status': host.status, 'specs': host.specs, 'hostname': host.hostname, } for host in hosts } return r, 200 class GetSSHKeys(Resource): @staticmethod def post(): data = request.json validator = schemas.GetSSHSchema(data) if validator.is_valid(): if not validator.key_name.value: # {user_prefix}/{realm}/{name}/key/ etcd_key = join_path( shared.settings['etcd']['user_prefix'], data['realm'], data['name'], 'key', ) etcd_entry = shared.etcd_client.get_prefix( etcd_key, value_in_json=True ) keys = { key.key.split('/')[-1]: key.value for key in etcd_entry } return {'keys': keys} else: # {user_prefix}/{realm}/{name}/key/{key_name} etcd_key = join_path( shared.settings['etcd']['user_prefix'], data['realm'], data['name'], 'key', data['key_name'], ) etcd_entry = shared.etcd_client.get( etcd_key, value_in_json=True ) if etcd_entry: return { 'keys': { etcd_entry.key.split('/')[ -1 ]: etcd_entry.value } } else: return {'keys': {}} else: return validator.get_errors(), 400 class AddSSHKey(Resource): @staticmethod def post(): data = request.json validator = schemas.AddSSHSchema(data) if validator.is_valid(): # {user_prefix}/{realm}/{name}/key/{key_name} etcd_key = join_path( shared.settings['etcd']['user_prefix'], data['realm'], data['name'], 'key', data['key_name'], ) etcd_entry = shared.etcd_client.get( etcd_key, value_in_json=True ) if etcd_entry: return { 'message': 'Key with name "{}" already exists'.format( data['key_name'] ) } else: # Key Not Found. It implies user' haven't added any key yet. shared.etcd_client.put( etcd_key, data['key'], value_in_json=True ) return {'message': 'Key added successfully'} else: return validator.get_errors(), 400 class RemoveSSHKey(Resource): @staticmethod def post(): data = request.json validator = schemas.RemoveSSHSchema(data) if validator.is_valid(): # {user_prefix}/{realm}/{name}/key/{key_name} etcd_key = join_path( shared.settings['etcd']['user_prefix'], data['realm'], data['name'], 'key', data['key_name'], ) etcd_entry = shared.etcd_client.get( etcd_key, value_in_json=True ) if etcd_entry: shared.etcd_client.client.delete(etcd_key) return {'message': 'Key successfully removed.'} else: return { 'message': 'No Key with name "{}" Exists at all.'.format( data['key_name'] ) } else: return validator.get_errors(), 400 class CreateNetwork(Resource): @staticmethod def post(): data = request.json validator = schemas.CreateNetwork(data) if validator.is_valid(): network_entry = { 'id': counters.increment_etcd_counter( shared.etcd_client, shared.settings['etcd']['vxlan_counter'] ), 'type': data['type'], } if validator.user.value: try: nb = pynetbox.api( url=shared.settings['netbox']['url'], token=shared.settings['netbox']['token'], ) nb_prefix = nb.ipam.prefixes.get( prefix=shared.settings['network']['prefix'] ) prefix = nb_prefix.available_prefixes.create( data={ 'prefix_length': int( shared.settings['network']['prefix_length'] ), 'description': '{}\'s network "{}"'.format( data['name'], data['network_name'] ), 'is_pool': True, } ) except Exception as err: app.logger.error(err) return { 'message': 'Error occured while creating network.' } else: network_entry['ipv6'] = prefix['prefix'] else: network_entry['ipv6'] = 'fd00::/64' network_key = join_path( shared.settings['etcd']['network_prefix'], data['name'], data['network_name'], ) shared.etcd_client.put( network_key, network_entry, value_in_json=True ) return {'message': 'Network successfully added.'} else: return validator.get_errors(), 400 class ListUserNetwork(Resource): @staticmethod def post(): data = request.json validator = schemas.OTPSchema(data) if validator.is_valid(): prefix = join_path( shared.settings['etcd']['network_prefix'], data['name'] ) networks = shared.etcd_client.get_prefix( prefix, value_in_json=True ) user_networks = [] for net in networks: net.value['name'] = net.key.split('/')[-1] user_networks.append(net.value) return {'networks': user_networks}, 200 else: return validator.get_errors(), 400 api.add_resource(CreateVM, '/vm/create') api.add_resource(VmStatus, '/vm/status') api.add_resource(VMAction, '/vm/action') api.add_resource(VMMigration, '/vm/migrate') api.add_resource(CreateImage, '/image/create') api.add_resource(ListPublicImages, '/image/list-public') api.add_resource(ListUserVM, '/user/vms') api.add_resource(ListUserFiles, '/user/files') api.add_resource(ListUserNetwork, '/user/networks') api.add_resource(AddSSHKey, '/user/add-ssh') api.add_resource(RemoveSSHKey, '/user/remove-ssh') api.add_resource(GetSSHKeys, '/user/get-ssh') api.add_resource(CreateHost, '/host/create') api.add_resource(ListHost, '/host/list') api.add_resource(CreateNetwork, '/network/create') def main(arguments): debug = arguments['debug'] port = arguments['port'] try: image_stores = list( shared.etcd_client.get_prefix( shared.settings['etcd']['image_store_prefix'], value_in_json=True ) ) except KeyError: image_stores = False # Do not inject default values that might be very wrong # fail when required, not before # # if not image_stores: # data = { # 'is_public': True, # 'type': 'ceph', # 'name': 'images', # 'description': 'first ever public image-store', # 'attributes': {'list': [], 'key': [], 'pool': 'images'}, # } # shared.etcd_client.put( # join_path( # shared.settings['etcd']['image_store_prefix'], uuid4().hex # ), # json.dumps(data), # ) try: app.run(host='::', port=port, debug=debug) except OSError as e: raise UncloudException('Failed to start Flask: {}'.format(e))