import json import os import bitmath 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 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)) 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): self.fields = [getattr(self, field) for field in dir(self) if isinstance(getattr(self, field), Field)] def validation(self): # custom validation is optional return True def is_valid(self): for field in self.fields: field.is_valid() for parent in self.__class__.__bases__: parent.validation(self) self.validation() for field in self.fields: setattr(self, field.name, field.value) 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): 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__() def validation(self): if check_otp(self.name.value, self.realm.value, self.token.value) != 200: raise ValidationException('Wrong Credentials') class CreateImageSchema(BaseSchema): def __init__(self, data): 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]) super().__init__() def file_uuid_validation(self): 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'])) 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): self.specs = Field('specs', dict, get(data, 'specs'), validators=[self.specs_validation]) self.hostname = Field('hostname', str, get(data, 'hostname')) super().__init__(data) 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]: 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: 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: 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(10): 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: 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, } def validation(self): if self.realm.value != 'ungleich-admin': raise ValidationException('Invalid Credentials/Insufficient Permission') class CreateVMSchema(OTPSchema): def __init__(self, data): 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 super().__init__(data=data) def image_validation(self): try: image_uuid = helper.resolve_image_name(self.image.value) except Exception: 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): 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: 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 try: _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: 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, } class VMStatusSchema(OTPSchema): def __init__(self, data): data['uuid'] = ( resolve_vm_name( name=get(data, 'vm_name', return_default=True), owner=( get(data, 'in_support_of', return_default=True) or get(data, 'name', return_default=True) ) ) or KeyError ) self.uuid = VmUUIDField(data) 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'): raise ValidationException('Invalid User') class VmActionSchema(OTPSchema): def __init__(self, data): data['uuid'] = ( resolve_vm_name( name=get(data, 'vm_name', return_default=True), owner=( 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, get(data, 'action'), validators=[self.action_validation]) super().__init__(data=data) def action_validation(self): allowed_actions = ['start', 'stop', 'delete'] if self.action.value not in 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'): raise ValidationException('Invalid User.') if self.action.value == 'start' and vm.status == VMStatus.running and vm.hostname != '': raise ValidationException('VM Already Running') if self.action.value == 'stop': if vm.status == VMStatus.stopped: raise ValidationException('VM Already Stopped') elif vm.status != VMStatus.running: raise ValidationException('Cannot stop non-running VM') class VmMigrationSchema(OTPSchema): def __init__(self, data): data['uuid'] = ( resolve_vm_name( name=get(data, 'vm_name', return_default=True), owner=( 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, get(data, 'destination'), validators=[self.destination_validation]) 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,) if not host: raise ValidationException('No Such Host ({}) exists'.format(self.destination.value)) elif host.status != HostStatus.alive: 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'): raise ValidationException('Invalid User') if vm.status != VMStatus.running: raise ValidationException("Can't migrate non-running VM") 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, 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, get(data, 'key_name')) super().__init__(data=data) class GetSSHSchema(OTPSchema): def __init__(self, data): 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, 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): 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: raise ValidationException('Network with name {} already exists'.format(self.network_name.value)) def network_type_validation(self): supported_network_types = ['vxlan'] if self.type.value not in supported_network_types: raise ValidationException('Unsupported Network Type. Supported network types are {}'.format(supported_network_types))