meow
93dee1c9fc
1. User can now use image name instead of image uuid when creation vm. For Example, now user can create an alpine vm using the following command ```shell ucloud-cli vm create --vm-name myvm --cpu 2 --ram '2GB' \ --os-ssd '10GB' --image images:alpine ``` 2. Instead of directly running code, code is now placed under a function main and is called using the following code ```python if __name__ == "__main__": main() ``` 3. Multiprocess (Process) is used instead of threading (Thread) to update heart beat of host. 4. IP Address of vm is included in vm's status which is retrieved by the following command ```shell ucloud-cli vm status --vm-name myvm ```
380 lines
12 KiB
Python
380 lines
12 KiB
Python
import json
|
|
import subprocess
|
|
import os
|
|
|
|
import schemas
|
|
|
|
from uuid import uuid4
|
|
|
|
from flask import Flask, request
|
|
from flask_restful import Resource, Api
|
|
|
|
from ucloud_common.vm import VMStatus
|
|
from ucloud_common.request import RequestEntry, RequestType
|
|
|
|
from helper import generate_mac, get_ip_addr
|
|
|
|
from config import (
|
|
etcd_client,
|
|
WITHOUT_CEPH,
|
|
VM_PREFIX,
|
|
HOST_PREFIX,
|
|
FILE_PREFIX,
|
|
IMAGE_PREFIX,
|
|
logging,
|
|
REQUEST_POOL,
|
|
VM_POOL,
|
|
HOST_POOL,
|
|
)
|
|
|
|
app = Flask(__name__)
|
|
api = Api(app)
|
|
|
|
|
|
class CreateVM(Resource):
|
|
@staticmethod
|
|
def post():
|
|
data = request.json
|
|
print(data)
|
|
validator = schemas.CreateVMSchema(data)
|
|
if validator.is_valid():
|
|
vm_uuid = uuid4().hex
|
|
vm_key = os.path.join(VM_PREFIX, vm_uuid)
|
|
specs = {
|
|
'cpu': validator.specs['cpu'],
|
|
'ram': validator.specs['ram'],
|
|
'os-ssd': validator.specs['os-ssd'],
|
|
'hdd': validator.specs['hdd']
|
|
}
|
|
|
|
vm_entry = {
|
|
"name": data["vm_name"],
|
|
"owner": data["name"],
|
|
"owner_realm": data["realm"],
|
|
"specs": specs,
|
|
"hostname": "",
|
|
"status": "",
|
|
"image_uuid": validator.image_uuid,
|
|
"log": [],
|
|
"vnc_socket": "",
|
|
"mac": str(generate_mac()),
|
|
"metadata": {
|
|
"ssh-keys": []
|
|
}
|
|
}
|
|
etcd_client.put(vm_key, vm_entry, value_in_json=True)
|
|
|
|
# Create ScheduleVM Request
|
|
r = RequestEntry.from_scratch(type=RequestType.ScheduleVM, uuid=vm_uuid)
|
|
REQUEST_POOL.put(r)
|
|
|
|
return {"message": "VM Creation Queued"}, 200
|
|
return validator.get_errors(), 400
|
|
|
|
|
|
class VmStatus(Resource):
|
|
@staticmethod
|
|
def get():
|
|
data = request.json
|
|
validator = schemas.VMStatusSchema(data)
|
|
if validator.is_valid():
|
|
vm = VM_POOL.get(os.path.join(VM_PREFIX, data["uuid"]))
|
|
vm_value = vm.value.copy()
|
|
vm_value["ip"] = list(map(str, get_ip_addr(vm.mac, "br0")))
|
|
vm.value = vm_value
|
|
print(vm.value)
|
|
return vm.value
|
|
else:
|
|
return validator.get_errors(), 400
|
|
|
|
|
|
class CreateImage(Resource):
|
|
@staticmethod
|
|
def post():
|
|
data = request.json
|
|
validator = schemas.CreateImageSchema(data)
|
|
if validator.is_valid():
|
|
file_entry = etcd_client.get(os.path.join(FILE_PREFIX, data["uuid"]))
|
|
file_entry_value = json.loads(file_entry.value)
|
|
|
|
image_entry_json = {
|
|
"status": "TO_BE_CREATED",
|
|
"owner": file_entry_value["owner"],
|
|
"filename": file_entry_value["filename"],
|
|
"name": data["name"],
|
|
"store_name": data["image_store"],
|
|
"visibility": "public",
|
|
}
|
|
etcd_client.put(
|
|
os.path.join(IMAGE_PREFIX, data["uuid"]), json.dumps(image_entry_json)
|
|
)
|
|
|
|
return {"message": "Image successfully created"}
|
|
return validator.get_errors(), 400
|
|
|
|
|
|
class ListPublicImages(Resource):
|
|
@staticmethod
|
|
def get():
|
|
images = etcd_client.get_prefix(IMAGE_PREFIX, value_in_json=True)
|
|
r = {}
|
|
r["images"] = []
|
|
for image in images:
|
|
image_key = "{}:{}".format(image.value["store_name"], image.value["name"])
|
|
r["images"].append({
|
|
"name":image_key,
|
|
"status": image.value["status"]
|
|
})
|
|
return r, 200
|
|
|
|
|
|
class VMAction(Resource):
|
|
@staticmethod
|
|
def post():
|
|
data = request.json
|
|
validator = schemas.VmActionSchema(data)
|
|
|
|
if validator.is_valid():
|
|
vm_entry = VM_POOL.get(os.path.join(VM_PREFIX, data["uuid"]))
|
|
action = data["action"]
|
|
|
|
if action == "start":
|
|
vm_entry.status = VMStatus.requested_start
|
|
VM_POOL.put(vm_entry)
|
|
action = "schedule"
|
|
|
|
if action == "delete" and vm_entry.hostname == "":
|
|
try:
|
|
path_without_protocol = vm_entry.path[vm_entry.path.find(":") + 1 :]
|
|
|
|
if WITHOUT_CEPH:
|
|
command_to_delete = [
|
|
"rm", "-rf",
|
|
os.path.join("/var/vm", vm_entry.uuid),
|
|
]
|
|
else:
|
|
command_to_delete = ["rbd", "rm", path_without_protocol]
|
|
|
|
subprocess.check_output(command_to_delete, stderr=subprocess.PIPE)
|
|
except subprocess.CalledProcessError as e:
|
|
if "No such file" in e.stderr.decode("utf-8"):
|
|
etcd_client.client.delete(vm_entry.key)
|
|
return {"message": "VM successfully deleted"}
|
|
else:
|
|
logging.exception(e)
|
|
return {"message": "Some error occurred while deleting VM"}
|
|
else:
|
|
etcd_client.client.delete(vm_entry.key)
|
|
return {"message": "VM successfully deleted"}
|
|
|
|
r = RequestEntry.from_scratch(
|
|
type="{}VM".format(action.title()),
|
|
uuid=data["uuid"],
|
|
hostname=vm_entry.hostname,
|
|
)
|
|
REQUEST_POOL.put(r)
|
|
return {"message": "VM {} Queued".format(action.title())}, 200
|
|
else:
|
|
return validator.get_errors(), 400
|
|
|
|
|
|
class VMMigration(Resource):
|
|
@staticmethod
|
|
def post():
|
|
data = request.json
|
|
validator = schemas.VmMigrationSchema(data)
|
|
|
|
if validator.is_valid():
|
|
vm = VM_POOL.get(data["uuid"])
|
|
|
|
r = RequestEntry.from_scratch(
|
|
type=RequestType.ScheduleVM,
|
|
uuid=vm.uuid,
|
|
destination=os.path.join(HOST_PREFIX, data["destination"]),
|
|
migration=True,
|
|
)
|
|
REQUEST_POOL.put(r)
|
|
return {"message": "VM Migration Initialization Queued"}, 200
|
|
else:
|
|
return validator.get_errors(), 400
|
|
|
|
|
|
class ListUserVM(Resource):
|
|
@staticmethod
|
|
def get():
|
|
data = request.json
|
|
validator = schemas.OTPSchema(data)
|
|
|
|
if validator.is_valid():
|
|
vms = etcd_client.get_prefix(VM_PREFIX, value_in_json=True)
|
|
return_vms = []
|
|
user_vms = filter(lambda v: v.value["owner"] == data["name"], vms)
|
|
for vm in user_vms:
|
|
return_vms.append(
|
|
{
|
|
"name": vm.value["name"],
|
|
"vm_uuid": vm.key.split("/")[-1],
|
|
"specs": vm.value["specs"],
|
|
"status": vm.value["status"],
|
|
"hostname": vm.value["hostname"],
|
|
"mac": vm.value["mac"],
|
|
"vnc_socket": None
|
|
if vm.value.get("vnc_socket", None) is None
|
|
else vm.value["vnc_socket"],
|
|
}
|
|
)
|
|
if return_vms:
|
|
return {"message": return_vms}, 200
|
|
return {"message": "No VM found"}, 404
|
|
|
|
else:
|
|
return validator.get_errors(), 400
|
|
|
|
|
|
class ListUserFiles(Resource):
|
|
@staticmethod
|
|
def get():
|
|
data = request.json
|
|
validator = schemas.OTPSchema(data)
|
|
|
|
if validator.is_valid():
|
|
files = etcd_client.get_prefix(FILE_PREFIX, value_in_json=True)
|
|
return_files = []
|
|
user_files = list(filter(lambda f: f.value["owner"] == data["name"], files))
|
|
for file in user_files:
|
|
return_files.append(
|
|
{
|
|
"filename": file.value["filename"],
|
|
"uuid": file.key.split("/")[-1],
|
|
}
|
|
)
|
|
return {"message": return_files}, 200
|
|
else:
|
|
return validator.get_errors(), 400
|
|
|
|
|
|
class CreateHost(Resource):
|
|
@staticmethod
|
|
def post():
|
|
data = request.json
|
|
validator = schemas.CreateHostSchema(data)
|
|
if validator.is_valid():
|
|
host_key = os.path.join(HOST_PREFIX, uuid4().hex)
|
|
host_entry = {
|
|
"specs": data["specs"],
|
|
"hostname": data["hostname"],
|
|
"status": "DEAD",
|
|
"last_heartbeat": "",
|
|
}
|
|
etcd_client.put(host_key, host_entry, value_in_json=True)
|
|
|
|
return {"message": "Host Created"}, 200
|
|
|
|
return validator.get_errors(), 400
|
|
|
|
|
|
class ListHost(Resource):
|
|
@staticmethod
|
|
def get():
|
|
hosts = HOST_POOL.hosts
|
|
r = {
|
|
host.key: {
|
|
"status": host.status,
|
|
"specs": host.specs,
|
|
"hostname": host.hostname,
|
|
}
|
|
for host in hosts
|
|
}
|
|
return r, 200
|
|
|
|
|
|
class GetSSHKeys(Resource):
|
|
@staticmethod
|
|
def get():
|
|
data = request.json
|
|
validator = schemas.GetSSHSchema(data)
|
|
if validator.is_valid():
|
|
if not validator.key_name.value:
|
|
|
|
# {user_prefix}/{realm}/{name}/key/
|
|
etcd_key = os.path.join(USER_PREFIX, data["realm"], data["name"], "key")
|
|
etcd_entry = etcd_client.get_prefix(etcd_key, value_in_json=True)
|
|
|
|
keys = {key.key.split("/")[-1]: key.value for key in etcd_entry}
|
|
return {"keys": keys}
|
|
else:
|
|
|
|
# {user_prefix}/{realm}/{name}/key/{key_name}
|
|
etcd_key = os.path.join(USER_PREFIX, data["realm"], data["name"],
|
|
"key", data["key_name"])
|
|
etcd_entry = etcd_client.get(etcd_key, value_in_json=True)
|
|
|
|
if etcd_entry:
|
|
return {"keys": {etcd_entry.key.split("/")[-1]: etcd_entry.value}}
|
|
else:
|
|
return {"keys": {}}
|
|
else:
|
|
return validator.get_errors(), 400
|
|
|
|
|
|
class AddSSHKey(Resource):
|
|
@staticmethod
|
|
def post():
|
|
data = request.json
|
|
validator = schemas.AddSSHSchema(data)
|
|
if validator.is_valid():
|
|
|
|
# {user_prefix}/{realm}/{name}/key/{key_name}
|
|
etcd_key = os.path.join(USER_PREFIX, data["realm"], data["name"],
|
|
"key", data["key_name"])
|
|
etcd_entry = etcd_client.get(etcd_key, value_in_json=True)
|
|
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.
|
|
etcd_client.put(etcd_key, data["key"], value_in_json=True)
|
|
return {"message": "Key added successfully"}
|
|
else:
|
|
return validator.get_errors(), 400
|
|
|
|
|
|
class RemoveSSHKey(Resource):
|
|
@staticmethod
|
|
def get():
|
|
data = request.json
|
|
validator = schemas.RemoveSSHSchema(data)
|
|
if validator.is_valid():
|
|
|
|
# {user_prefix}/{realm}/{name}/key/{key_name}
|
|
etcd_key = os.path.join(USER_PREFIX, data["realm"], data["name"],
|
|
"key", data["key_name"])
|
|
etcd_entry = etcd_client.get(etcd_key, value_in_json=True)
|
|
if etcd_entry:
|
|
etcd_client.client.delete(etcd_key)
|
|
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
|
|
|
|
api.add_resource(CreateVM, "/vm/create")
|
|
api.add_resource(VmStatus, "/vm/status")
|
|
|
|
api.add_resource(VMAction, "/vm/action")
|
|
api.add_resource(VMMigration, "/vm/migrate")
|
|
|
|
api.add_resource(CreateImage, "/image/create")
|
|
api.add_resource(ListPublicImages, "/image/list-public")
|
|
|
|
api.add_resource(ListUserVM, "/user/vms")
|
|
api.add_resource(ListUserFiles, "/user/files")
|
|
|
|
api.add_resource(AddSSHKey, "/user/add-ssh")
|
|
api.add_resource(RemoveSSHKey, "/user/remove-ssh")
|
|
api.add_resource(GetSSHKeys, "/user/get-ssh")
|
|
|
|
api.add_resource(CreateHost, "/host/create")
|
|
api.add_resource(ListHost, "/host/list")
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="::", debug=True)
|