2019-10-25 06:42:40 +00:00
|
|
|
"""
|
|
|
|
This module contain classes thats validates and intercept/modify
|
|
|
|
data coming from ucloud-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
|
2019-11-02 15:42:24 +00:00
|
|
|
|
2019-11-18 17:39:57 +00:00
|
|
|
import bitmath
|
2019-10-25 06:42:40 +00:00
|
|
|
|
2019-12-03 10:40:41 +00:00
|
|
|
from ucloud.common.host import HostStatus
|
|
|
|
from ucloud.common.vm import VMStatus
|
2019-12-22 07:26:48 +00:00
|
|
|
from ucloud.shared import shared
|
|
|
|
from ucloud.settings import settings
|
2019-12-14 15:23:31 +00:00
|
|
|
from . import helper, logger
|
2019-11-18 17:39:57 +00:00
|
|
|
from .common_fields import Field, VmUUIDField
|
|
|
|
from .helper import check_otp, resolve_vm_name
|
2019-10-25 06:42:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
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 validation(self):
|
|
|
|
# custom validation is optional
|
|
|
|
return True
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
if self.__errors:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
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 validation(self):
|
|
|
|
if check_otp(self.name.value, self.realm.value, self.token.value) != 200:
|
|
|
|
self.add_error("Wrong Credentials")
|
|
|
|
|
|
|
|
|
|
|
|
########################## Image Operations ###############################################
|
|
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
def file_uuid_validation(self):
|
2019-12-22 07:26:48 +00:00
|
|
|
file_entry = shared.etcd_client.get(os.path.join(settings['etcd']['file_prefix'], self.uuid.value))
|
2019-10-25 06:42:40 +00:00
|
|
|
if file_entry is None:
|
|
|
|
self.add_error(
|
|
|
|
"Image File with uuid '{}' Not Found".format(self.uuid.value)
|
|
|
|
)
|
|
|
|
|
|
|
|
def image_store_name_validation(self):
|
2019-12-22 07:26:48 +00:00
|
|
|
image_stores = list(shared.etcd_client.get_prefix(settings['etcd']['image_store_prefix']))
|
2019-10-25 06:42:40 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
class CreateHostSchema(OTPSchema):
|
|
|
|
def __init__(self, data):
|
|
|
|
self.parsed_specs = {}
|
|
|
|
# Fields
|
|
|
|
self.specs = Field("specs", dict, data.get("specs", KeyError))
|
|
|
|
self.hostname = Field("hostname", str, data.get("hostname", KeyError))
|
|
|
|
|
|
|
|
# Validation
|
|
|
|
self.specs.validation = self.specs_validation
|
|
|
|
|
|
|
|
fields = [self.hostname, self.specs]
|
|
|
|
|
|
|
|
super().__init__(data=data, fields=fields)
|
|
|
|
|
|
|
|
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 _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(10):
|
|
|
|
self.add_error("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
|
|
|
|
else:
|
|
|
|
parsed_hdd.append(str(_parsed_hdd))
|
|
|
|
|
|
|
|
except ValueError:
|
|
|
|
# TODO: Find some good error message
|
|
|
|
self.add_error("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
|
|
|
|
}
|
|
|
|
|
|
|
|
def validation(self):
|
|
|
|
if self.realm.value != "ungleich-admin":
|
|
|
|
self.add_error("Invalid Credentials/Insufficient Permission")
|
|
|
|
|
|
|
|
|
|
|
|
# VM Operations
|
|
|
|
|
|
|
|
|
|
|
|
class CreateVMSchema(OTPSchema):
|
|
|
|
def __init__(self, data):
|
|
|
|
self.parsed_specs = {}
|
2019-11-18 17:39:57 +00:00
|
|
|
|
2019-10-25 06:42:40 +00:00
|
|
|
# Fields
|
|
|
|
self.specs = Field("specs", dict, data.get("specs", KeyError))
|
|
|
|
self.vm_name = Field("vm_name", str, data.get("vm_name", KeyError))
|
2019-11-02 15:42:24 +00:00
|
|
|
self.image = Field("image", str, data.get("image", KeyError))
|
2019-11-11 18:42:57 +00:00
|
|
|
self.network = Field("network", list, data.get("network", KeyError))
|
2019-10-25 06:42:40 +00:00
|
|
|
|
|
|
|
# Validation
|
2019-11-02 15:42:24 +00:00
|
|
|
self.image.validation = self.image_validation
|
2019-10-25 06:42:40 +00:00
|
|
|
self.vm_name.validation = self.vm_name_validation
|
|
|
|
self.specs.validation = self.specs_validation
|
2019-11-11 18:42:57 +00:00
|
|
|
self.network.validation = self.network_validation
|
2019-10-25 06:42:40 +00:00
|
|
|
|
2019-11-11 18:42:57 +00:00
|
|
|
fields = [self.vm_name, self.image, self.specs, self.network]
|
2019-10-25 06:42:40 +00:00
|
|
|
|
|
|
|
super().__init__(data=data, fields=fields)
|
|
|
|
|
2019-11-02 15:42:24 +00:00
|
|
|
def image_validation(self):
|
|
|
|
try:
|
2019-12-22 07:26:48 +00:00
|
|
|
image_uuid = helper.resolve_image_name(self.image.value, shared.etcd_client)
|
2019-11-02 15:42:24 +00:00
|
|
|
except Exception as e:
|
2019-12-14 15:23:31 +00:00
|
|
|
logger.exception("Cannot resolve image name = %s", self.image.value)
|
2019-11-02 15:42:24 +00:00
|
|
|
self.add_error(str(e))
|
|
|
|
else:
|
|
|
|
self.image_uuid = image_uuid
|
2019-10-25 06:42:40 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
)
|
|
|
|
|
2019-11-18 17:39:57 +00:00
|
|
|
def network_validation(self):
|
2019-11-11 18:42:57 +00:00
|
|
|
_network = self.network.value
|
|
|
|
|
|
|
|
if _network:
|
|
|
|
for net in _network:
|
2019-12-22 07:26:48 +00:00
|
|
|
network = shared.etcd_client.get(os.path.join(settings['etcd']['network_prefix'],
|
2019-11-18 17:39:57 +00:00
|
|
|
self.name.value,
|
|
|
|
net), value_in_json=True)
|
2019-11-11 18:42:57 +00:00
|
|
|
if not network:
|
2019-11-18 17:39:57 +00:00
|
|
|
self.add_error("Network with name {} does not exists" \
|
|
|
|
.format(net))
|
2019-11-11 18:42:57 +00:00
|
|
|
|
2019-10-25 06:42:40 +00:00
|
|
|
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 _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.")
|
|
|
|
else:
|
|
|
|
if self.get_errors():
|
|
|
|
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=data.get("vm_name", None),
|
|
|
|
owner=(data.get("in_support_of", None) or data.get("name", None)),
|
|
|
|
)
|
|
|
|
or KeyError
|
|
|
|
)
|
|
|
|
self.uuid = VmUUIDField(data)
|
|
|
|
|
|
|
|
fields = [self.uuid]
|
|
|
|
|
|
|
|
super().__init__(data, fields)
|
|
|
|
|
|
|
|
def validation(self):
|
2019-12-22 07:26:48 +00:00
|
|
|
vm = shared.vm_pool.get(self.uuid.value)
|
2019-10-25 06:42:40 +00:00
|
|
|
if not (
|
|
|
|
vm.value["owner"] == self.name.value or self.realm.value == "ungleich-admin"
|
|
|
|
):
|
|
|
|
self.add_error("Invalid User")
|
|
|
|
|
|
|
|
|
|
|
|
class VmActionSchema(OTPSchema):
|
|
|
|
def __init__(self, data):
|
|
|
|
data["uuid"] = (
|
|
|
|
resolve_vm_name(
|
|
|
|
name=data.get("vm_name", None),
|
|
|
|
owner=(data.get("in_support_of", None) or data.get("name", None)),
|
|
|
|
)
|
|
|
|
or KeyError
|
|
|
|
)
|
|
|
|
self.uuid = VmUUIDField(data)
|
|
|
|
self.action = Field("action", str, data.get("action", KeyError))
|
|
|
|
|
|
|
|
self.action.validation = self.action_validation
|
|
|
|
|
|
|
|
_fields = [self.uuid, self.action]
|
|
|
|
|
|
|
|
super().__init__(data=data, fields=_fields)
|
|
|
|
|
|
|
|
def action_validation(self):
|
|
|
|
allowed_actions = ["start", "stop", "delete"]
|
|
|
|
if self.action.value not in allowed_actions:
|
|
|
|
self.add_error(
|
|
|
|
"Invalid Action. Allowed Actions are {}".format(allowed_actions)
|
|
|
|
)
|
|
|
|
|
|
|
|
def validation(self):
|
2019-12-22 07:26:48 +00:00
|
|
|
vm = shared.vm_pool.get(self.uuid.value)
|
2019-10-25 06:42:40 +00:00
|
|
|
if not (
|
|
|
|
vm.value["owner"] == self.name.value or self.realm.value == "ungleich-admin"
|
|
|
|
):
|
|
|
|
self.add_error("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 == "stop":
|
|
|
|
if vm.status == VMStatus.stopped:
|
|
|
|
self.add_error("VM Already Stopped")
|
|
|
|
elif vm.status != VMStatus.running:
|
|
|
|
self.add_error("Cannot stop non-running VM")
|
|
|
|
|
|
|
|
|
|
|
|
class VmMigrationSchema(OTPSchema):
|
|
|
|
def __init__(self, data):
|
|
|
|
data["uuid"] = (
|
|
|
|
resolve_vm_name(
|
|
|
|
name=data.get("vm_name", None),
|
|
|
|
owner=(data.get("in_support_of", None) or data.get("name", None)),
|
|
|
|
)
|
|
|
|
or KeyError
|
|
|
|
)
|
|
|
|
|
|
|
|
self.uuid = VmUUIDField(data)
|
|
|
|
self.destination = Field("destination", str, data.get("destination", KeyError))
|
|
|
|
|
|
|
|
self.destination.validation = self.destination_validation
|
|
|
|
|
|
|
|
fields = [self.destination]
|
|
|
|
super().__init__(data=data, fields=fields)
|
|
|
|
|
|
|
|
def destination_validation(self):
|
2019-11-25 06:52:36 +00:00
|
|
|
hostname = self.destination.value
|
2019-12-22 07:26:48 +00:00
|
|
|
host = next(filter(lambda h: h.hostname == hostname, shared.host_pool.hosts), None)
|
2019-10-25 06:42:40 +00:00
|
|
|
if not host:
|
|
|
|
self.add_error("No Such Host ({}) exists".format(self.destination.value))
|
|
|
|
elif host.status != HostStatus.alive:
|
|
|
|
self.add_error("Destination Host is dead")
|
2019-11-25 06:52:36 +00:00
|
|
|
else:
|
|
|
|
self.destination.value = host.key
|
2019-10-25 06:42:40 +00:00
|
|
|
|
|
|
|
def validation(self):
|
2019-12-22 07:26:48 +00:00
|
|
|
vm = shared.vm_pool.get(self.uuid.value)
|
2019-10-25 06:42:40 +00:00
|
|
|
if not (
|
|
|
|
vm.value["owner"] == self.name.value or self.realm.value == "ungleich-admin"
|
|
|
|
):
|
|
|
|
self.add_error("Invalid User")
|
|
|
|
|
|
|
|
if vm.status != VMStatus.running:
|
|
|
|
self.add_error("Can't migrate non-running VM")
|
|
|
|
|
2019-12-22 07:26:48 +00:00
|
|
|
if vm.hostname == os.path.join(settings['etcd']['host_prefix'], self.destination.value):
|
2019-10-25 06:42:40 +00:00
|
|
|
self.add_error("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)
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
class GetSSHSchema(OTPSchema):
|
|
|
|
def __init__(self, data):
|
|
|
|
self.key_name = Field("key_name", str, data.get("key_name", None))
|
|
|
|
|
|
|
|
fields = [self.key_name]
|
2019-11-11 18:42:57 +00:00
|
|
|
super().__init__(data=data, fields=fields)
|
|
|
|
|
|
|
|
|
|
|
|
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))
|
2019-11-15 16:11:45 +00:00
|
|
|
self.user = Field("user", bool, bool(data.get("user", False)))
|
2019-11-11 18:42:57 +00:00
|
|
|
|
|
|
|
self.network_name.validation = self.network_name_validation
|
|
|
|
self.type.validation = self.network_type_validation
|
|
|
|
|
2019-11-15 16:11:45 +00:00
|
|
|
fields = [self.network_name, self.type, self.user]
|
2019-11-11 18:42:57 +00:00
|
|
|
super().__init__(data, fields=fields)
|
2019-11-18 17:39:57 +00:00
|
|
|
|
2019-11-11 18:42:57 +00:00
|
|
|
def network_name_validation(self):
|
2019-12-22 07:26:48 +00:00
|
|
|
network = shared.etcd_client.get(os.path.join(settings['etcd']['network_prefix'],
|
2019-11-18 17:39:57 +00:00
|
|
|
self.name.value,
|
|
|
|
self.network_name.value),
|
|
|
|
value_in_json=True)
|
2019-11-11 18:42:57 +00:00
|
|
|
if network:
|
2019-11-18 17:39:57 +00:00
|
|
|
self.add_error("Network with name {} already exists" \
|
|
|
|
.format(self.network_name.value))
|
2019-11-11 18:42:57 +00:00
|
|
|
|
|
|
|
def network_type_validation(self):
|
|
|
|
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))
|