forked from uncloud/uncloud
		
	
		
			
				
	
	
		
			560 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			560 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
"""
 | 
						|
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
 | 
						|
 | 
						|
import bitmath
 | 
						|
 | 
						|
from uncloud.common.host import HostStatus
 | 
						|
from uncloud.common.vm import VMStatus
 | 
						|
from uncloud.shared import shared
 | 
						|
from uncloud.settings import settings
 | 
						|
from . import helper, logger
 | 
						|
from .common_fields import Field, VmUUIDField
 | 
						|
from .helper import check_otp, resolve_vm_name
 | 
						|
 | 
						|
 | 
						|
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):
 | 
						|
        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
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
    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
 | 
						|
 | 
						|
 | 
						|
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)
 | 
						|
        )
 | 
						|
 | 
						|
        # 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):
 | 
						|
        # 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)
 | 
						|
        )
 | 
						|
 | 
						|
        # 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)
 | 
						|
 | 
						|
    def image_validation(self):
 | 
						|
        try:
 | 
						|
            image_uuid = helper.resolve_image_name(
 | 
						|
                self.image.value, shared.etcd_client
 | 
						|
            )
 | 
						|
        except Exception as e:
 | 
						|
            logger.exception(
 | 
						|
                "Cannot resolve image name = %s", self.image.value
 | 
						|
            )
 | 
						|
            self.add_error(str(e))
 | 
						|
        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
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
    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
 | 
						|
                        )
 | 
						|
                    )
 | 
						|
 | 
						|
    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.")
 | 
						|
        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):
 | 
						|
        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")
 | 
						|
 | 
						|
 | 
						|
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):
 | 
						|
        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 (
 | 
						|
            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):
 | 
						|
        hostname = self.destination.value
 | 
						|
        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
 | 
						|
                )
 | 
						|
            )
 | 
						|
        elif host.status != HostStatus.alive:
 | 
						|
            self.add_error("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 vm.status != VMStatus.running:
 | 
						|
            self.add_error("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"
 | 
						|
            )
 | 
						|
 | 
						|
 | 
						|
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]
 | 
						|
        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))
 | 
						|
        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)
 | 
						|
 | 
						|
    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)
 | 
						|
        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
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
    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
 | 
						|
                )
 | 
						|
            )
 |