diff --git a/uncloud/api/common_fields.py b/uncloud/api/common_fields.py deleted file mode 100755 index d1fcb64..0000000 --- a/uncloud/api/common_fields.py +++ /dev/null @@ -1,62 +0,0 @@ -import os - -from uncloud.common.shared import shared -from uncloud.common.settings import settings - - -class Optional: - pass - - -class Field: - def __init__(self, _name, _type, _value=None): - self.name = _name - self.value = _value - self.type = _type - self.__errors = [] - - def validation(self): - return True - - def is_valid(self): - if self.value == KeyError: - self.add_error( - "'{}' field is a required field".format(self.name) - ) - else: - if isinstance(self.value, Optional): - pass - elif not isinstance(self.value, self.type): - self.add_error( - "Incorrect Type for '{}' field".format(self.name) - ) - else: - self.validation() - - if self.__errors: - return False - return True - - def get_errors(self): - return self.__errors - - def add_error(self, error): - self.__errors.append(error) - - -class VmUUIDField(Field): - def __init__(self, data): - self.uuid = data.get("uuid", KeyError) - - super().__init__("uuid", str, self.uuid) - - self.validation = self.vm_uuid_validation - - def vm_uuid_validation(self): - r = shared.etcd_client.get( - os.path.join(settings["etcd"]["vm_prefix"], self.uuid) - ) - if not r: - self.add_error( - "VM with uuid {} does not exists".format(self.uuid) - ) diff --git a/uncloud/api/create_image_store.py b/uncloud/api/create_image_store.py index 1040e97..802567f 100755 --- a/uncloud/api/create_image_store.py +++ b/uncloud/api/create_image_store.py @@ -14,7 +14,4 @@ data = { 'attributes': {'list': [], 'key': [], 'pool': 'images'}, } -shared.etcd_client.put( - os.path.join(settings['etcd']['image_store_prefix'], uuid4().hex), - json.dumps(data), -) +shared.etcd_client.put(os.path.join(settings['etcd']['image_store_prefix'], uuid4().hex), json.dumps(data)) diff --git a/uncloud/api/helper.py b/uncloud/api/helper.py index 0805280..c0205c8 100755 --- a/uncloud/api/helper.py +++ b/uncloud/api/helper.py @@ -1,6 +1,4 @@ import binascii -import ipaddress -import random import logging import requests @@ -15,25 +13,19 @@ logger = logging.getLogger(__name__) def check_otp(name, realm, token): try: data = { - "auth_name": settings["otp"]["auth_name"], - "auth_token": TOTP(settings["otp"]["auth_seed"]).now(), - "auth_realm": settings["otp"]["auth_realm"], - "name": name, - "realm": realm, - "token": token, + 'auth_name': settings['otp']['auth_name'], + 'auth_token': TOTP(settings['otp']['auth_seed']).now(), + 'auth_realm': settings['otp']['auth_realm'], + 'name': name, + 'realm': realm, + 'token': token, } - except binascii.Error as err: - logger.error( - "Cannot compute OTP for seed: {}".format( - settings["otp"]["auth_seed"] - ) - ) + except binascii.Error: + logger.error('Cannot compute OTP for seed: {}'.format(settings['otp']['auth_seed'])) return 400 - - response = requests.post( - settings["otp"]["verification_controller_url"], json=data - ) - return response.status_code + else: + response = requests.post(settings['otp']['verification_controller_url'], json=data) + return response.status_code def resolve_vm_name(name, owner): @@ -43,29 +35,24 @@ def resolve_vm_name(name, owner): Output: uuid of vm if found otherwise None """ result = next( - filter( - lambda vm: vm.value["owner"] == owner - and vm.value["name"] == name, - shared.vm_pool.vms, - ), - None, + filter(lambda vm: vm.value['owner'] == owner and vm.value['name'] == name, shared.vm_pool.vms), + None ) if result: - return result.key.split("/")[-1] + return result.key.split('/')[-1] return None -def resolve_image_name(name, etcd_client): +def resolve_image_name(name): """Return image uuid given its name and its store * If the provided name is not in correct format i.e {store_name}:{image_name} return ValueError * If no such image found then return KeyError - """ - seperator = ":" + seperator = ':' # Ensure, user/program passed valid name that is of type string try: @@ -73,78 +60,35 @@ def resolve_image_name(name, etcd_client): """ Examples, where it would work and where it would raise exception - "images:alpine" --> ["images", "alpine"] + 'images:alpine' --> ['images', 'alpine'] - "images" --> ["images"] it would raise Exception as non enough value to unpack + 'images' --> ['images'] it would raise Exception as non enough value to unpack - "images:alpine:meow" --> ["images", "alpine", "meow"] it would raise Exception + 'images:alpine:meow' --> ['images', 'alpine', 'meow'] it would raise Exception as too many values to unpack """ store_name, image_name = store_name_and_image_name except Exception: - raise ValueError( - "Image name not in correct format i.e {store_name}:{image_name}" - ) + raise ValueError('Image name not in correct format i.e {store_name}:{image_name}') - images = etcd_client.get_prefix( - settings["etcd"]["image_prefix"], value_in_json=True - ) + images = shared.etcd_client.get_prefix(settings['etcd']['image_prefix'], value_in_json=True) # Try to find image with name == image_name and store_name == store_name try: image = next( filter( - lambda im: im.value["name"] == image_name - and im.value["store_name"] == store_name, + lambda im: im.value['name'] == image_name + and im.value['store_name'] == store_name, images, ) ) except StopIteration: - raise KeyError("No image with name {} found.".format(name)) + raise KeyError('No image with name {} found.'.format(name)) else: - image_uuid = image.key.split("/")[-1] + image_uuid = image.key.split('/')[-1] return image_uuid -def random_bytes(num=6): - return [random.randrange(256) for _ in range(num)] - - -def generate_mac( - uaa=False, multicast=False, oui=None, separator=":", byte_fmt="%02x" -): - mac = random_bytes() - if oui: - if type(oui) == str: - oui = [int(chunk) for chunk in oui.split(separator)] - mac = oui + random_bytes(num=6 - len(oui)) - else: - if multicast: - mac[0] |= 1 # set bit 0 - else: - mac[0] &= ~1 # clear bit 0 - if uaa: - mac[0] &= ~(1 << 1) # clear bit 1 - else: - mac[0] |= 1 << 1 # set bit 1 - return separator.join(byte_fmt % b for b in mac) - - -def mac2ipv6(mac, prefix): - # only accept MACs separated by a colon - parts = mac.split(":") - - # modify parts to match IPv6 value - parts.insert(3, "ff") - parts.insert(4, "fe") - parts[0] = "%x" % (int(parts[0], 16) ^ 2) - - # format output - ipv6_parts = [str(0)] * 4 - for i in range(0, len(parts), 2): - ipv6_parts.append("".join(parts[i : i + 2])) - - lower_part = ipaddress.IPv6Address(":".join(ipv6_parts)) - prefix = ipaddress.IPv6Address(prefix) - return str(prefix + int(lower_part)) +def make_return_message(err, status_code=200): + return {'message': str(err)}, status_code diff --git a/uncloud/api/main.py b/uncloud/api/main.py index 2d8d035..15f3e2a 100644 --- a/uncloud/api/main.py +++ b/uncloud/api/main.py @@ -14,10 +14,13 @@ 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 . import schemas -from .helper import generate_mac, mac2ipv6 +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__) @@ -33,6 +36,7 @@ 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 @@ -42,13 +46,15 @@ def handle_exception(e): class CreateVM(Resource): - """API Request to Handle Creation of VM""" - @staticmethod def post(): data = request.json - validator = schemas.CreateVMSchema(data) - if validator.is_valid(): + 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 = { @@ -57,24 +63,22 @@ class CreateVM(Resource): 'os-ssd': validator.specs['os-ssd'], 'hdd': validator.specs['hdd'], } - macs = [generate_mac() for _ in range(len(data['network']))] + macs = [generate_mac() for _ in range(len(validator.network.value))] tap_ids = [ - counters.increment_etcd_counter( - shared.etcd_client, settings['etcd']['tap_counter'] - ) - for _ in range(len(data['network'])) + counters.increment_etcd_counter(settings['etcd']['tap_counter']) + for _ in range(len(validator.network.value)) ] vm_entry = { - 'name': data['vm_name'], - 'owner': data['name'], - 'owner_realm': data['realm'], + 'name': validator.vm_name.value, + 'owner': validator.name.value, + 'owner_realm': validator.realm.value, 'specs': specs, 'hostname': '', 'status': VMStatus.stopped, 'image_uuid': validator.image_uuid, 'log': [], 'vnc_socket': '', - 'network': list(zip(data['network'], macs, tap_ids)), + 'network': list(zip(validator.network.value, macs, tap_ids)), 'metadata': {'ssh-keys': []}, 'in_migration': False, } @@ -87,86 +91,76 @@ class CreateVM(Resource): request_prefix=settings['etcd']['request_prefix'], ) shared.request_pool.put(r) - - return {'message': 'VM Creation Queued'}, 200 - return validator.get_errors(), 400 + return make_return_message('VM Creation Queued') class VmStatus(Resource): @staticmethod def post(): data = request.json - validator = schemas.VMStatusSchema(data) - if validator.is_valid(): - vm = shared.vm_pool.get( - join_path(settings['etcd']['vm_prefix'], data['uuid']) - ) + 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'], 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( - settings['etcd']['network_prefix'], - data['name'], - network_name, - ), + join_path(settings['etcd']['network_prefix'], data['name'], network_name), value_in_json=True, ) - ipv6_addr = ( - network.value.get('ipv6').split('::')[0] + '::' - ) + 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 + return vm.value, 200 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(settings['etcd']['file_prefix'], data['uuid']) - ) - file_entry_value = json.loads(file_entry.value) + 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'], data['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'], data['uuid']), + json.dumps(image_entry_json), + ) - 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( - settings['etcd']['image_prefix'], data['uuid'] - ), - json.dumps(image_entry_json), - ) - - return {'message': 'Image queued for creation.'} - return validator.get_errors(), 400 + 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 - ) + 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']} - ) + image_key = '{}:{}'.format(image.value['store_name'], image.value['name']) + r['images'].append({'name': image_key, 'status': image.value['status']}) return r, 200 @@ -174,35 +168,30 @@ 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(settings['etcd']['vm_prefix'], data['uuid']) - ) - action = data['action'] + 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'], data['uuid'])) + action = validator.action.value 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 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'} + return make_return_message('VM successfully deleted') else: - logger.error( - 'Some Error Occurred while deleting VM' - ) - return {'message': 'VM deletion unsuccessfull'} + 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 {'message': 'VM successfully deleted'} + return make_return_message('VM successfully deleted') r = RequestEntry.from_scratch( type='{}VM'.format(action.title()), @@ -211,22 +200,20 @@ class VMAction(Resource): request_prefix=settings['etcd']['request_prefix'], ) shared.request_pool.put(r) - return ( - {'message': 'VM {} Queued'.format(action.title())}, - 200, - ) - else: - return validator.get_errors(), 400 + return make_return_message('VM {} Queued'.format(action.title())) class VMMigration(Resource): @staticmethod def post(): data = request.json - validator = schemas.VmMigrationSchema(data) - - if validator.is_valid(): - vm = shared.vm_pool.get(data['uuid']) + 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.value) r = RequestEntry.from_scratch( type=RequestType.InitVMMigration, uuid=vm.uuid, @@ -238,28 +225,22 @@ class VMMigration(Resource): ) shared.request_pool.put(r) - return ( - {'message': 'VM Migration Initialization Queued'}, - 200, - ) - else: - return validator.get_errors(), 400 + return make_return_message('VM Migration Initialization Queued') class ListUserVM(Resource): @staticmethod def post(): data = request.json - validator = schemas.OTPSchema(data) - - if validator.is_valid(): - vms = shared.etcd_client.get_prefix( - settings['etcd']['vm_prefix'], value_in_json=True - ) + 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'] == data['name'], vms - ) + user_vms = filter(lambda v: v.value['owner'] == validator.name.value, vms) for vm in user_vms: return_vms.append( { @@ -271,24 +252,20 @@ class ListUserVM(Resource): '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 + return make_return_message(return_vms) class ListUserFiles(Resource): @staticmethod def post(): data = request.json - validator = schemas.OTPSchema(data) - - if validator.is_valid(): - files = shared.etcd_client.get_prefix( - settings['etcd']['file_prefix'], value_in_json=True - ) + 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'] == data['name']] for file in user_files: @@ -300,33 +277,28 @@ class ListUserFiles(Resource): file.pop('owner', None) return_files.append(file) - return {'message': return_files}, 200 - else: - return validator.get_errors(), 400 + return make_return_message(return_files) class CreateHost(Resource): @staticmethod def post(): data = request.json - validator = schemas.CreateHostSchema(data) - if validator.is_valid(): - host_key = join_path( - settings['etcd']['host_prefix'], uuid4().hex - ) + 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': data['specs'], - 'hostname': data['hostname'], - 'status': 'DEAD', + 'specs': validator.specs.value, + 'hostname': validator.hostname.value, + 'status': HostStatus.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 + shared.etcd_client.put(host_key, host_entry, value_in_json=True) + return make_return_message('Host Created.') class ListHost(Resource): @@ -348,196 +320,138 @@ class GetSSHKeys(Resource): @staticmethod def post(): data = request.json - validator = schemas.GetSSHSchema(data) - if validator.is_valid(): + 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.value, + validator.name.value, 'key') if not validator.key_name.value: - - # {user_prefix}/{realm}/{name}/key/ - etcd_key = join_path( - settings['etcd']['user_prefix'], - data['realm'], - data['name'], - 'key', - ) - etcd_entry = shared.etcd_client.get_prefix( - etcd_key, value_in_json=True - ) - + 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( - 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 - } - } + etcd_key = join_path(validator.key_name.value) + 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': {}} - else: - return validator.get_errors(), 400 + return { + 'keys': {etcd_entry.key.split('/')[-1]: etcd_entry.value} + } class AddSSHKey(Resource): @staticmethod def post(): data = request.json - validator = schemas.AddSSHSchema(data) - if validator.is_valid(): - + 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'], - data['realm'], - data['name'], - 'key', - data['key_name'], + settings['etcd']['user_prefix'], validator.realm.value, + validator.name.value, 'key', validator.key_name.value ) - 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: + 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, data['key'], value_in_json=True - ) - return {'message': 'Key added successfully'} - else: - return validator.get_errors(), 400 + shared.etcd_client.put(etcd_key, validator.key.value, value_in_json=True) + return make_return_message('Key added successfully') + else: + return make_return_message('Key "{}" already exists'.format(validator.key_name.value)) class RemoveSSHKey(Resource): @staticmethod def post(): data = request.json - validator = schemas.RemoveSSHSchema(data) - if validator.is_valid(): - + 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'], - data['realm'], - data['name'], - 'key', - data['key_name'], - ) - etcd_entry = shared.etcd_client.get( - etcd_key, value_in_json=True - ) + etcd_key = join_path(settings['etcd']['user_prefix'], validator.realm.value, + validator.name.value, 'key', validator.key_name.value) + 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.value)) 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(): - + 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( - shared.etcd_client, settings['etcd']['vxlan_counter'] - ), - 'type': data['type'], + 'id': counters.increment_etcd_counter(settings['etcd']['vxlan_counter']), + 'type': validator.type.value, } if validator.user.value: try: - nb = pynetbox.api( - url=settings['netbox']['url'], - token=settings['netbox']['token'], - ) - nb_prefix = nb.ipam.prefixes.get( - prefix=settings['network']['prefix'] - ) + 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'] - ), + 'prefix_length': int(settings['network']['prefix_length']), 'description': '{}\'s network "{}"'.format( - data['name'], data['network_name'] + validator.name.value, + validator.network_name.value ), 'is_pool': True, } ) except Exception as err: app.logger.error(err) - return { - 'message': 'Error occured while creating network.' - } + 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'], - 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 + network_key = join_path(settings['etcd']['network_prefix'], validator.name.value, + validator.network_name.value) + 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 - validator = schemas.OTPSchema(data) - - if validator.is_valid(): - prefix = join_path( - settings['etcd']['network_prefix'], data['name'] - ) - networks = shared.etcd_client.get_prefix( - prefix, value_in_json=True - ) + 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'], 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') @@ -565,37 +479,7 @@ api.add_resource(CreateNetwork, '/network/create') def main(debug=False, port=None): try: - image_stores = list( - shared.etcd_client.get_prefix( - 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( - # settings['etcd']['image_store_prefix'], uuid4().hex - # ), - # json.dumps(data), - # ) - - try: - app.run(host='::', - port=port, - debug=debug) + app.run(host='::', port=port, debug=debug) except OSError as e: raise UncloudException('Failed to start Flask: {}'.format(e)) diff --git a/uncloud/api/schemas.py b/uncloud/api/schemas.py index e4de9a8..ffa33f6 100755 --- a/uncloud/api/schemas.py +++ b/uncloud/api/schemas.py @@ -1,19 +1,3 @@ -""" -This module contain classes thats validates and intercept/modify -data coming from uncloud-cli (user) - -It was primarily developed as an alternative to argument parser -of Flask_Restful which is going to be deprecated. I also tried -marshmallow for that purpose but it was an overkill (because it -do validation + serialization + deserialization) and little -inflexible for our purpose. -""" - -# TODO: Fix error message when user's mentioned VM (referred by name) -# does not exists. -# -# Currently, it says uuid is a required field. - import json import os @@ -23,19 +7,54 @@ from uncloud.common.host import HostStatus from uncloud.common.vm import VMStatus from uncloud.common.shared import shared from uncloud.common.settings import settings -from . import helper, logger -from .common_fields import Field, VmUUIDField -from .helper import check_otp, resolve_vm_name +from uncloud.api import helper +from uncloud.api.helper import check_otp, resolve_vm_name + + +class ValidationException(Exception): + """Validation Error""" + + +class Field: + def __init__(self, _name, _type, _value=None, validators=None): + if validators is None: + validators = [] + + assert isinstance(validators, list) + + self.name = _name + self.value = _value + self.type = _type + self.validators = validators + + def is_valid(self): + if not isinstance(self.value, self.type): + raise ValidationException("Incorrect Type for '{}' field".format(self.name)) + else: + for validator in self.validators: + validator() + + def __repr__(self): + return self.name + + +class VmUUIDField(Field): + def __init__(self, data): + self.uuid = data.get('uuid', KeyError) + + super().__init__('uuid', str, self.uuid, validators=[self.vm_uuid_validation]) + + def vm_uuid_validation(self): + try: + shared.etcd_client.get(os.path.join(settings['etcd']['vm_prefix'], self.uuid)) + except KeyError: + raise ValidationException('VM with uuid {} does not exists'.format(self.uuid)) class BaseSchema: - def __init__(self, data, fields=None): - _ = data # suppress linter warning - self.__errors = [] - if fields is None: - self.fields = [] - else: - self.fields = fields + def __init__(self, data): + print(data) + self.fields = [getattr(self, field) for field in dir(self) if isinstance(getattr(self, field), Field)] def validation(self): # custom validation is optional @@ -44,517 +63,338 @@ class BaseSchema: def is_valid(self): for field in self.fields: field.is_valid() - self.add_field_errors(field) for parent in self.__class__.__bases__: - try: - parent.validation(self) - except AttributeError: - pass - if not self.__errors: - self.validation() + parent.validation(self) - if self.__errors: - return False - return True + self.validation() - def get_errors(self): - return {"message": self.__errors} - def add_field_errors(self, field: Field): - self.__errors += field.get_errors() - - def add_error(self, error): - self.__errors.append(error) +def get(dictionary: dict, key: str, return_default=False, default=None): + if dictionary is None: + raise ValidationException('No data provided at all.') + try: + value = dictionary[key] + except KeyError: + if return_default: + return default + raise ValidationException("Missing data for '{}' field.".format(key)) + else: + return value class OTPSchema(BaseSchema): - def __init__(self, data: dict, fields=None): - self.name = Field("name", str, data.get("name", KeyError)) - self.realm = Field("realm", str, data.get("realm", KeyError)) - self.token = Field("token", str, data.get("token", KeyError)) - - _fields = [self.name, self.realm, self.token] - if fields: - _fields += fields - super().__init__(data=data, fields=_fields) + def __init__(self, data: dict): + self.name = Field('name', str, get(data, 'name')) + self.realm = Field('realm', str, get(data, 'realm')) + self.token = Field('token', str, get(data, 'token')) + super().__init__(data=data) def validation(self): - if ( - check_otp( - self.name.value, self.realm.value, self.token.value - ) - != 200 - ): - self.add_error("Wrong Credentials") - - -########################## Image Operations ############################################### + if check_otp(self.name.value, self.realm.value, self.token.value) != 200: + raise ValidationException('Wrong Credentials') class CreateImageSchema(BaseSchema): def __init__(self, data): - # Fields - self.uuid = Field("uuid", str, data.get("uuid", KeyError)) - self.name = Field("name", str, data.get("name", KeyError)) - self.image_store = Field( - "image_store", str, data.get("image_store", KeyError) - ) + self.uuid = Field('uuid', str, get(data, 'uuid'), validators=[self.file_uuid_validation]) + self.name = Field('name', str, get(data, 'name')) + self.image_store = Field('image_store', str, get(data, 'image_store'), + validators=[self.image_store_name_validation]) - # Validations - self.uuid.validation = self.file_uuid_validation - self.image_store.validation = self.image_store_name_validation - - # All Fields - fields = [self.uuid, self.name, self.image_store] - super().__init__(data, fields) + super().__init__(data) def file_uuid_validation(self): - file_entry = shared.etcd_client.get( - os.path.join( - settings["etcd"]["file_prefix"], self.uuid.value - ) - ) - if file_entry is None: - self.add_error( - "Image File with uuid '{}' Not Found".format( - self.uuid.value - ) - ) + try: + shared.etcd_client.get(os.path.join(settings['etcd']['file_prefix'], self.uuid.value)) + except KeyError: + raise ValidationException("Image File with uuid '{}' Not Found".format(self.uuid.value)) def image_store_name_validation(self): - image_stores = list( - shared.etcd_client.get_prefix( - settings["etcd"]["image_store_prefix"] - ) - ) - - image_store = next( - filter( - lambda s: json.loads(s.value)["name"] - == self.image_store.value, - image_stores, - ), - None, - ) - if not image_store: - self.add_error( - "Store '{}' does not exists".format( - self.image_store.value - ) - ) - - -# Host Operations + image_stores = list(shared.etcd_client.get_prefix(settings['etcd']['image_store_prefix'])) + try: + next(filter(lambda s: json.loads(s.value)['name'] == self.image_store.value, image_stores)) + except StopIteration: + raise ValidationException("Store '{}' does not exists".format(self.image_store.value)) class CreateHostSchema(OTPSchema): def __init__(self, data): - # Fields - self.specs = Field("specs", dict, data.get("specs", KeyError)) - self.hostname = Field( - "hostname", str, data.get("hostname", KeyError) - ) + self.specs = Field('specs', dict, get(data, 'specs'), validators=[self.specs_validation]) + self.hostname = Field('hostname', str, get(data, 'hostname')) - # Validation - self.specs.validation = self.specs_validation - - fields = [self.hostname, self.specs] - - super().__init__(data=data, fields=fields) + super().__init__(data=data) def specs_validation(self): - ALLOWED_BASE = 10 + allowed_base = 10 - _cpu = self.specs.value.get("cpu", KeyError) - _ram = self.specs.value.get("ram", KeyError) - _os_ssd = self.specs.value.get("os-ssd", KeyError) - _hdd = self.specs.value.get("hdd", KeyError) + _cpu = self.specs.value.get('cpu', KeyError) + _ram = self.specs.value.get('ram', KeyError) + _os_ssd = self.specs.value.get('os-ssd', KeyError) + _hdd = self.specs.value.get('hdd', KeyError) - if KeyError in [_cpu, _ram, _os_ssd, _hdd]: - self.add_error( - "You must specify CPU, RAM and OS-SSD in your specs" - ) - return None + if KeyError in [_cpu, _ram, _os_ssd]: + raise ValidationException('You must specify CPU, RAM and OS-SSD in your specs') try: parsed_ram = bitmath.parse_string_unsafe(_ram) parsed_os_ssd = bitmath.parse_string_unsafe(_os_ssd) - if parsed_ram.base != ALLOWED_BASE: - self.add_error( - "Your specified RAM is not in correct units" - ) - if parsed_os_ssd.base != ALLOWED_BASE: - self.add_error( - "Your specified OS-SSD is not in correct units" - ) + if parsed_ram.base != allowed_base: + raise ValidationException('Your specified RAM is not in correct units') + + if parsed_os_ssd.base != allowed_base: + raise ValidationException('Your specified OS-SSD is not in correct units') if _cpu < 1: - self.add_error("CPU must be atleast 1") + raise ValidationException('CPU must be atleast 1') if parsed_ram < bitmath.GB(1): - self.add_error("RAM must be atleast 1 GB") + raise ValidationException('RAM must be atleast 1 GB') if parsed_os_ssd < bitmath.GB(10): - self.add_error("OS-SSD must be atleast 10 GB") + raise ValidationException('OS-SSD must be atleast 10 GB') parsed_hdd = [] for hdd in _hdd: _parsed_hdd = bitmath.parse_string_unsafe(hdd) - if _parsed_hdd.base != ALLOWED_BASE: - self.add_error( - "Your specified HDD is not in correct units" - ) - break + if _parsed_hdd.base != allowed_base: + raise ValidationException('Your specified HDD is not in correct units') else: parsed_hdd.append(str(_parsed_hdd)) except ValueError: - # TODO: Find some good error message - self.add_error("Specs are not correct.") + raise ValidationException('Specs are not correct.') else: - if self.get_errors(): - self.specs = { - "cpu": _cpu, - "ram": str(parsed_ram), - "os-ssd": str(parsed_os_ssd), - "hdd": parsed_hdd, - } + self.specs = { + 'cpu': _cpu, + 'ram': str(parsed_ram), + 'os-ssd': str(parsed_os_ssd), + 'hdd': parsed_hdd, + } def validation(self): - if self.realm.value != "ungleich-admin": - self.add_error( - "Invalid Credentials/Insufficient Permission" - ) - - -# VM Operations + if self.realm.value != 'ungleich-admin': + raise ValidationException('Invalid Credentials/Insufficient Permission') class CreateVMSchema(OTPSchema): def __init__(self, data): - # Fields - self.specs = Field("specs", dict, data.get("specs", KeyError)) - self.vm_name = Field( - "vm_name", str, data.get("vm_name", KeyError) - ) - self.image = Field("image", str, data.get("image", KeyError)) - self.network = Field( - "network", list, data.get("network", KeyError) - ) + self.specs = Field('specs', dict, get(data, 'specs'), validators=[self.specs_validation]) + self.vm_name = Field('vm_name', str, get(data, 'vm_name'), validators=[self.vm_name_validation]) + self.image = Field('image', str, get(data, 'image'), validators=[self.image_validation]) + self.network = Field('network', list, get(data, 'network', return_default=True, default=[]), + validators=[self.network_validation]) + self.image_uuid = None - # Validation - self.image.validation = self.image_validation - self.vm_name.validation = self.vm_name_validation - self.specs.validation = self.specs_validation - self.network.validation = self.network_validation - - fields = [self.vm_name, self.image, self.specs, self.network] - - super().__init__(data=data, fields=fields) + super().__init__(data=data) def image_validation(self): try: - image_uuid = helper.resolve_image_name( - self.image.value, shared.etcd_client - ) + image_uuid = helper.resolve_image_name(self.image.value) except Exception as e: - logger.exception( - "Cannot resolve image name = %s", self.image.value - ) - self.add_error(str(e)) + raise ValidationException('No image of name \'{}\' found'.format(self.image.value)) else: self.image_uuid = image_uuid def vm_name_validation(self): - if resolve_vm_name( - name=self.vm_name.value, owner=self.name.value - ): - self.add_error( - 'VM with same name "{}" already exists'.format( - self.vm_name.value - ) - ) + if resolve_vm_name(name=self.vm_name.value, owner=self.name.value): + raise ValidationException("VM with same name '{}' already exists".format(self.vm_name.value)) def network_validation(self): _network = self.network.value if _network: for net in _network: - network = shared.etcd_client.get( - os.path.join( - settings["etcd"]["network_prefix"], - self.name.value, - net, - ), - value_in_json=True, - ) - if not network: - self.add_error( - "Network with name {} does not exists".format( - net - ) + try: + shared.etcd_client.get( + os.path.join(settings['etcd']['network_prefix'], self.name.value, net), + value_in_json=True ) + except KeyError: + raise ValidationException('Network with name {} does not exists'.format(net)) def specs_validation(self): ALLOWED_BASE = 10 - _cpu = self.specs.value.get("cpu", KeyError) - _ram = self.specs.value.get("ram", KeyError) - _os_ssd = self.specs.value.get("os-ssd", KeyError) - _hdd = self.specs.value.get("hdd", KeyError) - - if KeyError in [_cpu, _ram, _os_ssd, _hdd]: - self.add_error( - "You must specify CPU, RAM and OS-SSD in your specs" - ) - return None try: - parsed_ram = bitmath.parse_string_unsafe(_ram) - parsed_os_ssd = bitmath.parse_string_unsafe(_os_ssd) - - if parsed_ram.base != ALLOWED_BASE: - self.add_error( - "Your specified RAM is not in correct units" - ) - if parsed_os_ssd.base != ALLOWED_BASE: - self.add_error( - "Your specified OS-SSD is not in correct units" - ) - - if int(_cpu) < 1: - self.add_error("CPU must be atleast 1") - - if parsed_ram < bitmath.GB(1): - self.add_error("RAM must be atleast 1 GB") - - if parsed_os_ssd < bitmath.GB(1): - self.add_error("OS-SSD must be atleast 1 GB") - - parsed_hdd = [] - for hdd in _hdd: - _parsed_hdd = bitmath.parse_string_unsafe(hdd) - if _parsed_hdd.base != ALLOWED_BASE: - self.add_error( - "Your specified HDD is not in correct units" - ) - break - else: - parsed_hdd.append(str(_parsed_hdd)) - - except ValueError: - # TODO: Find some good error message - self.add_error("Specs are not correct.") + _cpu = get(self.specs.value, 'cpu') + _ram = get(self.specs.value, 'ram') + _os_ssd = get(self.specs.value, 'os-ssd') + _hdd = get(self.specs.value, 'hdd', return_default=True, default=[]) + except (KeyError, Exception): + raise ValidationException('You must specify CPU, RAM and OS-SSD in your specs') else: - if self.get_errors(): + try: + parsed_ram = bitmath.parse_string_unsafe(_ram) + parsed_os_ssd = bitmath.parse_string_unsafe(_os_ssd) + + if parsed_ram.base != ALLOWED_BASE: + raise ValidationException('Your specified RAM is not in correct units') + + if parsed_os_ssd.base != ALLOWED_BASE: + raise ValidationException('Your specified OS-SSD is not in correct units') + + if int(_cpu) < 1: + raise ValidationException('CPU must be atleast 1') + + if parsed_ram < bitmath.GB(1): + raise ValidationException('RAM must be atleast 1 GB') + + if parsed_os_ssd < bitmath.GB(1): + raise ValidationException('OS-SSD must be atleast 1 GB') + + parsed_hdd = [] + for hdd in _hdd: + _parsed_hdd = bitmath.parse_string_unsafe(hdd) + if _parsed_hdd.base != ALLOWED_BASE: + raise ValidationException('Your specified HDD is not in correct units') + else: + parsed_hdd.append(str(_parsed_hdd)) + + except ValueError: + raise ValidationException('Specs are not correct.') + else: self.specs = { - "cpu": _cpu, - "ram": str(parsed_ram), - "os-ssd": str(parsed_os_ssd), - "hdd": parsed_hdd, + 'cpu': _cpu, + 'ram': str(parsed_ram), + 'os-ssd': str(parsed_os_ssd), + 'hdd': parsed_hdd, } class VMStatusSchema(OTPSchema): def __init__(self, data): - data["uuid"] = ( + data['uuid'] = ( resolve_vm_name( - name=data.get("vm_name", None), + name=get(data, 'vm_name', return_default=True), owner=( - data.get("in_support_of", None) - or data.get("name", None) - ), + get(data, 'in_support_of', return_default=True) or + get(data, 'name', return_default=True) + ) ) or KeyError ) self.uuid = VmUUIDField(data) - fields = [self.uuid] - - super().__init__(data, fields) + super().__init__(data) def validation(self): vm = shared.vm_pool.get(self.uuid.value) - if not ( - vm.value["owner"] == self.name.value - or self.realm.value == "ungleich-admin" - ): - self.add_error("Invalid User") + if not (vm.value['owner'] == self.name.value or self.realm.value == 'ungleich-admin'): + raise ValidationException('Invalid User') class VmActionSchema(OTPSchema): def __init__(self, data): - data["uuid"] = ( + data['uuid'] = ( resolve_vm_name( - name=data.get("vm_name", None), + name=get(data, 'vm_name', return_default=True), owner=( - data.get("in_support_of", None) - or data.get("name", None) - ), + get(data, 'in_support_of', return_default=True) or + get(data, 'name', return_default=True) + ) ) or KeyError ) self.uuid = VmUUIDField(data) - self.action = Field("action", str, data.get("action", KeyError)) + self.action = Field('action', str, get(data, 'action'), validators=[self.action_validation]) - self.action.validation = self.action_validation - - _fields = [self.uuid, self.action] - - super().__init__(data=data, fields=_fields) + super().__init__(data=data) def action_validation(self): - allowed_actions = ["start", "stop", "delete"] + allowed_actions = ['start', 'stop', 'delete'] if self.action.value not in allowed_actions: - self.add_error( - "Invalid Action. Allowed Actions are {}".format( - allowed_actions - ) - ) + raise ValidationException('Invalid Action. Allowed Actions are {}'.format(allowed_actions)) def validation(self): vm = shared.vm_pool.get(self.uuid.value) - if not ( - vm.value["owner"] == self.name.value - or self.realm.value == "ungleich-admin" - ): - self.add_error("Invalid User") + if not (vm.value['owner'] == self.name.value or self.realm.value == 'ungleich-admin'): + raise ValidationException('Invalid User.') - if ( - self.action.value == "start" - and vm.status == VMStatus.running - and vm.hostname != "" - ): - self.add_error("VM Already Running") + if self.action.value == 'start' and vm.status == VMStatus.running and vm.hostname != '': + raise ValidationException('VM Already Running') - if self.action.value == "stop": + if self.action.value == 'stop': if vm.status == VMStatus.stopped: - self.add_error("VM Already Stopped") + raise ValidationException('VM Already Stopped') elif vm.status != VMStatus.running: - self.add_error("Cannot stop non-running VM") + raise ValidationException('Cannot stop non-running VM') class VmMigrationSchema(OTPSchema): def __init__(self, data): - data["uuid"] = ( + data['uuid'] = ( resolve_vm_name( - name=data.get("vm_name", None), + name=get(data, 'vm_name', return_default=True), owner=( - data.get("in_support_of", None) - or data.get("name", None) - ), - ) - or KeyError + get(data, 'in_support_of', return_default=True) or + get(data, 'name', return_default=True) + ) + ) or KeyError ) self.uuid = VmUUIDField(data) - self.destination = Field( - "destination", str, data.get("destination", KeyError) - ) + self.destination = Field('destination', str, get(data, 'destination'), + validators=[self.destination_validation]) - self.destination.validation = self.destination_validation - - fields = [self.destination] - super().__init__(data=data, fields=fields) + super().__init__(data=data) def destination_validation(self): hostname = self.destination.value - host = next( - filter( - lambda h: h.hostname == hostname, shared.host_pool.hosts - ), - None, - ) + host = next(filter(lambda h: h.hostname == hostname, shared.host_pool.hosts), None,) if not host: - self.add_error( - "No Such Host ({}) exists".format( - self.destination.value - ) - ) + raise ValidationException('No Such Host ({}) exists'.format(self.destination.value)) elif host.status != HostStatus.alive: - self.add_error("Destination Host is dead") + raise ValidationException('Destination Host is dead') else: self.destination.value = host.key def validation(self): vm = shared.vm_pool.get(self.uuid.value) - if not ( - vm.value["owner"] == self.name.value - or self.realm.value == "ungleich-admin" - ): - self.add_error("Invalid User") + if not (vm.value['owner'] == self.name.value or self.realm.value == 'ungleich-admin'): + raise ValidationException('Invalid User') if vm.status != VMStatus.running: - self.add_error("Can't migrate non-running VM") + raise ValidationException("Can't migrate non-running VM") - if vm.hostname == os.path.join( - settings["etcd"]["host_prefix"], self.destination.value - ): - self.add_error( - "Destination host couldn't be same as Source Host" - ) + if vm.hostname == os.path.join(settings['etcd']['host_prefix'], self.destination.value): + raise ValidationException("Destination host couldn't be same as Source Host") class AddSSHSchema(OTPSchema): def __init__(self, data): - self.key_name = Field( - "key_name", str, data.get("key_name", KeyError) - ) - self.key = Field("key", str, data.get("key_name", KeyError)) - - fields = [self.key_name, self.key] - super().__init__(data=data, fields=fields) + self.key_name = Field('key_name', str, get(data, 'key_name')) + self.key = Field('key', str, get(data, 'key')) + super().__init__(data=data) class RemoveSSHSchema(OTPSchema): def __init__(self, data): - self.key_name = Field( - "key_name", str, data.get("key_name", KeyError) - ) - - fields = [self.key_name] - super().__init__(data=data, fields=fields) + self.key_name = Field('key_name', str, get(data, 'key_name')) + super().__init__(data=data) class GetSSHSchema(OTPSchema): def __init__(self, data): - self.key_name = Field( - "key_name", str, data.get("key_name", None) - ) - - fields = [self.key_name] - super().__init__(data=data, fields=fields) + self.key_name = Field('key_name', str, get(data, 'key_name', return_default=True)) + super().__init__(data=data) class CreateNetwork(OTPSchema): def __init__(self, data): - self.network_name = Field("network_name", str, data.get("network_name", KeyError)) - self.type = Field("type", str, data.get("type", KeyError)) - self.user = Field("user", bool, bool(data.get("user", False))) - - self.network_name.validation = self.network_name_validation - self.type.validation = self.network_type_validation - - fields = [self.network_name, self.type, self.user] - super().__init__(data, fields=fields) + self.network_name = Field('network_name', str, get(data, 'name'), + validators=[self.network_name_validation]) + self.type = Field('type', str, get(data, 'type'), validators=[self.network_type_validation]) + self.user = Field('user', bool, bool(get(data, 'user', return_default=True, default=False))) + super().__init__(data) def network_name_validation(self): - print(self.name.value, self.network_name.value) - key = os.path.join(settings["etcd"]["network_prefix"], self.name.value, self.network_name.value) - print(key) + key = os.path.join(settings['etcd']['network_prefix'], self.name.value, self.network_name.value) network = shared.etcd_client.get(key, value_in_json=True) if network: - self.add_error( - "Network with name {} already exists".format( - self.network_name.value - ) - ) + raise ValidationException('Network with name {} already exists'.format(self.network_name.value)) def network_type_validation(self): - supported_network_types = ["vxlan"] + supported_network_types = ['vxlan'] if self.type.value not in supported_network_types: - self.add_error( - "Unsupported Network Type. Supported network types are {}".format( - supported_network_types - ) - ) + raise ValidationException('Unsupported Network Type. Supported network types are {}'.format(supported_network_types)) diff --git a/uncloud/common/counters.py b/uncloud/common/counters.py index 2d4a8e9..dba8a08 100644 --- a/uncloud/common/counters.py +++ b/uncloud/common/counters.py @@ -1,8 +1,8 @@ -from .etcd_wrapper import Etcd3Wrapper +from uncloud.common.shared import shared -def increment_etcd_counter(etcd_client: Etcd3Wrapper, key): - kv = etcd_client.get(key) +def increment_etcd_counter(key): + kv = shared.etcd_client.get(key) if kv: counter = int(kv.value) @@ -10,12 +10,12 @@ def increment_etcd_counter(etcd_client: Etcd3Wrapper, key): else: counter = 1 - etcd_client.put(key, str(counter)) + shared.etcd_client.put(key, str(counter)) return counter -def get_etcd_counter(etcd_client: Etcd3Wrapper, key): - kv = etcd_client.get(key) +def get_etcd_counter(key): + kv = shared.etcd_client.get(key) if kv: return int(kv.value) return None diff --git a/uncloud/common/etcd_wrapper.py b/uncloud/common/etcd_wrapper.py index fe768ac..119d0e6 100644 --- a/uncloud/common/etcd_wrapper.py +++ b/uncloud/common/etcd_wrapper.py @@ -5,6 +5,7 @@ from functools import wraps from uncloud import UncloudException from uncloud.common import logger +from typing import Iterator class EtcdEntry: @@ -42,14 +43,30 @@ class Etcd3Wrapper: self.client = etcd3.client(*args, **kwargs) @readable_errors - def get(self, *args, value_in_json=False, **kwargs): + def get(self, *args, value_in_json=False, **kwargs) -> EtcdEntry: + """Get a key/value pair from etcd + + :return: + EtcdEntry: if a key/value pair is found in etcd + :raises: + KeyError: If key is not found in etcd + Exception: Different type of exception can be raised depending on + situation + """ _value, _key = self.client.get(*args, **kwargs) if _key is None or _value is None: - return None + raise KeyError return EtcdEntry(_key, _value, value_in_json=value_in_json) @readable_errors def put(self, *args, value_in_json=False, **kwargs): + """Put key/value pair in etcd + + :return: a response containing a header and the prev_kv + :raises: + Exception: Different type of exception can be raised depending on + situation + """ _key, _value = args if value_in_json: _value = json.dumps(_value) @@ -60,20 +77,14 @@ class Etcd3Wrapper: return self.client.put(_key, _value, **kwargs) @readable_errors - def get_prefix(self, *args, value_in_json=False, raise_exception=True, **kwargs): - try: - event_iterator = self.client.get_prefix(*args, **kwargs) - for e in event_iterator: - yield EtcdEntry(*e[::-1], value_in_json=value_in_json) - except Exception as err: - if raise_exception: - raise Exception('Exception in etcd_wrapper.get_prefix') from err - else: - logger.exception('Error in etcd_wrapper') - return iter([]) + def get_prefix(self, *args, value_in_json=False, **kwargs) -> \ + Iterator[EtcdEntry]: + event_iterator = self.client.get_prefix(*args, **kwargs) + for e in event_iterator: + yield EtcdEntry(*e[::-1], value_in_json=value_in_json) @readable_errors - def watch_prefix(self, key, raise_exception=True, value_in_json=False): + def watch_prefix(self, key, raise_exception=True, value_in_json=False) -> Iterator[EtcdEntry]: try: event_iterator, cancel = self.client.watch_prefix(key) for e in event_iterator: diff --git a/uncloud/common/network.py b/uncloud/common/network.py index 32f6951..e74359d 100644 --- a/uncloud/common/network.py +++ b/uncloud/common/network.py @@ -1,6 +1,7 @@ import subprocess as sp import random import logging +import ipaddress logger = logging.getLogger(__name__) @@ -9,9 +10,7 @@ def random_bytes(num=6): return [random.randrange(256) for _ in range(num)] -def generate_mac( - uaa=False, multicast=False, oui=None, separator=":", byte_fmt="%02x" -): +def generate_mac(uaa=False, multicast=False, oui=None, separator=":", byte_fmt="%02x"): mac = random_bytes() if oui: if type(oui) == str: @@ -68,3 +67,21 @@ def delete_network_interface(iface): except Exception: logger.exception("Interface %s Deletion failed", iface) + +def mac2ipv6(mac, prefix): + # only accept MACs separated by a colon + parts = mac.split(':') + + # modify parts to match IPv6 value + parts.insert(3, 'ff') + parts.insert(4, 'fe') + parts[0] = '%x' % (int(parts[0], 16) ^ 2) + + # format output + ipv6_parts = [str(0)] * 4 + for i in range(0, len(parts), 2): + ipv6_parts.append(''.join(parts[i: i + 2])) + + lower_part = ipaddress.IPv6Address(':'.join(ipv6_parts)) + prefix = ipaddress.IPv6Address(prefix) + return str(prefix + int(lower_part))