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.host import HostStatus from uncloud.common.request import RequestEntry, RequestType from uncloud.common.settings import settings from uncloud.api import schemas from uncloud.api.schemas import ValidationException from uncloud.common.network import generate_mac, mac2ipv6 from uncloud.api.helper import make_return_message 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): @staticmethod def post(): data = request.json try: validator = schemas.CreateVMSchema(data) validator.is_valid() except ValidationException as err: return make_return_message(err, 400) else: vm_uuid = uuid4().hex vm_key = join_path(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(validator.network))] tap_ids = [ counters.increment_etcd_counter(settings['etcd']['tap_counter']) for _ in range(len(validator.network)) ] vm_entry = { 'name': validator.vm_name, 'owner': validator.name, 'owner_realm': validator.realm, 'specs': specs, 'hostname': '', 'status': VMStatus.stopped, 'image_uuid': validator.image_uuid, 'log': [], 'vnc_socket': '', 'network': list(zip(validator.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=settings['etcd']['request_prefix'], ) shared.request_pool.put(r) return make_return_message('VM Creation Queued') class GetVMStatus(Resource): @staticmethod def post(): data = request.json try: validator = schemas.VMStatusSchema(data) validator.is_valid() except (ValidationException, Exception) as err: return make_return_message(err, 400) else: vm = shared.vm_pool.get(join_path(settings['etcd']['vm_prefix'], validator.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(settings['etcd']['network_prefix'], validator.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, 200 class CreateImage(Resource): @staticmethod def post(): data = request.json try: validator = schemas.CreateImageSchema(data) validator.is_valid() except ValidationException as err: return make_return_message(err, 400) else: try: file_entry = shared.etcd_client.get( join_path(settings['etcd']['file_prefix'], validator.uuid), value_in_json=True ) except KeyError: # TODO: Add some message pass else: image_entry_json = { 'status': 'TO_BE_CREATED', 'owner': file_entry.value['owner'], 'filename': file_entry.value['filename'], 'name': validator.name, 'store_name': validator.image_store, 'visibility': 'public', } shared.etcd_client.put( join_path(settings['etcd']['image_prefix'], validator.uuid), json.dumps(image_entry_json), ) return {'message': 'Image queued for creation.'}, 200 class ListPublicImages(Resource): @staticmethod def get(): images = shared.etcd_client.get_prefix(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 try: validator = schemas.VmActionSchema(data) validator.is_valid() except ValidationException as err: return make_return_message(err, 400) else: vm_entry = shared.vm_pool.get(join_path(settings['etcd']['vm_prefix'], validator.uuid)) action = validator.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 make_return_message('VM successfully deleted') else: logger.error('Some Error Occurred while deleting VM') return make_return_message('VM deletion unsuccessfull') else: shared.etcd_client.client.delete(vm_entry.key) return make_return_message('VM successfully deleted') r = RequestEntry.from_scratch( type='{}VM'.format(action.title()), uuid=validator.uuid, hostname=vm_entry.hostname, request_prefix=settings['etcd']['request_prefix'], ) shared.request_pool.put(r) return make_return_message('VM {} Queued'.format(action.title())) class VMMigration(Resource): @staticmethod def post(): data = request.json try: validator = schemas.VmMigrationSchema(data) validator.is_valid() except ValidationException as err: return make_return_message(err), 400 else: vm = shared.vm_pool.get(validator.uuid) r = RequestEntry.from_scratch( type=RequestType.InitVMMigration, uuid=vm.uuid, hostname=join_path( settings['etcd']['host_prefix'], validator.destination, ), request_prefix=settings['etcd']['request_prefix'], ) shared.request_pool.put(r) return make_return_message('VM Migration Initialization Queued') class ListUserVM(Resource): @staticmethod def post(): data = request.json try: validator = schemas.OTPSchema(data) validator.is_valid() except ValidationException as err: return make_return_message(err, 400) else: vms = shared.etcd_client.get_prefix(settings['etcd']['vm_prefix'], value_in_json=True) return_vms = [] user_vms = filter(lambda v: v.value['owner'] == validator.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), } ) return make_return_message(return_vms) class ListUserFiles(Resource): @staticmethod def post(): data = request.json try: validator = schemas.OTPSchema(data) validator.is_valid() except ValidationException as err: return make_return_message(err, 400) else: files = shared.etcd_client.get_prefix(settings['etcd']['file_prefix'], value_in_json=True) return_files = [] user_files = [f for f in files if f.value['owner'] == validator.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 make_return_message(return_files) class CreateHost(Resource): @staticmethod def post(): data = request.json try: validator = schemas.CreateHostSchema(data) validator.is_valid() except ValidationException as err: return make_return_message(err, 400) else: host_key = join_path(settings['etcd']['host_prefix'], uuid4().hex) host_entry = { 'specs': validator.specs, 'hostname': validator.hostname, 'status': HostStatus.dead, 'last_heartbeat': '', } shared.etcd_client.put(host_key, host_entry, value_in_json=True) return make_return_message('Host Created.') 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 try: validator = schemas.GetSSHSchema(data) except ValidationException as err: return make_return_message(err, 400) else: etcd_key = join_path(settings['etcd']['user_prefix'], validator.realm, validator.name, 'key') if not validator.key_name: 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: etcd_key = join_path(validator.key_name) try: etcd_entry = shared.etcd_client.get(etcd_key, value_in_json=True) except KeyError: return make_return_message('No such key found.', 400) else: return { 'keys': {etcd_entry.key.split('/')[-1]: etcd_entry.value} } class AddSSHKey(Resource): @staticmethod def post(): data = request.json try: validator = schemas.AddSSHSchema(data) validator.is_valid() except ValidationException as err: return make_return_message(err, 400) else: # {user_prefix}/{realm}/{name}/key/{key_name} etcd_key = join_path( settings['etcd']['user_prefix'], validator.realm, validator.name, 'key', validator.key_name ) try: shared.etcd_client.get(etcd_key, value_in_json=True) except KeyError: # Key Not Found. It implies user' haven't added any key yet. shared.etcd_client.put(etcd_key, validator.key, value_in_json=True) return make_return_message('Key added successfully') else: return make_return_message('Key "{}" already exists'.format(validator.key_name)) class RemoveSSHKey(Resource): @staticmethod def post(): data = request.json try: validator = schemas.RemoveSSHSchema(data) validator.is_valid() except ValidationException as err: return make_return_message(err, 400) else: # {user_prefix}/{realm}/{name}/key/{key_name} etcd_key = join_path(settings['etcd']['user_prefix'], validator.realm, validator.name, 'key', validator.key_name) try: etcd_entry = shared.etcd_client.get(etcd_key, value_in_json=True) except KeyError: return make_return_message('No Key "{}" exists.'.format(validator.key_name)) if etcd_entry: shared.etcd_client.client.delete(etcd_key) return {'message': 'Key successfully removed.'} class CreateNetwork(Resource): @staticmethod def post(): data = request.json try: validator = schemas.CreateNetwork(data) validator.is_valid() except ValidationException as err: return make_return_message(err, 400) else: network_entry = { 'id': counters.increment_etcd_counter(settings['etcd']['vxlan_counter']), 'type': validator.type, } if validator.user: try: nb = pynetbox.api(url=settings['netbox']['url'], token=settings['netbox']['token']) nb_prefix = nb.ipam.prefixes.get(prefix=settings['network']['prefix']) prefix = nb_prefix.available_prefixes.create( data={ 'prefix_length': int(settings['network']['prefix_length']), 'description': '{}\'s network "{}"'.format( validator.name, validator.network_name ), 'is_pool': True, } ) except Exception as err: app.logger.error(err) return make_return_message('Error occured while creating network.', 400) else: network_entry['ipv6'] = prefix['prefix'] else: network_entry['ipv6'] = 'fd00::/64' network_key = join_path(settings['etcd']['network_prefix'], validator.name, validator.network_name) shared.etcd_client.put(network_key, network_entry, value_in_json=True) return make_return_message('Network successfully added.') class ListUserNetwork(Resource): @staticmethod def post(): data = request.json try: validator = schemas.OTPSchema(data) validator.is_valid() except ValidationException as err: return make_return_message(err, 400) else: prefix = join_path(settings['etcd']['network_prefix'], validator.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 api.add_resource(CreateVM, '/vm/create') api.add_resource(GetVMStatus, '/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(debug=False, port=None): try: app.run(host='::', port=port, debug=debug) except OSError as e: raise UncloudException('Failed to start Flask: {}'.format(e)) if __name__ == '__main__': main()