New Features + Refactoring
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
```
This commit is contained in:
parent
da77ac65eb
commit
93dee1c9fc
13 changed files with 354 additions and 233 deletions
|
|
@ -1,9 +1,12 @@
|
|||
import binascii
|
||||
import requests
|
||||
import random
|
||||
import subprocess as sp
|
||||
import ipaddress
|
||||
|
||||
from decouple import config
|
||||
from pyotp import TOTP
|
||||
from config import VM_POOL
|
||||
from config import VM_POOL, etcd_client, IMAGE_PREFIX
|
||||
|
||||
|
||||
def check_otp(name, realm, token):
|
||||
|
|
@ -47,7 +50,47 @@ def resolve_vm_name(name, owner):
|
|||
|
||||
return None
|
||||
|
||||
import random
|
||||
|
||||
def resolve_image_name(name, etcd_client):
|
||||
"""Return image uuid given its name and its store
|
||||
|
||||
* If the provided name is not in correct format
|
||||
i.e {store_name}:{image_name} return ValueError
|
||||
* If no such image found then return KeyError
|
||||
|
||||
"""
|
||||
|
||||
seperator = ":"
|
||||
|
||||
# Ensure, user/program passed valid name that is of type string
|
||||
try:
|
||||
store_name_and_image_name = name.split(seperator)
|
||||
|
||||
"""
|
||||
Examples, where it would work and where it would raise exception
|
||||
"images:alpine" --> ["images", "alpine"]
|
||||
|
||||
"images" --> ["images"] it would raise Exception as non enough value to unpack
|
||||
|
||||
"images:alpine:meow" --> ["images", "alpine", "meow"] it would raise Exception
|
||||
as too many values to unpack
|
||||
"""
|
||||
store_name, image_name = store_name_and_image_name
|
||||
except Exception:
|
||||
raise ValueError("Image name not in correct format i.e {store_name}:{image_name}")
|
||||
|
||||
images = etcd_client.get_prefix(IMAGE_PREFIX, value_in_json=True)
|
||||
|
||||
# Try to find image with name == image_name and store_name == store_name
|
||||
try:
|
||||
image = next(filter(lambda im: im.value['name'] == image_name \
|
||||
and im.value['store_name'] == store_name, images))
|
||||
except StopIteration:
|
||||
raise KeyError("No image with name {} found.".format(name))
|
||||
else:
|
||||
image_uuid = image.key.split('/')[-1]
|
||||
|
||||
return image_uuid
|
||||
|
||||
def random_bytes(num=6):
|
||||
return [random.randrange(256) for _ in range(num)]
|
||||
|
|
@ -68,3 +111,32 @@ def generate_mac(uaa=False, multicast=False, oui=None, separator=':', byte_fmt='
|
|||
else:
|
||||
mac[0] |= 1 << 1 # set bit 1
|
||||
return separator.join(byte_fmt % b for b in mac)
|
||||
|
||||
def get_ip_addr(mac_address, device):
|
||||
"""Return IP address of a device provided its mac address / link local address
|
||||
and the device with which it is connected.
|
||||
|
||||
For Example, if we call get_ip_addr(mac_address="52:54:00:12:34:56", device="br0")
|
||||
the following two scenarios can happen
|
||||
1. It would return None if we can't be able to find device whose mac_address is equal
|
||||
to the arg:mac_address or the mentioned arg:device does not exists or the ip address
|
||||
we found is local.
|
||||
2. It would return ip_address of device whose mac_address is equal to arg:mac_address
|
||||
and is connected/neighbor of arg:device
|
||||
"""
|
||||
try:
|
||||
output = sp.check_output(['ip','-6','neigh', 'show', 'dev', device], stderr=sp.PIPE)
|
||||
except sp.CalledProcessError:
|
||||
return None
|
||||
else:
|
||||
result = []
|
||||
output = output.strip().decode("utf-8")
|
||||
output = output.split("\n")
|
||||
for entry in output:
|
||||
entry = entry.split()
|
||||
if entry:
|
||||
ip = ipaddress.ip_address(entry[0])
|
||||
mac = entry[2]
|
||||
if ip.is_global and mac_address == mac:
|
||||
result.append(ip)
|
||||
return result
|
||||
|
|
|
|||
56
api/main.py
56
api/main.py
|
|
@ -1,6 +1,9 @@
|
|||
import json
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
import schemas
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from flask import Flask, request
|
||||
|
|
@ -9,7 +12,7 @@ from flask_restful import Resource, Api
|
|||
from ucloud_common.vm import VMStatus
|
||||
from ucloud_common.request import RequestEntry, RequestType
|
||||
|
||||
from helper import generate_mac
|
||||
from helper import generate_mac, get_ip_addr
|
||||
|
||||
from config import (
|
||||
etcd_client,
|
||||
|
|
@ -23,18 +26,6 @@ from config import (
|
|||
VM_POOL,
|
||||
HOST_POOL,
|
||||
)
|
||||
from schemas import (
|
||||
CreateVMSchema,
|
||||
VMStatusSchema,
|
||||
CreateImageSchema,
|
||||
VmActionSchema,
|
||||
OTPSchema,
|
||||
CreateHostSchema,
|
||||
VmMigrationSchema,
|
||||
AddSSHSchema,
|
||||
RemoveSSHSchema,
|
||||
GetSSHSchema
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
api = Api(app)
|
||||
|
|
@ -45,7 +36,7 @@ class CreateVM(Resource):
|
|||
def post():
|
||||
data = request.json
|
||||
print(data)
|
||||
validator = CreateVMSchema(data)
|
||||
validator = schemas.CreateVMSchema(data)
|
||||
if validator.is_valid():
|
||||
vm_uuid = uuid4().hex
|
||||
vm_key = os.path.join(VM_PREFIX, vm_uuid)
|
||||
|
|
@ -63,7 +54,7 @@ class CreateVM(Resource):
|
|||
"specs": specs,
|
||||
"hostname": "",
|
||||
"status": "",
|
||||
"image_uuid": data["image_uuid"],
|
||||
"image_uuid": validator.image_uuid,
|
||||
"log": [],
|
||||
"vnc_socket": "",
|
||||
"mac": str(generate_mac()),
|
||||
|
|
@ -85,10 +76,14 @@ class VmStatus(Resource):
|
|||
@staticmethod
|
||||
def get():
|
||||
data = request.json
|
||||
validator = VMStatusSchema(data)
|
||||
validator = schemas.VMStatusSchema(data)
|
||||
if validator.is_valid():
|
||||
vm = VM_POOL.get(os.path.join(VM_PREFIX, data["uuid"]))
|
||||
return json.dumps(str(vm))
|
||||
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
|
||||
|
||||
|
|
@ -97,7 +92,7 @@ class CreateImage(Resource):
|
|||
@staticmethod
|
||||
def post():
|
||||
data = request.json
|
||||
validator = CreateImageSchema(data)
|
||||
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)
|
||||
|
|
@ -121,10 +116,15 @@ class CreateImage(Resource):
|
|||
class ListPublicImages(Resource):
|
||||
@staticmethod
|
||||
def get():
|
||||
images = etcd_client.get_prefix(IMAGE_PREFIX)
|
||||
images = etcd_client.get_prefix(IMAGE_PREFIX, value_in_json=True)
|
||||
r = {}
|
||||
r["images"] = []
|
||||
for image in images:
|
||||
r[image.key.split("/")[-1]] = json.loads(image.value)
|
||||
image_key = "{}:{}".format(image.value["store_name"], image.value["name"])
|
||||
r["images"].append({
|
||||
"name":image_key,
|
||||
"status": image.value["status"]
|
||||
})
|
||||
return r, 200
|
||||
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ class VMAction(Resource):
|
|||
@staticmethod
|
||||
def post():
|
||||
data = request.json
|
||||
validator = VmActionSchema(data)
|
||||
validator = schemas.VmActionSchema(data)
|
||||
|
||||
if validator.is_valid():
|
||||
vm_entry = VM_POOL.get(os.path.join(VM_PREFIX, data["uuid"]))
|
||||
|
|
@ -182,7 +182,7 @@ class VMMigration(Resource):
|
|||
@staticmethod
|
||||
def post():
|
||||
data = request.json
|
||||
validator = VmMigrationSchema(data)
|
||||
validator = schemas.VmMigrationSchema(data)
|
||||
|
||||
if validator.is_valid():
|
||||
vm = VM_POOL.get(data["uuid"])
|
||||
|
|
@ -203,7 +203,7 @@ class ListUserVM(Resource):
|
|||
@staticmethod
|
||||
def get():
|
||||
data = request.json
|
||||
validator = OTPSchema(data)
|
||||
validator = schemas.OTPSchema(data)
|
||||
|
||||
if validator.is_valid():
|
||||
vms = etcd_client.get_prefix(VM_PREFIX, value_in_json=True)
|
||||
|
|
@ -235,7 +235,7 @@ class ListUserFiles(Resource):
|
|||
@staticmethod
|
||||
def get():
|
||||
data = request.json
|
||||
validator = OTPSchema(data)
|
||||
validator = schemas.OTPSchema(data)
|
||||
|
||||
if validator.is_valid():
|
||||
files = etcd_client.get_prefix(FILE_PREFIX, value_in_json=True)
|
||||
|
|
@ -257,7 +257,7 @@ class CreateHost(Resource):
|
|||
@staticmethod
|
||||
def post():
|
||||
data = request.json
|
||||
validator = CreateHostSchema(data)
|
||||
validator = schemas.CreateHostSchema(data)
|
||||
if validator.is_valid():
|
||||
host_key = os.path.join(HOST_PREFIX, uuid4().hex)
|
||||
host_entry = {
|
||||
|
|
@ -292,7 +292,7 @@ class GetSSHKeys(Resource):
|
|||
@staticmethod
|
||||
def get():
|
||||
data = request.json
|
||||
validator = GetSSHSchema(data)
|
||||
validator = schemas.GetSSHSchema(data)
|
||||
if validator.is_valid():
|
||||
if not validator.key_name.value:
|
||||
|
||||
|
|
@ -321,7 +321,7 @@ class AddSSHKey(Resource):
|
|||
@staticmethod
|
||||
def post():
|
||||
data = request.json
|
||||
validator = AddSSHSchema(data)
|
||||
validator = schemas.AddSSHSchema(data)
|
||||
if validator.is_valid():
|
||||
|
||||
# {user_prefix}/{realm}/{name}/key/{key_name}
|
||||
|
|
@ -342,7 +342,7 @@ class RemoveSSHKey(Resource):
|
|||
@staticmethod
|
||||
def get():
|
||||
data = request.json
|
||||
validator = RemoveSSHSchema(data)
|
||||
validator = schemas.RemoveSSHSchema(data)
|
||||
if validator.is_valid():
|
||||
|
||||
# {user_prefix}/{realm}/{name}/key/{key_name}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import json
|
|||
import os
|
||||
import bitmath
|
||||
|
||||
import helper
|
||||
|
||||
from ucloud_common.host import HostPool, HostStatus
|
||||
from ucloud_common.vm import VmPool, VMStatus
|
||||
|
||||
|
|
@ -207,22 +209,24 @@ class CreateVMSchema(OTPSchema):
|
|||
# Fields
|
||||
self.specs = Field("specs", dict, data.get("specs", KeyError))
|
||||
self.vm_name = Field("vm_name", str, data.get("vm_name", KeyError))
|
||||
self.image_uuid = Field("image_uuid", str, data.get("image_uuid", KeyError))
|
||||
self.image = Field("image", str, data.get("image", KeyError))
|
||||
|
||||
# Validation
|
||||
self.image_uuid.validation = self.image_uuid_validation
|
||||
self.image.validation = self.image_validation
|
||||
self.vm_name.validation = self.vm_name_validation
|
||||
self.specs.validation = self.specs_validation
|
||||
|
||||
fields = [self.vm_name, self.image_uuid, self.specs]
|
||||
fields = [self.vm_name, self.image, self.specs]
|
||||
|
||||
super().__init__(data=data, fields=fields)
|
||||
|
||||
def image_uuid_validation(self):
|
||||
images = client.get_prefix(IMAGE_PREFIX)
|
||||
|
||||
if self.image_uuid.value not in [i.key.split("/")[-1] for i in images]:
|
||||
self.add_error("Image UUID not valid")
|
||||
def image_validation(self):
|
||||
try:
|
||||
image_uuid = helper.resolve_image_name(self.image.value, client)
|
||||
except Exception as e:
|
||||
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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue