uncloud/uncloud/api/schemas.py

402 lines
15 KiB
Python
Raw Normal View History

import json
import os
import bitmath
from uncloud.common.host import HostStatus
from uncloud.common.vm import VMStatus
from uncloud.common.shared import shared
2020-01-06 07:25:59 +00:00
from uncloud.common.settings import settings
2020-01-09 19:03:10 +00:00
from uncloud.api import helper
from uncloud.api.helper import check_otp, resolve_vm_name
2020-01-09 19:03:10 +00:00
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))
2020-01-09 19:33:35 +00:00
for validator in self.validators:
validator()
2020-01-09 19:03:10 +00:00
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:
2020-01-09 19:33:35 +00:00
def __init__(self):
2020-01-09 19:03:10 +00:00
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__:
2020-01-09 19:03:10 +00:00
parent.validation(self)
2020-01-09 19:03:10 +00:00
self.validation()
2020-01-09 19:33:35 +00:00
for field in self.fields:
setattr(self, field.name, field.value)
2020-01-09 19:03:10 +00:00
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):
2020-01-09 19:03:10 +00:00
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'))
2020-01-09 19:33:35 +00:00
super().__init__()
def validation(self):
2020-01-09 19:03:10 +00:00
if check_otp(self.name.value, self.realm.value, self.token.value) != 200:
raise ValidationException('Wrong Credentials')
class CreateImageSchema(BaseSchema):
def __init__(self, data):
2020-01-09 19:03:10 +00:00
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])
2020-01-09 19:33:35 +00:00
super().__init__()
def file_uuid_validation(self):
2020-01-09 19:03:10 +00:00
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):
2020-01-09 19:03:10 +00:00
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):
2020-01-09 19:03:10 +00:00
self.specs = Field('specs', dict, get(data, 'specs'), validators=[self.specs_validation])
self.hostname = Field('hostname', str, get(data, 'hostname'))
2020-01-09 19:33:35 +00:00
super().__init__(data)
def specs_validation(self):
2020-01-09 19:03:10 +00:00
allowed_base = 10
2020-01-09 19:03:10 +00:00
_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)
2020-01-09 19:03:10 +00:00
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)
2020-01-09 19:03:10 +00:00
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:
2020-01-09 19:03:10 +00:00
raise ValidationException('CPU must be atleast 1')
if parsed_ram < bitmath.GB(1):
2020-01-09 19:03:10 +00:00
raise ValidationException('RAM must be atleast 1 GB')
if parsed_os_ssd < bitmath.GB(10):
2020-01-09 19:03:10 +00:00
raise ValidationException('OS-SSD must be atleast 10 GB')
parsed_hdd = []
for hdd in _hdd:
_parsed_hdd = bitmath.parse_string_unsafe(hdd)
2020-01-09 19:03:10 +00:00
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:
2020-01-09 19:03:10 +00:00
raise ValidationException('Specs are not correct.')
else:
2020-01-09 19:03:10 +00:00
self.specs = {
'cpu': _cpu,
'ram': str(parsed_ram),
'os-ssd': str(parsed_os_ssd),
'hdd': parsed_hdd,
}
def validation(self):
2020-01-09 19:03:10 +00:00
if self.realm.value != 'ungleich-admin':
raise ValidationException('Invalid Credentials/Insufficient Permission')
class CreateVMSchema(OTPSchema):
def __init__(self, data):
2020-01-09 19:03:10 +00:00
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
2020-01-09 19:03:10 +00:00
super().__init__(data=data)
def image_validation(self):
try:
2020-01-09 19:03:10 +00:00
image_uuid = helper.resolve_image_name(self.image.value)
2020-01-09 19:33:35 +00:00
except Exception:
2020-01-09 19:03:10 +00:00
raise ValidationException('No image of name \'{}\' found'.format(self.image.value))
else:
self.image_uuid = image_uuid
def vm_name_validation(self):
2020-01-09 19:03:10 +00:00
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:
2020-01-09 19:03:10 +00:00
try:
shared.etcd_client.get(
os.path.join(settings['etcd']['network_prefix'], self.name.value, net),
value_in_json=True
)
2020-01-09 19:03:10 +00:00
except KeyError:
raise ValidationException('Network with name {} does not exists'.format(net))
def specs_validation(self):
2020-01-09 19:33:35 +00:00
allowed_base = 10
try:
2020-01-09 19:03:10 +00:00
_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)
2020-01-09 19:33:35 +00:00
if parsed_ram.base != allowed_base:
2020-01-09 19:03:10 +00:00
raise ValidationException('Your specified RAM is not in correct units')
2020-01-09 19:33:35 +00:00
if parsed_os_ssd.base != allowed_base:
2020-01-09 19:03:10 +00:00
raise ValidationException('Your specified OS-SSD is not in correct units')
2020-01-09 19:03:10 +00:00
if int(_cpu) < 1:
raise ValidationException('CPU must be atleast 1')
2020-01-09 19:03:10 +00:00
if parsed_ram < bitmath.GB(1):
raise ValidationException('RAM must be atleast 1 GB')
2020-01-09 19:03:10 +00:00
if parsed_os_ssd < bitmath.GB(1):
raise ValidationException('OS-SSD must be atleast 1 GB')
2020-01-09 19:03:10 +00:00
parsed_hdd = []
for hdd in _hdd:
_parsed_hdd = bitmath.parse_string_unsafe(hdd)
2020-01-09 19:33:35 +00:00
if _parsed_hdd.base != allowed_base:
2020-01-09 19:03:10 +00:00
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 = {
2020-01-09 19:03:10 +00:00
'cpu': _cpu,
'ram': str(parsed_ram),
'os-ssd': str(parsed_os_ssd),
'hdd': parsed_hdd,
}
class VMStatusSchema(OTPSchema):
def __init__(self, data):
2020-01-09 19:03:10 +00:00
data['uuid'] = (
resolve_vm_name(
2020-01-09 19:03:10 +00:00
name=get(data, 'vm_name', return_default=True),
owner=(
2020-01-09 19:03:10 +00:00
get(data, 'in_support_of', return_default=True) or
get(data, 'name', return_default=True)
)
)
or KeyError
)
self.uuid = VmUUIDField(data)
2020-01-09 19:03:10 +00:00
super().__init__(data)
def validation(self):
vm = shared.vm_pool.get(self.uuid.value)
2020-01-09 19:03:10 +00:00
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):
2020-01-09 19:03:10 +00:00
data['uuid'] = (
resolve_vm_name(
2020-01-09 19:03:10 +00:00
name=get(data, 'vm_name', return_default=True),
owner=(
2020-01-09 19:03:10 +00:00
get(data, 'in_support_of', return_default=True) or
get(data, 'name', return_default=True)
)
)
or KeyError
)
self.uuid = VmUUIDField(data)
2020-01-09 19:03:10 +00:00
self.action = Field('action', str, get(data, 'action'), validators=[self.action_validation])
2020-01-09 19:03:10 +00:00
super().__init__(data=data)
def action_validation(self):
2020-01-09 19:03:10 +00:00
allowed_actions = ['start', 'stop', 'delete']
if self.action.value not in allowed_actions:
2020-01-09 19:03:10 +00:00
raise ValidationException('Invalid Action. Allowed Actions are {}'.format(allowed_actions))
def validation(self):
vm = shared.vm_pool.get(self.uuid.value)
2020-01-09 19:03:10 +00:00
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:
2020-01-09 19:03:10 +00:00
raise ValidationException('VM Already Stopped')
elif vm.status != VMStatus.running:
2020-01-09 19:03:10 +00:00
raise ValidationException('Cannot stop non-running VM')
class VmMigrationSchema(OTPSchema):
def __init__(self, data):
2020-01-09 19:03:10 +00:00
data['uuid'] = (
resolve_vm_name(
2020-01-09 19:03:10 +00:00
name=get(data, 'vm_name', return_default=True),
owner=(
2020-01-09 19:03:10 +00:00
get(data, 'in_support_of', return_default=True) or
get(data, 'name', return_default=True)
)
) or KeyError
)
self.uuid = VmUUIDField(data)
2020-01-09 19:03:10 +00:00
self.destination = Field('destination', str, get(data, 'destination'),
validators=[self.destination_validation])
2020-01-09 19:03:10 +00:00
super().__init__(data=data)
def destination_validation(self):
hostname = self.destination.value
2020-01-09 19:03:10 +00:00
host = next(filter(lambda h: h.hostname == hostname, shared.host_pool.hosts), None,)
if not host:
2020-01-09 19:03:10 +00:00
raise ValidationException('No Such Host ({}) exists'.format(self.destination.value))
elif host.status != HostStatus.alive:
2020-01-09 19:03:10 +00:00
raise ValidationException('Destination Host is dead')
else:
self.destination.value = host.key
def validation(self):
vm = shared.vm_pool.get(self.uuid.value)
2020-01-09 19:03:10 +00:00
if not (vm.value['owner'] == self.name.value or self.realm.value == 'ungleich-admin'):
raise ValidationException('Invalid User')
if vm.status != VMStatus.running:
2020-01-09 19:03:10 +00:00
raise ValidationException("Can't migrate non-running VM")
2020-01-09 19:03:10 +00:00
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):
2020-01-09 19:03:10 +00:00
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):
2020-01-09 19:03:10 +00:00
self.key_name = Field('key_name', str, get(data, 'key_name'))
super().__init__(data=data)
class GetSSHSchema(OTPSchema):
def __init__(self, data):
2020-01-09 19:03:10 +00:00
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):
2020-01-09 19:03:10 +00:00
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):
2020-01-09 19:03:10 +00:00
key = os.path.join(settings['etcd']['network_prefix'], self.name.value, self.network_name.value)
2020-01-03 13:38:59 +00:00
network = shared.etcd_client.get(key, value_in_json=True)
if network:
2020-01-09 19:03:10 +00:00
raise ValidationException('Network with name {} already exists'.format(self.network_name.value))
def network_type_validation(self):
2020-01-09 19:03:10 +00:00
supported_network_types = ['vxlan']
if self.type.value not in supported_network_types:
2020-01-09 19:03:10 +00:00
raise ValidationException('Unsupported Network Type. Supported network types are {}'.format(supported_network_types))