uncloud/uncloud/api/schemas.py

402 lines
15 KiB
Python
Executable File

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))