diff --git a/Pipfile b/Pipfile index 273e7a6..bd5f667 100644 --- a/Pipfile +++ b/Pipfile @@ -14,6 +14,7 @@ flask-restful = "*" etcd3 = "*" gunicorn = "*" bitmath = "*" +pylint = "*" [requires] python_version = "3.7" diff --git a/helper.py b/helper.py index 4b580f1..9d3a254 100644 --- a/helper.py +++ b/helper.py @@ -8,22 +8,22 @@ from pyotp import TOTP def check_otp(name, realm, token): try: data = { - "auth_name": config('AUTH_NAME', ''), - "auth_token": TOTP(config('AUTH_SEED', '')).now(), - "auth_realm": config('AUTH_REALM', ''), + "auth_name": config("AUTH_NAME", ""), + "auth_token": TOTP(config("AUTH_SEED", "")).now(), + "auth_realm": config("AUTH_REALM", ""), "name": name, "realm": realm, - "token": token + "token": token, } except binascii.Error: return 400 response = requests.post( "{OTP_SERVER}{OTP_VERIFY_ENDPOINT}".format( - OTP_SERVER=config('OTP_SERVER', ''), - OTP_VERIFY_ENDPOINT=config('OTP_VERIFY_ENDPOINT', 'verify/') + OTP_SERVER=config("OTP_SERVER", ""), + OTP_VERIFY_ENDPOINT=config("OTP_VERIFY_ENDPOINT", "verify/"), ), - data=data + data=data, ) return response.status_code diff --git a/main.py b/main.py index 8cba8e1..5617eb3 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ # TODO -# convert etcd3 usage to etcd3_wrapper -import etcd3 +# 1. Allow user of realm ungleich-admin to perform any action on +# any user vm. + import json from helper import check_otp, add_otp_args, add_vmid_args @@ -10,11 +11,11 @@ from decouple import config from uuid import uuid4 from etcd3_wrapper import Etcd3Wrapper from specs_parser import SpecsParser +from functools import wraps app = Flask(__name__) api = Api(app) -etcd_client = etcd3.client(host=config("ETCD_HOST"), port=int(config("ETCD_PORT"))) client = Etcd3Wrapper() # CreateVM argparser @@ -48,6 +49,12 @@ add_otp_args(uservm_argparser) # Specs parser specs_parser = SpecsParser(exceptional_devices=["cpu"]) +# AddHost argparser +addhost_argparser = reqparse.RequestParser() +addhost_argparser.add_argument("hostname", type=str, required=True) +addhost_argparser.add_argument("specs", type=dict, required=True) +add_otp_args(addhost_argparser) + def is_image_valid(image_uuid): images = client.get_prefix("/v1/image/") @@ -57,25 +64,36 @@ def is_image_valid(image_uuid): class CreateVM(Resource): def post(self): createvm_args = createvm_argparser.parse_args() - name, realm, token, specs, image_uuid = createvm_args.name, createvm_args.realm,\ - createvm_args.token, createvm_args.specs,\ - createvm_args.image_uuid + name, realm, token, specs, image_uuid = ( + createvm_args.name, + createvm_args.realm, + createvm_args.token, + createvm_args.specs, + createvm_args.image_uuid, + ) if check_otp(name, realm, token) == 200: # User is good if is_image_valid(image_uuid): if not specs_parser.transform_specs(specs): - return {"message": f"""Invalid unit - Please use following units {specs_parser.get_allowed_units()}"""}, 400 + return ( + { + "message": f"""Invalid unit - Please use following units {specs_parser.get_allowed_units()}""" + }, + 400, + ) print(specs) vm_key = f"/v1/vm/{uuid4().hex}" - vm_entry = {"owner": name, - "specs": specs, - "hostname": "", - "status": "REQUESTED_NEW", - "image_uuid": image_uuid} + vm_entry = { + "owner": name, + "specs": specs, + "hostname": "", + "status": "REQUESTED_NEW", + "image_uuid": image_uuid, + } - etcd_client.put(vm_key, json.dumps(vm_entry)) + client.put(vm_key, vm_entry, value_in_json=True) return {"message": "VM Creation Queued"}, 200 else: @@ -87,18 +105,23 @@ class CreateVM(Resource): class DeleteVM(Resource): def post(self): deletevm_args = deletevm_argparser.parse_args() - name, realm, token, vmid = deletevm_args.name, deletevm_args.realm,\ - deletevm_args.token, deletevm_args.vmid + name, realm, token, vmid = ( + deletevm_args.name, + deletevm_args.realm, + deletevm_args.token, + deletevm_args.vmid, + ) if check_otp(name, realm, token) == 200: # User is good - vmentry_etcd = etcd_client.get(f"/v1/vm/{vmid}")[0] + vmentry_etcd = client.get(f"/v1/vm/{vmid}").value if vmentry_etcd: vmentry_etcd = json.loads(vmentry_etcd) vmentry_etcd["status"] = "REQUESTED_DELETE" - etcd_client.put(f"/v1/vm/{vmid}", json.dumps(vmentry_etcd)) + client.put(f"/v1/vm/{vmid}", vmentry_etcd, + value_in_json=True) return {"message": "VM Deletion Queued"}, 200 else: @@ -110,7 +133,7 @@ class DeleteVM(Resource): class VmStatus(Resource): def get(self): args = vmstatus_argparser.parse_args() - r = etcd_client.get(f"/v1/vm/{args.vmid}")[0] + r = client.get(f"/v1/vm/{args.vmid}").value print(r) if r: r = dict(json.loads(r.decode("utf-8"))) @@ -127,13 +150,18 @@ class CreateImage(Resource): file_entry = client.get(f"/v1/files/{image_file_uuid}") if file_entry is None: - return { - "Message": - f"Image File with uuid '{image_file_uuid}' Not Found"}, 400 + return ( + {"Message": f"Image File with uuid '{image_file_uuid}' Not Found"}, + 400, + ) file_entry_value = json.loads(file_entry.value) - image_store = list(filter(lambda s: json.loads(s.value)["name"] == image_store_name, image_stores)) + image_store = list( + filter( + lambda s: json.loads(s.value)["name"] == image_store_name, image_stores + ) + ) if not image_store: return {"Message": f"Store '{image_store_name}' does not exists"}, 400 @@ -144,7 +172,7 @@ class CreateImage(Resource): "filename": file_entry_value["filename"], "name": args.name, "store_name": image_store_name, - "visibility": "public" + "visibility": "public", } client.put(f"/v1/image/{image_file_uuid}", json.dumps(image_entry_json)) @@ -168,6 +196,8 @@ class StartVM(Resource): if check_otp(name, realm, token) == 200: vm = client.get(f"/v1/vm/{vm_uuid}", value_in_json=True) + if vm.value["owner"] != name: + return {"message": "Invalid User"} if vm: vm.value["status"] = "REQUESTED_START" client.put(vm.key, json.dumps(vm.value)) @@ -185,6 +215,8 @@ class SuspendVM(Resource): if check_otp(name, realm, token) == 200: vm = client.get(f"/v1/vm/{vm_uuid}", value_in_json=True) + if vm.value["owner"] != name: + return {"message": "Invalid User"} if vm: vm.value["status"] = "REQUESTED_SUSPEND" client.put(vm.key, json.dumps(vm.value)) @@ -202,6 +234,8 @@ class ResumeVM(Resource): if check_otp(name, realm, token) == 200: vm = client.get(f"/v1/vm/{vm_uuid}", value_in_json=True) + if vm.value["owner"] != name: + return {"message": "Invalid User"} if vm: vm.value["status"] = "REQUESTED_RESUME" client.put(vm.key, json.dumps(vm.value)) @@ -219,6 +253,9 @@ class ShutdownVM(Resource): if check_otp(name, realm, token) == 200: vm = client.get(f"/v1/vm/{vm_uuid}", value_in_json=True) + if vm.value["owner"] != name: + return {"message": "Invalid User"} + if vm: vm.value["status"] = "REQUESTED_SHUTDOWN" client.put(vm.key, json.dumps(vm.value)) @@ -240,11 +277,13 @@ class ListUserVM(Resource): return_vms = [] user_vms = list(filter(lambda v: v.value["owner"] == name, vms)) for vm in user_vms: - return_vms.append({ - "vm_uuid": vm.key.split("/")[-1], - "specs": vm.value["specs"], - "status": vm.value["status"] - }) + return_vms.append( + { + "vm_uuid": vm.key.split("/")[-1], + "specs": vm.value["specs"], + "status": vm.value["status"], + } + ) return {"message": return_vms}, 200 else: return {"message": "No VM found"}, 404 @@ -263,15 +302,55 @@ class ListUserFiles(Resource): return_files = [] user_files = list(filter(lambda f: f.value["owner"] == name, files)) for file in user_files: - return_files.append({ - "filename": vm.value["filename"] - }) + return_files.append( + { + "filename": file.value["filename"], + "uuid": file.key.split("/")[-1], + } + ) return {"message": return_files}, 200 else: return {"message": "No File found"}, 404 else: return {"message": "Invalid Credentials"}, 400 + +class CreateHost(Resource): + def post(self): + args = addhost_argparser.parse_args() + name, realm, token, specs, hostname = ( + args.name, + args.realm, + args.token, + args.specs, + args.hostname, + ) + + if realm == "ungleich-admin" and check_otp(name, realm, token) == 200: + # User is good + if not specs_parser.transform_specs(specs): + return ( + { + "message": f"""Invalid unit - Please use following units {specs_parser.get_allowed_units()}""" + }, + 400, + ) + + print(specs) + host_key = f"/v1/host/{uuid4().hex}" + host_entry = { + "specs": specs, + "hostname": hostname, + "status": "DEAD", + "last_heart_beat": "", + } + client.put(host_key, host_entry, value_in_json=True) + + return {"message": "Host Created"}, 200 + else: + return {"message": "Invalid Credentials/Insufficient Permission"}, 400 + + api.add_resource(CreateVM, "/vm/create") api.add_resource(DeleteVM, "/vm/delete") api.add_resource(VmStatus, "/vm/status") @@ -287,5 +366,8 @@ api.add_resource(ListPublicImages, "/image/list-public") api.add_resource(ListUserVM, "/user/vms") api.add_resource(ListUserFiles, "/user/files") +api.add_resource(CreateHost, "/host/create") + + if __name__ == "__main__": app.run(host="::", debug=True) diff --git a/specs_parser.py b/specs_parser.py index 0cd9894..c2996af 100644 --- a/specs_parser.py +++ b/specs_parser.py @@ -1,7 +1,8 @@ import bitmath + class SpecsParser(object): - def __init__(self, exceptional_devices, allowed_unit = 10): + def __init__(self, exceptional_devices, allowed_unit=10): self.exceptional_devices = exceptional_devices self.allowed_unit = allowed_unit