Compare commits

...

2 commits

Author SHA1 Message Date
f1bb1ee3ca 2nd commit 2020-01-10 00:33:35 +05:00
e5dd5e45c6 initial work 2020-01-10 00:03:10 +05:00
8 changed files with 502 additions and 870 deletions

View file

@ -1,62 +0,0 @@
import os
from uncloud.common.shared import shared
from uncloud.common.settings import settings
class Optional:
pass
class Field:
def __init__(self, _name, _type, _value=None):
self.name = _name
self.value = _value
self.type = _type
self.__errors = []
def validation(self):
return True
def is_valid(self):
if self.value == KeyError:
self.add_error(
"'{}' field is a required field".format(self.name)
)
else:
if isinstance(self.value, Optional):
pass
elif not isinstance(self.value, self.type):
self.add_error(
"Incorrect Type for '{}' field".format(self.name)
)
else:
self.validation()
if self.__errors:
return False
return True
def get_errors(self):
return self.__errors
def add_error(self, error):
self.__errors.append(error)
class VmUUIDField(Field):
def __init__(self, data):
self.uuid = data.get("uuid", KeyError)
super().__init__("uuid", str, self.uuid)
self.validation = self.vm_uuid_validation
def vm_uuid_validation(self):
r = shared.etcd_client.get(
os.path.join(settings["etcd"]["vm_prefix"], self.uuid)
)
if not r:
self.add_error(
"VM with uuid {} does not exists".format(self.uuid)
)

View file

@ -14,7 +14,4 @@ data = {
'attributes': {'list': [], 'key': [], 'pool': 'images'}, 'attributes': {'list': [], 'key': [], 'pool': 'images'},
} }
shared.etcd_client.put( shared.etcd_client.put(os.path.join(settings['etcd']['image_store_prefix'], uuid4().hex), json.dumps(data))
os.path.join(settings['etcd']['image_store_prefix'], uuid4().hex),
json.dumps(data),
)

View file

@ -1,6 +1,4 @@
import binascii import binascii
import ipaddress
import random
import logging import logging
import requests import requests
@ -15,25 +13,19 @@ logger = logging.getLogger(__name__)
def check_otp(name, realm, token): def check_otp(name, realm, token):
try: try:
data = { data = {
"auth_name": settings["otp"]["auth_name"], 'auth_name': settings['otp']['auth_name'],
"auth_token": TOTP(settings["otp"]["auth_seed"]).now(), 'auth_token': TOTP(settings['otp']['auth_seed']).now(),
"auth_realm": settings["otp"]["auth_realm"], 'auth_realm': settings['otp']['auth_realm'],
"name": name, 'name': name,
"realm": realm, 'realm': realm,
"token": token, 'token': token,
} }
except binascii.Error as err: except binascii.Error:
logger.error( logger.error('Cannot compute OTP for seed: {}'.format(settings['otp']['auth_seed']))
"Cannot compute OTP for seed: {}".format(
settings["otp"]["auth_seed"]
)
)
return 400 return 400
else:
response = requests.post( response = requests.post(settings['otp']['verification_controller_url'], json=data)
settings["otp"]["verification_controller_url"], json=data return response.status_code
)
return response.status_code
def resolve_vm_name(name, owner): def resolve_vm_name(name, owner):
@ -43,29 +35,24 @@ def resolve_vm_name(name, owner):
Output: uuid of vm if found otherwise None Output: uuid of vm if found otherwise None
""" """
result = next( result = next(
filter( filter(lambda vm: vm.value['owner'] == owner and vm.value['name'] == name, shared.vm_pool.vms),
lambda vm: vm.value["owner"] == owner None
and vm.value["name"] == name,
shared.vm_pool.vms,
),
None,
) )
if result: if result:
return result.key.split("/")[-1] return result.key.split('/')[-1]
return None return None
def resolve_image_name(name, etcd_client): def resolve_image_name(name):
"""Return image uuid given its name and its store """Return image uuid given its name and its store
* If the provided name is not in correct format * If the provided name is not in correct format
i.e {store_name}:{image_name} return ValueError i.e {store_name}:{image_name} return ValueError
* If no such image found then return KeyError * If no such image found then return KeyError
""" """
seperator = ":" seperator = ':'
# Ensure, user/program passed valid name that is of type string # Ensure, user/program passed valid name that is of type string
try: try:
@ -73,78 +60,35 @@ def resolve_image_name(name, etcd_client):
""" """
Examples, where it would work and where it would raise exception Examples, where it would work and where it would raise exception
"images:alpine" --> ["images", "alpine"] 'images:alpine' --> ['images', 'alpine']
"images" --> ["images"] it would raise Exception as non enough value to unpack 'images' --> ['images'] it would raise Exception as non enough value to unpack
"images:alpine:meow" --> ["images", "alpine", "meow"] it would raise Exception 'images:alpine:meow' --> ['images', 'alpine', 'meow'] it would raise Exception
as too many values to unpack as too many values to unpack
""" """
store_name, image_name = store_name_and_image_name store_name, image_name = store_name_and_image_name
except Exception: except Exception:
raise ValueError( raise ValueError('Image name not in correct format i.e {store_name}:{image_name}')
"Image name not in correct format i.e {store_name}:{image_name}"
)
images = etcd_client.get_prefix( images = shared.etcd_client.get_prefix(settings['etcd']['image_prefix'], value_in_json=True)
settings["etcd"]["image_prefix"], value_in_json=True
)
# Try to find image with name == image_name and store_name == store_name # Try to find image with name == image_name and store_name == store_name
try: try:
image = next( image = next(
filter( filter(
lambda im: im.value["name"] == image_name lambda im: im.value['name'] == image_name
and im.value["store_name"] == store_name, and im.value['store_name'] == store_name,
images, images,
) )
) )
except StopIteration: except StopIteration:
raise KeyError("No image with name {} found.".format(name)) raise KeyError('No image with name {} found.'.format(name))
else: else:
image_uuid = image.key.split("/")[-1] image_uuid = image.key.split('/')[-1]
return image_uuid return image_uuid
def random_bytes(num=6): def make_return_message(err, status_code=200):
return [random.randrange(256) for _ in range(num)] return {'message': str(err)}, status_code
def generate_mac(
uaa=False, multicast=False, oui=None, separator=":", byte_fmt="%02x"
):
mac = random_bytes()
if oui:
if type(oui) == str:
oui = [int(chunk) for chunk in oui.split(separator)]
mac = oui + random_bytes(num=6 - len(oui))
else:
if multicast:
mac[0] |= 1 # set bit 0
else:
mac[0] &= ~1 # clear bit 0
if uaa:
mac[0] &= ~(1 << 1) # clear bit 1
else:
mac[0] |= 1 << 1 # set bit 1
return separator.join(byte_fmt % b for b in mac)
def mac2ipv6(mac, prefix):
# only accept MACs separated by a colon
parts = mac.split(":")
# modify parts to match IPv6 value
parts.insert(3, "ff")
parts.insert(4, "fe")
parts[0] = "%x" % (int(parts[0], 16) ^ 2)
# format output
ipv6_parts = [str(0)] * 4
for i in range(0, len(parts), 2):
ipv6_parts.append("".join(parts[i : i + 2]))
lower_part = ipaddress.IPv6Address(":".join(ipv6_parts))
prefix = ipaddress.IPv6Address(prefix)
return str(prefix + int(lower_part))

View file

@ -14,10 +14,13 @@ from uncloud.common.shared import shared
from uncloud.common import counters from uncloud.common import counters
from uncloud.common.vm import VMStatus from uncloud.common.vm import VMStatus
from uncloud.common.host import HostStatus
from uncloud.common.request import RequestEntry, RequestType from uncloud.common.request import RequestEntry, RequestType
from uncloud.common.settings import settings from uncloud.common.settings import settings
from . import schemas from uncloud.api import schemas
from .helper import generate_mac, mac2ipv6 from uncloud.api.schemas import ValidationException
from uncloud.common.network import generate_mac, mac2ipv6
from uncloud.api.helper import make_return_message
from uncloud import UncloudException from uncloud import UncloudException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,25 +33,28 @@ arg_parser = argparse.ArgumentParser('api', add_help=False)
arg_parser.add_argument('--port', '-p') arg_parser.add_argument('--port', '-p')
@app.errorhandler(Exception) # @app.errorhandler(Exception)
def handle_exception(e): # def handle_exception(e):
app.logger.error(e) # app.logger.error(e)
# pass through HTTP errors #
if isinstance(e, HTTPException): # # pass through HTTP errors
return e # if isinstance(e, HTTPException):
# return e
# now you're handling non-HTTP exceptions only #
return {'message': 'Server Error'}, 500 # # now you're handling non-HTTP exceptions only
# return {'message': 'Server Error'}, 500
class CreateVM(Resource): class CreateVM(Resource):
"""API Request to Handle Creation of VM"""
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.CreateVMSchema(data) try:
if validator.is_valid(): validator = schemas.CreateVMSchema(data)
validator.is_valid()
except ValidationException as err:
return make_return_message(err, 400)
else:
vm_uuid = uuid4().hex vm_uuid = uuid4().hex
vm_key = join_path(settings['etcd']['vm_prefix'], vm_uuid) vm_key = join_path(settings['etcd']['vm_prefix'], vm_uuid)
specs = { specs = {
@ -57,24 +63,22 @@ class CreateVM(Resource):
'os-ssd': validator.specs['os-ssd'], 'os-ssd': validator.specs['os-ssd'],
'hdd': validator.specs['hdd'], 'hdd': validator.specs['hdd'],
} }
macs = [generate_mac() for _ in range(len(data['network']))] macs = [generate_mac() for _ in range(len(validator.network))]
tap_ids = [ tap_ids = [
counters.increment_etcd_counter( counters.increment_etcd_counter(settings['etcd']['tap_counter'])
shared.etcd_client, settings['etcd']['tap_counter'] for _ in range(len(validator.network))
)
for _ in range(len(data['network']))
] ]
vm_entry = { vm_entry = {
'name': data['vm_name'], 'name': validator.vm_name,
'owner': data['name'], 'owner': validator.name,
'owner_realm': data['realm'], 'owner_realm': validator.realm,
'specs': specs, 'specs': specs,
'hostname': '', 'hostname': '',
'status': VMStatus.stopped, 'status': VMStatus.stopped,
'image_uuid': validator.image_uuid, 'image_uuid': validator.image_uuid,
'log': [], 'log': [],
'vnc_socket': '', 'vnc_socket': '',
'network': list(zip(data['network'], macs, tap_ids)), 'network': list(zip(validator.network, macs, tap_ids)),
'metadata': {'ssh-keys': []}, 'metadata': {'ssh-keys': []},
'in_migration': False, 'in_migration': False,
} }
@ -87,86 +91,76 @@ class CreateVM(Resource):
request_prefix=settings['etcd']['request_prefix'], request_prefix=settings['etcd']['request_prefix'],
) )
shared.request_pool.put(r) shared.request_pool.put(r)
return make_return_message('VM Creation Queued')
return {'message': 'VM Creation Queued'}, 200
return validator.get_errors(), 400
class VmStatus(Resource): class GetVMStatus(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.VMStatusSchema(data) try:
if validator.is_valid(): validator = schemas.VMStatusSchema(data)
vm = shared.vm_pool.get( validator.is_valid()
join_path(settings['etcd']['vm_prefix'], data['uuid']) except (ValidationException, Exception) as err:
) return make_return_message(err, 400)
else:
vm = shared.vm_pool.get(join_path(settings['etcd']['vm_prefix'], validator.uuid))
vm_value = vm.value.copy() vm_value = vm.value.copy()
vm_value['ip'] = [] vm_value['ip'] = []
for network_mac_and_tap in vm.network: for network_mac_and_tap in vm.network:
network_name, mac, tap = network_mac_and_tap network_name, mac, tap = network_mac_and_tap
network = shared.etcd_client.get( network = shared.etcd_client.get(
join_path( join_path(settings['etcd']['network_prefix'], validator.name, network_name),
settings['etcd']['network_prefix'],
data['name'],
network_name,
),
value_in_json=True, value_in_json=True,
) )
ipv6_addr = ( ipv6_addr = (network.value.get('ipv6').split('::')[0] + '::')
network.value.get('ipv6').split('::')[0] + '::'
)
vm_value['ip'].append(mac2ipv6(mac, ipv6_addr)) vm_value['ip'].append(mac2ipv6(mac, ipv6_addr))
vm.value = vm_value vm.value = vm_value
return vm.value return vm.value, 200
else:
return validator.get_errors(), 400
class CreateImage(Resource): class CreateImage(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.CreateImageSchema(data) try:
if validator.is_valid(): validator = schemas.CreateImageSchema(data)
file_entry = shared.etcd_client.get( validator.is_valid()
join_path(settings['etcd']['file_prefix'], data['uuid']) except ValidationException as err:
) return make_return_message(err, 400)
file_entry_value = json.loads(file_entry.value) else:
try:
file_entry = shared.etcd_client.get(
join_path(settings['etcd']['file_prefix'], validator.uuid), value_in_json=True
)
except KeyError:
# TODO: Add some message
pass
else:
image_entry_json = {
'status': 'TO_BE_CREATED',
'owner': file_entry.value['owner'],
'filename': file_entry.value['filename'],
'name': validator.name,
'store_name': validator.image_store,
'visibility': 'public',
}
shared.etcd_client.put(
join_path(settings['etcd']['image_prefix'], validator.uuid),
json.dumps(image_entry_json),
)
image_entry_json = { return {'message': 'Image queued for creation.'}, 200
'status': 'TO_BE_CREATED',
'owner': file_entry_value['owner'],
'filename': file_entry_value['filename'],
'name': data['name'],
'store_name': data['image_store'],
'visibility': 'public',
}
shared.etcd_client.put(
join_path(
settings['etcd']['image_prefix'], data['uuid']
),
json.dumps(image_entry_json),
)
return {'message': 'Image queued for creation.'}
return validator.get_errors(), 400
class ListPublicImages(Resource): class ListPublicImages(Resource):
@staticmethod @staticmethod
def get(): def get():
images = shared.etcd_client.get_prefix( images = shared.etcd_client.get_prefix(settings['etcd']['image_prefix'], value_in_json=True)
settings['etcd']['image_prefix'], value_in_json=True
)
r = {'images': []} r = {'images': []}
for image in images: for image in images:
image_key = '{}:{}'.format( image_key = '{}:{}'.format(image.value['store_name'], image.value['name'])
image.value['store_name'], image.value['name'] r['images'].append({'name': image_key, 'status': image.value['status']})
)
r['images'].append(
{'name': image_key, 'status': image.value['status']}
)
return r, 200 return r, 200
@ -174,92 +168,79 @@ class VMAction(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.VmActionSchema(data) try:
validator = schemas.VmActionSchema(data)
if validator.is_valid(): validator.is_valid()
vm_entry = shared.vm_pool.get( except ValidationException as err:
join_path(settings['etcd']['vm_prefix'], data['uuid']) return make_return_message(err, 400)
) else:
action = data['action'] vm_entry = shared.vm_pool.get(join_path(settings['etcd']['vm_prefix'], validator.uuid))
action = validator.action
if action == 'start': if action == 'start':
action = 'schedule' action = 'schedule'
if action == 'delete' and vm_entry.hostname == '': if action == 'delete' and vm_entry.hostname == '':
if shared.storage_handler.is_vm_image_exists( if shared.storage_handler.is_vm_image_exists(vm_entry.uuid):
vm_entry.uuid r_status = shared.storage_handler.delete_vm_image(vm_entry.uuid)
):
r_status = shared.storage_handler.delete_vm_image(
vm_entry.uuid
)
if r_status: if r_status:
shared.etcd_client.client.delete(vm_entry.key) shared.etcd_client.client.delete(vm_entry.key)
return {'message': 'VM successfully deleted'} return make_return_message('VM successfully deleted')
else: else:
logger.error( logger.error('Some Error Occurred while deleting VM')
'Some Error Occurred while deleting VM' return make_return_message('VM deletion unsuccessfull')
)
return {'message': 'VM deletion unsuccessfull'}
else: else:
shared.etcd_client.client.delete(vm_entry.key) shared.etcd_client.client.delete(vm_entry.key)
return {'message': 'VM successfully deleted'} return make_return_message('VM successfully deleted')
r = RequestEntry.from_scratch( r = RequestEntry.from_scratch(
type='{}VM'.format(action.title()), type='{}VM'.format(action.title()),
uuid=data['uuid'], uuid=validator.uuid,
hostname=vm_entry.hostname, hostname=vm_entry.hostname,
request_prefix=settings['etcd']['request_prefix'], request_prefix=settings['etcd']['request_prefix'],
) )
shared.request_pool.put(r) shared.request_pool.put(r)
return ( return make_return_message('VM {} Queued'.format(action.title()))
{'message': 'VM {} Queued'.format(action.title())},
200,
)
else:
return validator.get_errors(), 400
class VMMigration(Resource): class VMMigration(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.VmMigrationSchema(data) try:
validator = schemas.VmMigrationSchema(data)
if validator.is_valid(): validator.is_valid()
vm = shared.vm_pool.get(data['uuid']) except ValidationException as err:
return make_return_message(err), 400
else:
vm = shared.vm_pool.get(validator.uuid)
r = RequestEntry.from_scratch( r = RequestEntry.from_scratch(
type=RequestType.InitVMMigration, type=RequestType.InitVMMigration,
uuid=vm.uuid, uuid=vm.uuid,
hostname=join_path( hostname=join_path(
settings['etcd']['host_prefix'], settings['etcd']['host_prefix'],
validator.destination.value, validator.destination,
), ),
request_prefix=settings['etcd']['request_prefix'], request_prefix=settings['etcd']['request_prefix'],
) )
shared.request_pool.put(r) shared.request_pool.put(r)
return ( return make_return_message('VM Migration Initialization Queued')
{'message': 'VM Migration Initialization Queued'},
200,
)
else:
return validator.get_errors(), 400
class ListUserVM(Resource): class ListUserVM(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.OTPSchema(data) try:
validator = schemas.OTPSchema(data)
if validator.is_valid(): validator.is_valid()
vms = shared.etcd_client.get_prefix( except ValidationException as err:
settings['etcd']['vm_prefix'], value_in_json=True return make_return_message(err, 400)
) else:
vms = shared.etcd_client.get_prefix(settings['etcd']['vm_prefix'], value_in_json=True)
return_vms = [] return_vms = []
user_vms = filter( user_vms = filter(lambda v: v.value['owner'] == validator.name, vms)
lambda v: v.value['owner'] == data['name'], vms
)
for vm in user_vms: for vm in user_vms:
return_vms.append( return_vms.append(
{ {
@ -271,26 +252,22 @@ class ListUserVM(Resource):
'vnc_socket': vm.value.get('vnc_socket', None), 'vnc_socket': vm.value.get('vnc_socket', None),
} }
) )
if return_vms: return make_return_message(return_vms)
return {'message': return_vms}, 200
return {'message': 'No VM found'}, 404
else:
return validator.get_errors(), 400
class ListUserFiles(Resource): class ListUserFiles(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.OTPSchema(data) try:
validator = schemas.OTPSchema(data)
if validator.is_valid(): validator.is_valid()
files = shared.etcd_client.get_prefix( except ValidationException as err:
settings['etcd']['file_prefix'], value_in_json=True return make_return_message(err, 400)
) else:
files = shared.etcd_client.get_prefix(settings['etcd']['file_prefix'], value_in_json=True)
return_files = [] return_files = []
user_files = [f for f in files if f.value['owner'] == data['name']] user_files = [f for f in files if f.value['owner'] == validator.name]
for file in user_files: for file in user_files:
file_uuid = file.key.split('/')[-1] file_uuid = file.key.split('/')[-1]
file = file.value file = file.value
@ -300,33 +277,28 @@ class ListUserFiles(Resource):
file.pop('owner', None) file.pop('owner', None)
return_files.append(file) return_files.append(file)
return {'message': return_files}, 200 return make_return_message(return_files)
else:
return validator.get_errors(), 400
class CreateHost(Resource): class CreateHost(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.CreateHostSchema(data) try:
if validator.is_valid(): validator = schemas.CreateHostSchema(data)
host_key = join_path( validator.is_valid()
settings['etcd']['host_prefix'], uuid4().hex except ValidationException as err:
) return make_return_message(err, 400)
else:
host_key = join_path(settings['etcd']['host_prefix'], uuid4().hex)
host_entry = { host_entry = {
'specs': data['specs'], 'specs': validator.specs,
'hostname': data['hostname'], 'hostname': validator.hostname,
'status': 'DEAD', 'status': HostStatus.dead,
'last_heartbeat': '', 'last_heartbeat': '',
} }
shared.etcd_client.put( shared.etcd_client.put(host_key, host_entry, value_in_json=True)
host_key, host_entry, value_in_json=True return make_return_message('Host Created.')
)
return {'message': 'Host Created'}, 200
return validator.get_errors(), 400
class ListHost(Resource): class ListHost(Resource):
@ -348,200 +320,142 @@ class GetSSHKeys(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.GetSSHSchema(data) try:
if validator.is_valid(): validator = schemas.GetSSHSchema(data)
if not validator.key_name.value: except ValidationException as err:
return make_return_message(err, 400)
# {user_prefix}/{realm}/{name}/key/ else:
etcd_key = join_path( etcd_key = join_path(settings['etcd']['user_prefix'], validator.realm,
settings['etcd']['user_prefix'], validator.name, 'key')
data['realm'], if not validator.key_name:
data['name'], etcd_entry = shared.etcd_client.get_prefix(etcd_key, value_in_json=True)
'key',
)
etcd_entry = shared.etcd_client.get_prefix(
etcd_key, value_in_json=True
)
keys = { keys = {
key.key.split('/')[-1]: key.value key.key.split('/')[-1]: key.value
for key in etcd_entry for key in etcd_entry
} }
return {'keys': keys} return {'keys': keys}
else: else:
etcd_key = join_path(validator.key_name)
# {user_prefix}/{realm}/{name}/key/{key_name} try:
etcd_key = join_path( etcd_entry = shared.etcd_client.get(etcd_key, value_in_json=True)
settings['etcd']['user_prefix'], except KeyError:
data['realm'], return make_return_message('No such key found.', 400)
data['name'],
'key',
data['key_name'],
)
etcd_entry = shared.etcd_client.get(
etcd_key, value_in_json=True
)
if etcd_entry:
return {
'keys': {
etcd_entry.key.split('/')[
-1
]: etcd_entry.value
}
}
else: else:
return {'keys': {}} return {
else: 'keys': {etcd_entry.key.split('/')[-1]: etcd_entry.value}
return validator.get_errors(), 400 }
class AddSSHKey(Resource): class AddSSHKey(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.AddSSHSchema(data) try:
if validator.is_valid(): validator = schemas.AddSSHSchema(data)
validator.is_valid()
except ValidationException as err:
return make_return_message(err, 400)
else:
# {user_prefix}/{realm}/{name}/key/{key_name} # {user_prefix}/{realm}/{name}/key/{key_name}
etcd_key = join_path( etcd_key = join_path(
settings['etcd']['user_prefix'], settings['etcd']['user_prefix'], validator.realm,
data['realm'], validator.name, 'key', validator.key_name
data['name'],
'key',
data['key_name'],
) )
etcd_entry = shared.etcd_client.get( try:
etcd_key, value_in_json=True shared.etcd_client.get(etcd_key, value_in_json=True)
) except KeyError:
if etcd_entry:
return {
'message': 'Key with name "{}" already exists'.format(
data['key_name']
)
}
else:
# Key Not Found. It implies user' haven't added any key yet. # Key Not Found. It implies user' haven't added any key yet.
shared.etcd_client.put( shared.etcd_client.put(etcd_key, validator.key, value_in_json=True)
etcd_key, data['key'], value_in_json=True return make_return_message('Key added successfully')
) else:
return {'message': 'Key added successfully'} return make_return_message('Key "{}" already exists'.format(validator.key_name))
else:
return validator.get_errors(), 400
class RemoveSSHKey(Resource): class RemoveSSHKey(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.RemoveSSHSchema(data) try:
if validator.is_valid(): validator = schemas.RemoveSSHSchema(data)
validator.is_valid()
except ValidationException as err:
return make_return_message(err, 400)
else:
# {user_prefix}/{realm}/{name}/key/{key_name} # {user_prefix}/{realm}/{name}/key/{key_name}
etcd_key = join_path( etcd_key = join_path(settings['etcd']['user_prefix'], validator.realm,
settings['etcd']['user_prefix'], validator.name, 'key', validator.key_name)
data['realm'], try:
data['name'], etcd_entry = shared.etcd_client.get(etcd_key, value_in_json=True)
'key', except KeyError:
data['key_name'], return make_return_message('No Key "{}" exists.'.format(validator.key_name))
)
etcd_entry = shared.etcd_client.get(
etcd_key, value_in_json=True
)
if etcd_entry: if etcd_entry:
shared.etcd_client.client.delete(etcd_key) shared.etcd_client.client.delete(etcd_key)
return {'message': 'Key successfully removed.'} return {'message': 'Key successfully removed.'}
else:
return {
'message': 'No Key with name "{}" Exists at all.'.format(
data['key_name']
)
}
else:
return validator.get_errors(), 400
class CreateNetwork(Resource): class CreateNetwork(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.CreateNetwork(data) try:
validator = schemas.CreateNetwork(data)
if validator.is_valid(): validator.is_valid()
except ValidationException as err:
return make_return_message(err, 400)
else:
network_entry = { network_entry = {
'id': counters.increment_etcd_counter( 'id': counters.increment_etcd_counter(settings['etcd']['vxlan_counter']),
shared.etcd_client, settings['etcd']['vxlan_counter'] 'type': validator.type,
),
'type': data['type'],
} }
if validator.user.value: if validator.user:
try: try:
nb = pynetbox.api( nb = pynetbox.api(url=settings['netbox']['url'], token=settings['netbox']['token'])
url=settings['netbox']['url'], nb_prefix = nb.ipam.prefixes.get(prefix=settings['network']['prefix'])
token=settings['netbox']['token'],
)
nb_prefix = nb.ipam.prefixes.get(
prefix=settings['network']['prefix']
)
prefix = nb_prefix.available_prefixes.create( prefix = nb_prefix.available_prefixes.create(
data={ data={
'prefix_length': int( 'prefix_length': int(settings['network']['prefix_length']),
settings['network']['prefix_length']
),
'description': '{}\'s network "{}"'.format( 'description': '{}\'s network "{}"'.format(
data['name'], data['network_name'] validator.name,
validator.network_name
), ),
'is_pool': True, 'is_pool': True,
} }
) )
except Exception as err: except Exception as err:
app.logger.error(err) app.logger.error(err)
return { return make_return_message('Error occured while creating network.', 400)
'message': 'Error occured while creating network.'
}
else: else:
network_entry['ipv6'] = prefix['prefix'] network_entry['ipv6'] = prefix['prefix']
else: else:
network_entry['ipv6'] = 'fd00::/64' network_entry['ipv6'] = 'fd00::/64'
network_key = join_path( network_key = join_path(settings['etcd']['network_prefix'], validator.name,
settings['etcd']['network_prefix'], validator.network_name)
data['name'], shared.etcd_client.put(network_key, network_entry, value_in_json=True)
data['network_name'], return make_return_message('Network successfully added.')
)
shared.etcd_client.put(
network_key, network_entry, value_in_json=True
)
return {'message': 'Network successfully added.'}
else:
return validator.get_errors(), 400
class ListUserNetwork(Resource): class ListUserNetwork(Resource):
@staticmethod @staticmethod
def post(): def post():
data = request.json data = request.json
validator = schemas.OTPSchema(data) try:
validator = schemas.OTPSchema(data)
if validator.is_valid(): validator.is_valid()
prefix = join_path( except ValidationException as err:
settings['etcd']['network_prefix'], data['name'] return make_return_message(err, 400)
) else:
networks = shared.etcd_client.get_prefix( prefix = join_path(settings['etcd']['network_prefix'], validator.name)
prefix, value_in_json=True networks = shared.etcd_client.get_prefix(prefix, value_in_json=True)
)
user_networks = [] user_networks = []
for net in networks: for net in networks:
net.value['name'] = net.key.split('/')[-1] net.value['name'] = net.key.split('/')[-1]
user_networks.append(net.value) user_networks.append(net.value)
return {'networks': user_networks}, 200 return {'networks': user_networks}, 200
else:
return validator.get_errors(), 400
api.add_resource(CreateVM, '/vm/create') api.add_resource(CreateVM, '/vm/create')
api.add_resource(VmStatus, '/vm/status') api.add_resource(GetVMStatus, '/vm/status')
api.add_resource(VMAction, '/vm/action') api.add_resource(VMAction, '/vm/action')
api.add_resource(VMMigration, '/vm/migrate') api.add_resource(VMMigration, '/vm/migrate')
@ -565,37 +479,7 @@ api.add_resource(CreateNetwork, '/network/create')
def main(debug=False, port=None): def main(debug=False, port=None):
try: try:
image_stores = list( app.run(host='::', port=port, debug=debug)
shared.etcd_client.get_prefix(
settings['etcd']['image_store_prefix'], value_in_json=True
)
)
except KeyError:
image_stores = False
# Do not inject default values that might be very wrong
# fail when required, not before
#
# if not image_stores:
# data = {
# 'is_public': True,
# 'type': 'ceph',
# 'name': 'images',
# 'description': 'first ever public image-store',
# 'attributes': {'list': [], 'key': [], 'pool': 'images'},
# }
# shared.etcd_client.put(
# join_path(
# settings['etcd']['image_store_prefix'], uuid4().hex
# ),
# json.dumps(data),
# )
try:
app.run(host='::',
port=port,
debug=debug)
except OSError as e: except OSError as e:
raise UncloudException('Failed to start Flask: {}'.format(e)) raise UncloudException('Failed to start Flask: {}'.format(e))

View file

@ -1,19 +1,3 @@
"""
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 json
import os import os
@ -23,19 +7,53 @@ from uncloud.common.host import HostStatus
from uncloud.common.vm import VMStatus from uncloud.common.vm import VMStatus
from uncloud.common.shared import shared from uncloud.common.shared import shared
from uncloud.common.settings import settings from uncloud.common.settings import settings
from . import helper, logger from uncloud.api import helper
from .common_fields import Field, VmUUIDField from uncloud.api.helper import check_otp, resolve_vm_name
from .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: class BaseSchema:
def __init__(self, data, fields=None): def __init__(self):
_ = data # suppress linter warning self.fields = [getattr(self, field) for field in dir(self) if isinstance(getattr(self, field), Field)]
self.__errors = []
if fields is None:
self.fields = []
else:
self.fields = fields
def validation(self): def validation(self):
# custom validation is optional # custom validation is optional
@ -44,517 +62,340 @@ class BaseSchema:
def is_valid(self): def is_valid(self):
for field in self.fields: for field in self.fields:
field.is_valid() field.is_valid()
self.add_field_errors(field)
for parent in self.__class__.__bases__: for parent in self.__class__.__bases__:
try: parent.validation(self)
parent.validation(self)
except AttributeError:
pass
if not self.__errors:
self.validation()
if self.__errors: self.validation()
return False
return True
def get_errors(self): for field in self.fields:
return {"message": self.__errors} setattr(self, field.name, field.value)
def add_field_errors(self, field: Field):
self.__errors += field.get_errors()
def add_error(self, error): def get(dictionary: dict, key: str, return_default=False, default=None):
self.__errors.append(error) 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): class OTPSchema(BaseSchema):
def __init__(self, data: dict, fields=None): def __init__(self, data: dict):
self.name = Field("name", str, data.get("name", KeyError)) self.name = Field('name', str, get(data, 'name'))
self.realm = Field("realm", str, data.get("realm", KeyError)) self.realm = Field('realm', str, get(data, 'realm'))
self.token = Field("token", str, data.get("token", KeyError)) self.token = Field('token', str, get(data, 'token'))
super().__init__()
_fields = [self.name, self.realm, self.token]
if fields:
_fields += fields
super().__init__(data=data, fields=_fields)
def validation(self): def validation(self):
if ( if check_otp(self.name.value, self.realm.value, self.token.value) != 200:
check_otp( raise ValidationException('Wrong Credentials')
self.name.value, self.realm.value, self.token.value
)
!= 200
):
self.add_error("Wrong Credentials")
########################## Image Operations ###############################################
class CreateImageSchema(BaseSchema): class CreateImageSchema(BaseSchema):
def __init__(self, data): def __init__(self, data):
# Fields self.uuid = Field('uuid', str, get(data, 'uuid'), validators=[self.file_uuid_validation])
self.uuid = Field("uuid", str, data.get("uuid", KeyError)) self.name = Field('name', str, get(data, 'name'))
self.name = Field("name", str, data.get("name", KeyError)) self.image_store = Field('image_store', str, get(data, 'image_store'),
self.image_store = Field( validators=[self.image_store_name_validation])
"image_store", str, data.get("image_store", KeyError) super().__init__()
)
# 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): def file_uuid_validation(self):
file_entry = shared.etcd_client.get( try:
os.path.join( shared.etcd_client.get(os.path.join(settings['etcd']['file_prefix'], self.uuid.value))
settings["etcd"]["file_prefix"], self.uuid.value except KeyError:
) raise ValidationException("Image File with uuid '{}' Not Found".format(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): def image_store_name_validation(self):
image_stores = list( image_stores = list(shared.etcd_client.get_prefix(settings['etcd']['image_store_prefix']))
shared.etcd_client.get_prefix( try:
settings["etcd"]["image_store_prefix"] 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))
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): class CreateHostSchema(OTPSchema):
def __init__(self, data): def __init__(self, data):
# Fields self.specs = Field('specs', dict, get(data, 'specs'), validators=[self.specs_validation])
self.specs = Field("specs", dict, data.get("specs", KeyError)) self.hostname = Field('hostname', str, get(data, 'hostname'))
self.hostname = Field(
"hostname", str, data.get("hostname", KeyError)
)
# Validation super().__init__(data)
self.specs.validation = self.specs_validation
fields = [self.hostname, self.specs]
super().__init__(data=data, fields=fields)
def specs_validation(self): def specs_validation(self):
ALLOWED_BASE = 10 allowed_base = 10
_cpu = self.specs.value.get("cpu", KeyError) _cpu = self.specs.value.get('cpu', KeyError)
_ram = self.specs.value.get("ram", KeyError) _ram = self.specs.value.get('ram', KeyError)
_os_ssd = self.specs.value.get("os-ssd", KeyError) _os_ssd = self.specs.value.get('os-ssd', KeyError)
_hdd = self.specs.value.get("hdd", KeyError) _hdd = self.specs.value.get('hdd', KeyError)
if KeyError in [_cpu, _ram, _os_ssd, _hdd]: if KeyError in [_cpu, _ram, _os_ssd]:
self.add_error( raise ValidationException('You must specify CPU, RAM and OS-SSD in your specs')
"You must specify CPU, RAM and OS-SSD in your specs"
)
return None
try: try:
parsed_ram = bitmath.parse_string_unsafe(_ram) parsed_ram = bitmath.parse_string_unsafe(_ram)
parsed_os_ssd = bitmath.parse_string_unsafe(_os_ssd) parsed_os_ssd = bitmath.parse_string_unsafe(_os_ssd)
if parsed_ram.base != ALLOWED_BASE: if parsed_ram.base != allowed_base:
self.add_error( raise ValidationException('Your specified RAM is not in correct units')
"Your specified RAM is not in correct units"
) if parsed_os_ssd.base != allowed_base:
if parsed_os_ssd.base != ALLOWED_BASE: raise ValidationException('Your specified OS-SSD is not in correct units')
self.add_error(
"Your specified OS-SSD is not in correct units"
)
if _cpu < 1: if _cpu < 1:
self.add_error("CPU must be atleast 1") raise ValidationException('CPU must be atleast 1')
if parsed_ram < bitmath.GB(1): if parsed_ram < bitmath.GB(1):
self.add_error("RAM must be atleast 1 GB") raise ValidationException('RAM must be atleast 1 GB')
if parsed_os_ssd < bitmath.GB(10): if parsed_os_ssd < bitmath.GB(10):
self.add_error("OS-SSD must be atleast 10 GB") raise ValidationException('OS-SSD must be atleast 10 GB')
parsed_hdd = [] parsed_hdd = []
for hdd in _hdd: for hdd in _hdd:
_parsed_hdd = bitmath.parse_string_unsafe(hdd) _parsed_hdd = bitmath.parse_string_unsafe(hdd)
if _parsed_hdd.base != ALLOWED_BASE: if _parsed_hdd.base != allowed_base:
self.add_error( raise ValidationException('Your specified HDD is not in correct units')
"Your specified HDD is not in correct units"
)
break
else: else:
parsed_hdd.append(str(_parsed_hdd)) parsed_hdd.append(str(_parsed_hdd))
except ValueError: except ValueError:
# TODO: Find some good error message raise ValidationException('Specs are not correct.')
self.add_error("Specs are not correct.")
else: else:
if self.get_errors(): self.specs = {
self.specs = { 'cpu': _cpu,
"cpu": _cpu, 'ram': str(parsed_ram),
"ram": str(parsed_ram), 'os-ssd': str(parsed_os_ssd),
"os-ssd": str(parsed_os_ssd), 'hdd': parsed_hdd,
"hdd": parsed_hdd, }
}
def validation(self): def validation(self):
if self.realm.value != "ungleich-admin": if self.realm.value != 'ungleich-admin':
self.add_error( raise ValidationException('Invalid Credentials/Insufficient Permission')
"Invalid Credentials/Insufficient Permission"
)
# VM Operations
class CreateVMSchema(OTPSchema): class CreateVMSchema(OTPSchema):
def __init__(self, data): def __init__(self, data):
# Fields self.specs = Field('specs', dict, get(data, 'specs'), validators=[self.specs_validation])
self.specs = Field("specs", dict, data.get("specs", KeyError)) self.vm_name = Field('vm_name', str, get(data, 'vm_name'), validators=[self.vm_name_validation])
self.vm_name = Field( self.image = Field('image', str, get(data, 'image'), validators=[self.image_validation])
"vm_name", str, data.get("vm_name", KeyError) self.network = Field('network', list, get(data, 'network', return_default=True, default=[]),
) validators=[self.network_validation])
self.image = Field("image", str, data.get("image", KeyError)) self.image_uuid = None
self.network = Field(
"network", list, data.get("network", KeyError)
)
# Validation super().__init__(data=data)
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): def image_validation(self):
try: try:
image_uuid = helper.resolve_image_name( image_uuid = helper.resolve_image_name(self.image.value)
self.image.value, shared.etcd_client except Exception:
) raise ValidationException('No image of name \'{}\' found'.format(self.image.value))
except Exception as e:
logger.exception(
"Cannot resolve image name = %s", self.image.value
)
self.add_error(str(e))
else: else:
self.image_uuid = image_uuid self.image_uuid = image_uuid
def vm_name_validation(self): def vm_name_validation(self):
if resolve_vm_name( if resolve_vm_name(name=self.vm_name.value, owner=self.name.value):
name=self.vm_name.value, owner=self.name.value raise ValidationException("VM with same name '{}' already exists".format(self.vm_name.value))
):
self.add_error(
'VM with same name "{}" already exists'.format(
self.vm_name.value
)
)
def network_validation(self): def network_validation(self):
_network = self.network.value _network = self.network.value
if _network: if _network:
for net in _network: for net in _network:
network = shared.etcd_client.get( try:
os.path.join( shared.etcd_client.get(
settings["etcd"]["network_prefix"], os.path.join(settings['etcd']['network_prefix'], self.name.value, net),
self.name.value, value_in_json=True
net,
),
value_in_json=True,
)
if not network:
self.add_error(
"Network with name {} does not exists".format(
net
)
) )
except KeyError:
raise ValidationException('Network with name {} does not exists'.format(net))
def specs_validation(self): def specs_validation(self):
ALLOWED_BASE = 10 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: try:
parsed_ram = bitmath.parse_string_unsafe(_ram) _cpu = get(self.specs.value, 'cpu')
parsed_os_ssd = bitmath.parse_string_unsafe(_os_ssd) _ram = get(self.specs.value, 'ram')
_os_ssd = get(self.specs.value, 'os-ssd')
if parsed_ram.base != ALLOWED_BASE: _hdd = get(self.specs.value, 'hdd', return_default=True, default=[])
self.add_error( except (KeyError, Exception):
"Your specified RAM is not in correct units" raise ValidationException('You must specify CPU, RAM and OS-SSD in your specs')
)
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: else:
if self.get_errors(): 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 = { self.specs = {
"cpu": _cpu, 'cpu': _cpu,
"ram": str(parsed_ram), 'ram': str(parsed_ram),
"os-ssd": str(parsed_os_ssd), 'os-ssd': str(parsed_os_ssd),
"hdd": parsed_hdd, 'hdd': parsed_hdd,
} }
class VMStatusSchema(OTPSchema): class VMStatusSchema(OTPSchema):
def __init__(self, data): def __init__(self, data):
data["uuid"] = ( data['uuid'] = (
resolve_vm_name( resolve_vm_name(
name=data.get("vm_name", None), name=get(data, 'vm_name', return_default=True),
owner=( owner=(
data.get("in_support_of", None) get(data, 'in_support_of', return_default=True) or
or data.get("name", None) get(data, 'name', return_default=True)
), )
) )
or KeyError or KeyError
) )
self.uuid = VmUUIDField(data) self.uuid = VmUUIDField(data)
fields = [self.uuid] super().__init__(data)
super().__init__(data, fields)
def validation(self): def validation(self):
vm = shared.vm_pool.get(self.uuid.value) vm = shared.vm_pool.get(self.uuid.value)
if not ( if not (vm.value['owner'] == self.name.value or self.realm.value == 'ungleich-admin'):
vm.value["owner"] == self.name.value raise ValidationException('Invalid User')
or self.realm.value == "ungleich-admin"
):
self.add_error("Invalid User")
class VmActionSchema(OTPSchema): class VmActionSchema(OTPSchema):
def __init__(self, data): def __init__(self, data):
data["uuid"] = ( data['uuid'] = (
resolve_vm_name( resolve_vm_name(
name=data.get("vm_name", None), name=get(data, 'vm_name', return_default=True),
owner=( owner=(
data.get("in_support_of", None) get(data, 'in_support_of', return_default=True) or
or data.get("name", None) get(data, 'name', return_default=True)
), )
) )
or KeyError or KeyError
) )
self.uuid = VmUUIDField(data) self.uuid = VmUUIDField(data)
self.action = Field("action", str, data.get("action", KeyError)) self.action = Field('action', str, get(data, 'action'), validators=[self.action_validation])
self.action.validation = self.action_validation super().__init__(data=data)
_fields = [self.uuid, self.action]
super().__init__(data=data, fields=_fields)
def action_validation(self): def action_validation(self):
allowed_actions = ["start", "stop", "delete"] allowed_actions = ['start', 'stop', 'delete']
if self.action.value not in allowed_actions: if self.action.value not in allowed_actions:
self.add_error( raise ValidationException('Invalid Action. Allowed Actions are {}'.format(allowed_actions))
"Invalid Action. Allowed Actions are {}".format(
allowed_actions
)
)
def validation(self): def validation(self):
vm = shared.vm_pool.get(self.uuid.value) vm = shared.vm_pool.get(self.uuid.value)
if not ( if not (vm.value['owner'] == self.name.value or self.realm.value == 'ungleich-admin'):
vm.value["owner"] == self.name.value raise ValidationException('Invalid User.')
or self.realm.value == "ungleich-admin"
):
self.add_error("Invalid User")
if ( if self.action.value == 'start' and vm.status == VMStatus.running and vm.hostname != '':
self.action.value == "start" raise ValidationException('VM Already Running')
and vm.status == VMStatus.running
and vm.hostname != ""
):
self.add_error("VM Already Running")
if self.action.value == "stop": if self.action.value == 'stop':
if vm.status == VMStatus.stopped: if vm.status == VMStatus.stopped:
self.add_error("VM Already Stopped") raise ValidationException('VM Already Stopped')
elif vm.status != VMStatus.running: elif vm.status != VMStatus.running:
self.add_error("Cannot stop non-running VM") raise ValidationException('Cannot stop non-running VM')
class VmMigrationSchema(OTPSchema): class VmMigrationSchema(OTPSchema):
def __init__(self, data): def __init__(self, data):
data["uuid"] = ( data['uuid'] = (
resolve_vm_name( resolve_vm_name(
name=data.get("vm_name", None), name=get(data, 'vm_name', return_default=True),
owner=( owner=(
data.get("in_support_of", None) get(data, 'in_support_of', return_default=True) or
or data.get("name", None) get(data, 'name', return_default=True)
), )
) ) or KeyError
or KeyError
) )
self.uuid = VmUUIDField(data) self.uuid = VmUUIDField(data)
self.destination = Field( self.destination = Field('destination', str, get(data, 'destination'),
"destination", str, data.get("destination", KeyError) validators=[self.destination_validation])
)
self.destination.validation = self.destination_validation super().__init__(data=data)
fields = [self.destination]
super().__init__(data=data, fields=fields)
def destination_validation(self): def destination_validation(self):
hostname = self.destination.value hostname = self.destination.value
host = next( host = next(filter(lambda h: h.hostname == hostname, shared.host_pool.hosts), None,)
filter(
lambda h: h.hostname == hostname, shared.host_pool.hosts
),
None,
)
if not host: if not host:
self.add_error( raise ValidationException('No Such Host ({}) exists'.format(self.destination.value))
"No Such Host ({}) exists".format(
self.destination.value
)
)
elif host.status != HostStatus.alive: elif host.status != HostStatus.alive:
self.add_error("Destination Host is dead") raise ValidationException('Destination Host is dead')
else: else:
self.destination.value = host.key self.destination.value = host.key
def validation(self): def validation(self):
vm = shared.vm_pool.get(self.uuid.value) vm = shared.vm_pool.get(self.uuid.value)
if not ( if not (vm.value['owner'] == self.name.value or self.realm.value == 'ungleich-admin'):
vm.value["owner"] == self.name.value raise ValidationException('Invalid User')
or self.realm.value == "ungleich-admin"
):
self.add_error("Invalid User")
if vm.status != VMStatus.running: if vm.status != VMStatus.running:
self.add_error("Can't migrate non-running VM") raise ValidationException("Can't migrate non-running VM")
if vm.hostname == os.path.join( if vm.hostname == os.path.join(settings['etcd']['host_prefix'], self.destination.value):
settings["etcd"]["host_prefix"], self.destination.value raise ValidationException("Destination host couldn't be same as Source Host")
):
self.add_error(
"Destination host couldn't be same as Source Host"
)
class AddSSHSchema(OTPSchema): class AddSSHSchema(OTPSchema):
def __init__(self, data): def __init__(self, data):
self.key_name = Field( self.key_name = Field('key_name', str, get(data, 'key_name'))
"key_name", str, data.get("key_name", KeyError) self.key = Field('key', str, get(data, 'key'))
) super().__init__(data=data)
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): class RemoveSSHSchema(OTPSchema):
def __init__(self, data): def __init__(self, data):
self.key_name = Field( self.key_name = Field('key_name', str, get(data, 'key_name'))
"key_name", str, data.get("key_name", KeyError) super().__init__(data=data)
)
fields = [self.key_name]
super().__init__(data=data, fields=fields)
class GetSSHSchema(OTPSchema): class GetSSHSchema(OTPSchema):
def __init__(self, data): def __init__(self, data):
self.key_name = Field( self.key_name = Field('key_name', str, get(data, 'key_name', return_default=True))
"key_name", str, data.get("key_name", None) super().__init__(data=data)
)
fields = [self.key_name]
super().__init__(data=data, fields=fields)
class CreateNetwork(OTPSchema): class CreateNetwork(OTPSchema):
def __init__(self, data): def __init__(self, data):
self.network_name = Field("network_name", str, data.get("network_name", KeyError)) self.network_name = Field('network_name', str, get(data, 'name'),
self.type = Field("type", str, data.get("type", KeyError)) validators=[self.network_name_validation])
self.user = Field("user", bool, bool(data.get("user", False))) 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)))
self.network_name.validation = self.network_name_validation super().__init__(data)
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): 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)
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) network = shared.etcd_client.get(key, value_in_json=True)
if network: if network:
self.add_error( raise ValidationException('Network with name {} already exists'.format(self.network_name.value))
"Network with name {} already exists".format(
self.network_name.value
)
)
def network_type_validation(self): def network_type_validation(self):
supported_network_types = ["vxlan"] supported_network_types = ['vxlan']
if self.type.value not in supported_network_types: if self.type.value not in supported_network_types:
self.add_error( raise ValidationException('Unsupported Network Type. Supported network types are {}'.format(supported_network_types))
"Unsupported Network Type. Supported network types are {}".format(
supported_network_types
)
)

View file

@ -1,8 +1,8 @@
from .etcd_wrapper import Etcd3Wrapper from uncloud.common.shared import shared
def increment_etcd_counter(etcd_client: Etcd3Wrapper, key): def increment_etcd_counter(key):
kv = etcd_client.get(key) kv = shared.etcd_client.get(key)
if kv: if kv:
counter = int(kv.value) counter = int(kv.value)
@ -10,12 +10,12 @@ def increment_etcd_counter(etcd_client: Etcd3Wrapper, key):
else: else:
counter = 1 counter = 1
etcd_client.put(key, str(counter)) shared.etcd_client.put(key, str(counter))
return counter return counter
def get_etcd_counter(etcd_client: Etcd3Wrapper, key): def get_etcd_counter(key):
kv = etcd_client.get(key) kv = shared.etcd_client.get(key)
if kv: if kv:
return int(kv.value) return int(kv.value)
return None return None

View file

@ -5,6 +5,7 @@ from functools import wraps
from uncloud import UncloudException from uncloud import UncloudException
from uncloud.common import logger from uncloud.common import logger
from typing import Iterator
class EtcdEntry: class EtcdEntry:
@ -42,14 +43,30 @@ class Etcd3Wrapper:
self.client = etcd3.client(*args, **kwargs) self.client = etcd3.client(*args, **kwargs)
@readable_errors @readable_errors
def get(self, *args, value_in_json=False, **kwargs): def get(self, *args, value_in_json=False, **kwargs) -> EtcdEntry:
"""Get a key/value pair from etcd
:return:
EtcdEntry: if a key/value pair is found in etcd
:raises:
KeyError: If key is not found in etcd
Exception: Different type of exception can be raised depending on
situation
"""
_value, _key = self.client.get(*args, **kwargs) _value, _key = self.client.get(*args, **kwargs)
if _key is None or _value is None: if _key is None or _value is None:
return None raise KeyError
return EtcdEntry(_key, _value, value_in_json=value_in_json) return EtcdEntry(_key, _value, value_in_json=value_in_json)
@readable_errors @readable_errors
def put(self, *args, value_in_json=False, **kwargs): def put(self, *args, value_in_json=False, **kwargs):
"""Put key/value pair in etcd
:return: a response containing a header and the prev_kv
:raises:
Exception: Different type of exception can be raised depending on
situation
"""
_key, _value = args _key, _value = args
if value_in_json: if value_in_json:
_value = json.dumps(_value) _value = json.dumps(_value)
@ -60,20 +77,14 @@ class Etcd3Wrapper:
return self.client.put(_key, _value, **kwargs) return self.client.put(_key, _value, **kwargs)
@readable_errors @readable_errors
def get_prefix(self, *args, value_in_json=False, raise_exception=True, **kwargs): def get_prefix(self, *args, value_in_json=False, **kwargs) -> \
try: Iterator[EtcdEntry]:
event_iterator = self.client.get_prefix(*args, **kwargs) event_iterator = self.client.get_prefix(*args, **kwargs)
for e in event_iterator: for e in event_iterator:
yield EtcdEntry(*e[::-1], value_in_json=value_in_json) yield EtcdEntry(*e[::-1], value_in_json=value_in_json)
except Exception as err:
if raise_exception:
raise Exception('Exception in etcd_wrapper.get_prefix') from err
else:
logger.exception('Error in etcd_wrapper')
return iter([])
@readable_errors @readable_errors
def watch_prefix(self, key, raise_exception=True, value_in_json=False): def watch_prefix(self, key, raise_exception=True, value_in_json=False) -> Iterator[EtcdEntry]:
try: try:
event_iterator, cancel = self.client.watch_prefix(key) event_iterator, cancel = self.client.watch_prefix(key)
for e in event_iterator: for e in event_iterator:

View file

@ -1,6 +1,7 @@
import subprocess as sp import subprocess as sp
import random import random
import logging import logging
import ipaddress
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -9,9 +10,7 @@ def random_bytes(num=6):
return [random.randrange(256) for _ in range(num)] return [random.randrange(256) for _ in range(num)]
def generate_mac( def generate_mac(uaa=False, multicast=False, oui=None, separator=":", byte_fmt="%02x"):
uaa=False, multicast=False, oui=None, separator=":", byte_fmt="%02x"
):
mac = random_bytes() mac = random_bytes()
if oui: if oui:
if type(oui) == str: if type(oui) == str:
@ -68,3 +67,21 @@ def delete_network_interface(iface):
except Exception: except Exception:
logger.exception("Interface %s Deletion failed", iface) logger.exception("Interface %s Deletion failed", iface)
def mac2ipv6(mac, prefix):
# only accept MACs separated by a colon
parts = mac.split(':')
# modify parts to match IPv6 value
parts.insert(3, 'ff')
parts.insert(4, 'fe')
parts[0] = '%x' % (int(parts[0], 16) ^ 2)
# format output
ipv6_parts = [str(0)] * 4
for i in range(0, len(parts), 2):
ipv6_parts.append(''.join(parts[i: i + 2]))
lower_part = ipaddress.IPv6Address(':'.join(ipv6_parts))
prefix = ipaddress.IPv6Address(prefix)
return str(prefix + int(lower_part))