Refactoring, VMM added, uncloud-host mostly new, migration is better now
This commit is contained in:
		
					parent
					
						
							
								cd9d4cb78c
							
						
					
				
			
			
				commit
				
					
						ba515f0b48
					
				
			
		
					 12 changed files with 423 additions and 364 deletions
				
			
		
							
								
								
									
										2
									
								
								setup.py
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
										
									
									
									
								
							|  | @ -40,7 +40,7 @@ setup(name='ucloud', | ||||||
|           'colorama', |           'colorama', | ||||||
|           'sphinx-rtd-theme', |           'sphinx-rtd-theme', | ||||||
|           'etcd3 @ https://github.com/kragniz/python-etcd3/tarball/master#egg=etcd3', |           'etcd3 @ https://github.com/kragniz/python-etcd3/tarball/master#egg=etcd3', | ||||||
|           'werkzeug' |           'werkzeug', 'marshmallow' | ||||||
|       ], |       ], | ||||||
|       scripts=['scripts/ucloud'], |       scripts=['scripts/ucloud'], | ||||||
|       data_files=[(os.path.expanduser('~/ucloud/'), ['conf/ucloud.conf'])], |       data_files=[(os.path.expanduser('~/ucloud/'), ['conf/ucloud.conf'])], | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import json | import json | ||||||
| import pynetbox | import pynetbox | ||||||
| import logging | import logging | ||||||
| import urllib3 |  | ||||||
| 
 | 
 | ||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
| from os.path import join as join_path | from os.path import join as join_path | ||||||
|  | @ -78,6 +77,7 @@ class CreateVM(Resource): | ||||||
|                 "vnc_socket": "", |                 "vnc_socket": "", | ||||||
|                 "network": list(zip(data["network"], macs, tap_ids)), |                 "network": list(zip(data["network"], macs, tap_ids)), | ||||||
|                 "metadata": {"ssh-keys": []}, |                 "metadata": {"ssh-keys": []}, | ||||||
|  |                 "in_migration": False | ||||||
|             } |             } | ||||||
|             shared.etcd_client.put(vm_key, vm_entry, value_in_json=True) |             shared.etcd_client.put(vm_key, vm_entry, value_in_json=True) | ||||||
| 
 | 
 | ||||||
|  | @ -216,16 +216,13 @@ class VMMigration(Resource): | ||||||
| 
 | 
 | ||||||
|         if validator.is_valid(): |         if validator.is_valid(): | ||||||
|             vm = shared.vm_pool.get(data["uuid"]) |             vm = shared.vm_pool.get(data["uuid"]) | ||||||
| 
 |             r = RequestEntry.from_scratch(type=RequestType.InitVMMigration, | ||||||
|             r = RequestEntry.from_scratch( |  | ||||||
|                 type=RequestType.ScheduleVM, |  | ||||||
|                                           uuid=vm.uuid, |                                           uuid=vm.uuid, | ||||||
|                 destination=join_path( |                                           hostname=join_path( | ||||||
|                                               settings['etcd']['host_prefix'], validator.destination.value |                                               settings['etcd']['host_prefix'], validator.destination.value | ||||||
|                                           ), |                                           ), | ||||||
|                 migration=True, |                                           request_prefix=settings['etcd']['request_prefix']) | ||||||
|                 request_prefix=settings['etcd']['request_prefix'] | 
 | ||||||
|             ) |  | ||||||
|             shared.request_pool.put(r) |             shared.request_pool.put(r) | ||||||
|             return {"message": "VM Migration Initialization Queued"}, 200 |             return {"message": "VM Migration Initialization Queued"}, 200 | ||||||
|         else: |         else: | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ def generate_mac(uaa=False, multicast=False, oui=None, separator=':', byte_fmt=' | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def create_dev(script, _id, dev, ip=None): | def create_dev(script, _id, dev, ip=None): | ||||||
|     command = [script, _id, dev] |     command = [script, str(_id), dev] | ||||||
|     if ip: |     if ip: | ||||||
|         command.append(ip) |         command.append(ip) | ||||||
|     try: |     try: | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ class RequestType: | ||||||
| class RequestEntry(SpecificEtcdEntryBase): | class RequestEntry(SpecificEtcdEntryBase): | ||||||
| 
 | 
 | ||||||
|     def __init__(self, e): |     def __init__(self, e): | ||||||
|  |         self.destination_host_key = None | ||||||
|         self.type = None  # type: str |         self.type = None  # type: str | ||||||
|         self.migration = None  # type: bool |         self.migration = None  # type: bool | ||||||
|         self.destination = None  # type: str |         self.destination = None  # type: str | ||||||
|  |  | ||||||
							
								
								
									
										39
									
								
								ucloud/common/schemas.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								ucloud/common/schemas.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | ||||||
|  | import bitmath | ||||||
|  | 
 | ||||||
|  | from marshmallow import fields, Schema | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class StorageUnit(fields.Field): | ||||||
|  |     def _serialize(self, value, attr, obj, **kwargs): | ||||||
|  |         return str(value) | ||||||
|  | 
 | ||||||
|  |     def _deserialize(self, value, attr, data, **kwargs): | ||||||
|  |         return bitmath.parse_string_unsafe(value) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SpecsSchema(Schema): | ||||||
|  |     cpu = fields.Int() | ||||||
|  |     ram = StorageUnit() | ||||||
|  |     os_ssd = StorageUnit(data_key='os-ssd', attribute='os-ssd') | ||||||
|  |     hdd = fields.List(StorageUnit()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class VMSchema(Schema): | ||||||
|  |     name = fields.Str() | ||||||
|  |     owner = fields.Str() | ||||||
|  |     owner_realm = fields.Str() | ||||||
|  |     specs = fields.Nested(SpecsSchema) | ||||||
|  |     status = fields.Str() | ||||||
|  |     log = fields.List(fields.Str()) | ||||||
|  |     vnc_socket = fields.Str() | ||||||
|  |     image_uuid = fields.Str() | ||||||
|  |     hostname = fields.Str() | ||||||
|  |     metadata = fields.Dict() | ||||||
|  |     network = fields.List(fields.Tuple((fields.Str(), fields.Str(), fields.Int()))) | ||||||
|  |     in_migration = fields.Bool() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class NetworkSchema(Schema): | ||||||
|  |     _id = fields.Int(data_key='id', attribute='id') | ||||||
|  |     _type = fields.Str(data_key='type', attribute='type') | ||||||
|  |     ipv6 = fields.Str() | ||||||
|  | @ -19,8 +19,8 @@ class ImageStorageHandler(ABC): | ||||||
| 
 | 
 | ||||||
|     def import_image(self, image_src, image_dest, protect=False): |     def import_image(self, image_src, image_dest, protect=False): | ||||||
|         """Put an image at the destination |         """Put an image at the destination | ||||||
|         :param src: An Image file |         :param image_src: An Image file | ||||||
|         :param dest: A path where :param src: is to be put. |         :param image_dest: A path where :param src: is to be put. | ||||||
|         :param protect: If protect is true then the dest is protect (readonly etc) |         :param protect: If protect is true then the dest is protect (readonly etc) | ||||||
|         The obj must exist on filesystem. |         The obj must exist on filesystem. | ||||||
|         """ |         """ | ||||||
|  | @ -30,8 +30,8 @@ class ImageStorageHandler(ABC): | ||||||
|     def make_vm_image(self, image_path, path): |     def make_vm_image(self, image_path, path): | ||||||
|         """Copy image from src to dest |         """Copy image from src to dest | ||||||
| 
 | 
 | ||||||
|         :param src: A path |         :param image_path: A path | ||||||
|         :param dest: A path |         :param path: A path | ||||||
| 
 | 
 | ||||||
|         src and destination must be on same storage system i.e both on file system or both on CEPH etc. |         src and destination must be on same storage system i.e both on file system or both on CEPH etc. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|  | @ -12,6 +12,12 @@ class VMStatus: | ||||||
|     error = "ERROR"  # An error occurred that cannot be resolved automatically |     error = "ERROR"  # An error occurred that cannot be resolved automatically | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def declare_stopped(vm): | ||||||
|  |     vm['hostname'] = '' | ||||||
|  |     vm['in_migration'] = False | ||||||
|  |     vm['status'] = VMStatus.stopped | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class VMEntry(SpecificEtcdEntryBase): | class VMEntry(SpecificEtcdEntryBase): | ||||||
| 
 | 
 | ||||||
|     def __init__(self, e): |     def __init__(self, e): | ||||||
|  |  | ||||||
|  | @ -1,17 +1,16 @@ | ||||||
| import argparse | import argparse | ||||||
| import multiprocessing as mp | import multiprocessing as mp | ||||||
| import time | import time | ||||||
| import sys |  | ||||||
| 
 | 
 | ||||||
| from ucloud.common.request import RequestEntry, RequestType | from ucloud.common.request import RequestEntry, RequestType | ||||||
| from ucloud.common.host import HostPool |  | ||||||
| from ucloud.shared import shared | from ucloud.shared import shared | ||||||
| from ucloud.settings import settings | from ucloud.settings import settings | ||||||
|  | from ucloud.common.vm import VMStatus | ||||||
|  | from ucloud.vmm import VMM | ||||||
|  | from os.path import join as join_path | ||||||
| 
 | 
 | ||||||
| from . import virtualmachine, logger | from . import virtualmachine, logger | ||||||
| 
 | 
 | ||||||
| vmm = virtualmachine.VMM() |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| def update_heartbeat(hostname): | def update_heartbeat(hostname): | ||||||
|     """Update Last HeartBeat Time for :param hostname: in etcd""" |     """Update Last HeartBeat Time for :param hostname: in etcd""" | ||||||
|  | @ -25,6 +24,16 @@ def update_heartbeat(hostname): | ||||||
|         time.sleep(10) |         time.sleep(10) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def maintenance(): | ||||||
|  |     vmm = VMM() | ||||||
|  |     running_vms = vmm.discover() | ||||||
|  |     for vm_uuid in running_vms: | ||||||
|  |         if vmm.is_running(vm_uuid) and vmm.get_status(vm_uuid) == 'running': | ||||||
|  |             vm = shared.vm_pool.get(join_path(settings['etcd']['vm_prefix'], vm_uuid)) | ||||||
|  |             vm.status = VMStatus.running | ||||||
|  |             shared.vm_pool.put(vm) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def main(hostname): | def main(hostname): | ||||||
|     host_pool = shared.host_pool |     host_pool = shared.host_pool | ||||||
|     host = next(filter(lambda h: h.hostname == hostname, host_pool.hosts), None) |     host = next(filter(lambda h: h.hostname == hostname, host_pool.hosts), None) | ||||||
|  | @ -34,8 +43,7 @@ def main(hostname): | ||||||
|         heartbeat_updating_process = mp.Process(target=update_heartbeat, args=(hostname,)) |         heartbeat_updating_process = mp.Process(target=update_heartbeat, args=(hostname,)) | ||||||
|         heartbeat_updating_process.start() |         heartbeat_updating_process.start() | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         logger.exception(e) |         raise e.__class__('ucloud-host heartbeat updating mechanism is not working') from e | ||||||
|         sys.exit("No Need To Go Further. ucloud-host heartbeat updating mechanism is not working") |  | ||||||
| 
 | 
 | ||||||
|     for events_iterator in [ |     for events_iterator in [ | ||||||
|         shared.etcd_client.get_prefix(settings['etcd']['request_prefix'], value_in_json=True), |         shared.etcd_client.get_prefix(settings['etcd']['request_prefix'], value_in_json=True), | ||||||
|  | @ -45,36 +53,37 @@ def main(hostname): | ||||||
|             request_event = RequestEntry(request_event) |             request_event = RequestEntry(request_event) | ||||||
| 
 | 
 | ||||||
|             if request_event.type == "TIMEOUT": |             if request_event.type == "TIMEOUT": | ||||||
|                 vmm.maintenance(host) |                 maintenance() | ||||||
|                 continue |  | ||||||
| 
 | 
 | ||||||
|             # If the event is directed toward me OR I am destination of a InitVMMigration |             if request_event.hostname == host.key: | ||||||
|             if request_event.hostname == host.key or request_event.destination == host.key: |  | ||||||
|                 logger.debug("VM Request: %s", request_event) |                 logger.debug("VM Request: %s", request_event) | ||||||
| 
 | 
 | ||||||
|                 shared.request_pool.client.client.delete(request_event.key) |                 shared.request_pool.client.client.delete(request_event.key) | ||||||
|                 vm_entry = shared.vm_pool.get(request_event.uuid) |                 vm_entry = shared.etcd_client.get(join_path(settings['etcd']['vm_prefix'], request_event.uuid)) | ||||||
| 
 | 
 | ||||||
|                 if vm_entry: |                 if vm_entry: | ||||||
|  |                     vm = virtualmachine.VM(vm_entry) | ||||||
|                     if request_event.type == RequestType.StartVM: |                     if request_event.type == RequestType.StartVM: | ||||||
|                         vmm.start(vm_entry) |                         vm.start() | ||||||
| 
 | 
 | ||||||
|                     elif request_event.type == RequestType.StopVM: |                     elif request_event.type == RequestType.StopVM: | ||||||
|                         vmm.stop(vm_entry) |                         vm.stop() | ||||||
| 
 | 
 | ||||||
|                     elif request_event.type == RequestType.DeleteVM: |                     elif request_event.type == RequestType.DeleteVM: | ||||||
|                         vmm.delete(vm_entry) |                         vm.delete() | ||||||
| 
 | 
 | ||||||
|                     elif request_event.type == RequestType.InitVMMigration: |                     elif request_event.type == RequestType.InitVMMigration: | ||||||
|                         vmm.start(vm_entry, host.key) |                         vm.start(destination_host_key=host.key) | ||||||
| 
 | 
 | ||||||
|                     elif request_event.type == RequestType.TransferVM: |                     elif request_event.type == RequestType.TransferVM: | ||||||
|                         vmm.transfer(request_event) |                         host = host_pool.get(request_event.destination_host_key) | ||||||
|  |                         if host: | ||||||
|  |                             vm.migrate(destination=host.hostname) | ||||||
|  |                         else: | ||||||
|  |                             logger.error('Host %s not found!', request_event.destination_host_key) | ||||||
|                 else: |                 else: | ||||||
|                     logger.info("VM Entry missing") |                     logger.info("VM Entry missing") | ||||||
| 
 | 
 | ||||||
|                 logger.info("Running VMs %s", vmm.running_vms) |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     argparser = argparse.ArgumentParser() |     argparser = argparse.ArgumentParser() | ||||||
|  |  | ||||||
|  | @ -6,325 +6,138 @@ | ||||||
| 
 | 
 | ||||||
| import os | import os | ||||||
| import subprocess as sp | import subprocess as sp | ||||||
| import tempfile |  | ||||||
| import time |  | ||||||
| import ipaddress | import ipaddress | ||||||
| 
 | 
 | ||||||
| from functools import wraps |  | ||||||
| from string import Template | from string import Template | ||||||
| from typing import Union |  | ||||||
| from os.path import join as join_path | from os.path import join as join_path | ||||||
| 
 | 
 | ||||||
| import bitmath |  | ||||||
| import sshtunnel |  | ||||||
| 
 |  | ||||||
| from ucloud.common.helpers import get_ipv6_address |  | ||||||
| from ucloud.common.request import RequestEntry, RequestType | from ucloud.common.request import RequestEntry, RequestType | ||||||
| from ucloud.common.vm import VMEntry, VMStatus | from ucloud.common.vm import VMStatus, declare_stopped | ||||||
| from ucloud.common.network import create_dev, delete_network_interface, find_free_port | from ucloud.common.network import create_dev, delete_network_interface | ||||||
|  | from ucloud.common.schemas import VMSchema, NetworkSchema | ||||||
| from ucloud.host import logger | from ucloud.host import logger | ||||||
| from ucloud.shared import shared | from ucloud.shared import shared | ||||||
| from ucloud.settings import settings | from ucloud.settings import settings | ||||||
|  | from ucloud.vmm import VMM | ||||||
| 
 | 
 | ||||||
| from . import qmp | from marshmallow import ValidationError | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def maintenance(): | ||||||
|  |     pass | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class VM: | class VM: | ||||||
|     def __init__(self, key, handle, vnc_socket_file): |     def __init__(self, vm_entry): | ||||||
|         self.key = key  # type: str |         self.schema = VMSchema() | ||||||
|         self.handle = handle  # type: qmp.QEMUMachine |         self.vmm = VMM() | ||||||
|         self.vnc_socket_file = vnc_socket_file  # type: tempfile.NamedTemporaryFile |         self.key = vm_entry.key | ||||||
| 
 |  | ||||||
|     def __repr__(self): |  | ||||||
|         return "VM({})".format(self.key) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def capture_all_exception(func): |  | ||||||
|     @wraps(func) |  | ||||||
|     def wrapper(*args, **kwargs): |  | ||||||
|         try: |         try: | ||||||
|             func(*args, **kwargs) |             self.vm = self.schema.loads(vm_entry.value) | ||||||
|         except Exception: |         except ValidationError: | ||||||
|             logger.exception('Unhandled exception occur in %s. For more details see Syslog.', __name__) |             logger.exception('Couldn\'t validate VM Entry', vm_entry.value) | ||||||
| 
 |             self.vm = None | ||||||
|     return wrapper |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class VMM: |  | ||||||
|     def __init__(self): |  | ||||||
|         self.etcd_client = shared.etcd_client |  | ||||||
|         self.storage_handler = shared.storage_handler |  | ||||||
|         self.running_vms = [] |  | ||||||
| 
 |  | ||||||
|     def get_start_command_args(self, vm_entry, vnc_sock_filename: str, migration=False, migration_port=None): |  | ||||||
|         threads_per_core = 1 |  | ||||||
|         vm_memory = int(bitmath.parse_string_unsafe(vm_entry.specs['ram']).to_MB()) |  | ||||||
|         vm_cpus = int(vm_entry.specs['cpu']) |  | ||||||
|         vm_uuid = vm_entry.uuid |  | ||||||
|         vm_networks = vm_entry.network |  | ||||||
| 
 |  | ||||||
|         command = '-name {}_{}'.format(vm_entry.owner, vm_entry.name) |  | ||||||
| 
 |  | ||||||
|         command += ' -drive file={},format=raw,if=virtio,cache=none'.format( |  | ||||||
|             self.storage_handler.qemu_path_string(vm_uuid) |  | ||||||
|         ) |  | ||||||
|         command += ' -device virtio-rng-pci -vnc unix:{}'.format(vnc_sock_filename) |  | ||||||
|         command += ' -m {} -smp cores={},threads={}'.format( |  | ||||||
|             vm_memory, vm_cpus, threads_per_core |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         if migration: |  | ||||||
|             command += ' -incoming tcp:[::]:{}'.format(migration_port) |  | ||||||
| 
 |  | ||||||
|         for network_mac_and_tap in vm_networks: |  | ||||||
|             network_name, mac, tap = network_mac_and_tap |  | ||||||
| 
 |  | ||||||
|             _key = os.path.join(settings['etcd']['network_prefix'], vm_entry.owner, network_name) |  | ||||||
|             network = self.etcd_client.get(_key, value_in_json=True) |  | ||||||
|             network_type = network.value["type"] |  | ||||||
|             network_id = str(network.value["id"]) |  | ||||||
|             network_ipv6 = network.value["ipv6"] |  | ||||||
| 
 |  | ||||||
|             if network_type == "vxlan": |  | ||||||
|                 tap = create_vxlan_br_tap(_id=network_id, |  | ||||||
|                                           _dev=settings['network']['vxlan_phy_dev'], |  | ||||||
|                                           tap_id=tap, |  | ||||||
|                                           ip=network_ipv6) |  | ||||||
|                 all_networks = self.etcd_client.get_prefix('/v1/network/', value_in_json=True) |  | ||||||
| 
 |  | ||||||
|                 if ipaddress.ip_network(network_ipv6).is_global: |  | ||||||
|                     update_radvd_conf(all_networks) |  | ||||||
| 
 |  | ||||||
|             command += " -netdev tap,id=vmnet{net_id},ifname={tap},script=no,downscript=no" \ |  | ||||||
|                        " -device virtio-net-pci,netdev=vmnet{net_id},mac={mac}" \ |  | ||||||
|                 .format(tap=tap, net_id=network_id, mac=mac) |  | ||||||
| 
 |  | ||||||
|         return command.split(" ") |  | ||||||
| 
 |  | ||||||
|     def create_vm_object(self, vm_entry, migration=False, migration_port=None): |  | ||||||
|         vnc_sock_file = tempfile.NamedTemporaryFile() |  | ||||||
| 
 |  | ||||||
|         qemu_args = self.get_start_command_args( |  | ||||||
|             vm_entry=vm_entry, |  | ||||||
|             vnc_sock_filename=vnc_sock_file.name, |  | ||||||
|             migration=migration, |  | ||||||
|             migration_port=migration_port, |  | ||||||
|         ) |  | ||||||
|         qemu_machine = qmp.QEMUMachine("/usr/bin/qemu-system-x86_64", args=qemu_args) |  | ||||||
|         return VM(vm_entry.key, qemu_machine, vnc_sock_file) |  | ||||||
| 
 |  | ||||||
|     @staticmethod |  | ||||||
|     def get_vm(vm_list: list, vm_key) -> Union[VM, None]: |  | ||||||
|         return next((vm for vm in vm_list if vm.key == vm_key), None) |  | ||||||
| 
 |  | ||||||
|     @capture_all_exception |  | ||||||
|     def create(self, vm_entry: VMEntry): |  | ||||||
|         if self.storage_handler.is_vm_image_exists(vm_entry.uuid): |  | ||||||
|             # File Already exists. No Problem Continue |  | ||||||
|             logger.debug("Image for vm %s exists", vm_entry.uuid) |  | ||||||
|             return None |  | ||||||
|         else: |         else: | ||||||
|             vm_hdd = int(bitmath.parse_string_unsafe(vm_entry.specs["os-ssd"]).to_MB()) |             self.uuid = vm_entry.key.split('/')[-1] | ||||||
|             if self.storage_handler.make_vm_image(src=vm_entry.image_uuid, dest=vm_entry.uuid): |             self.host_key = self.vm['hostname'] | ||||||
|                 if not self.storage_handler.resize_vm_image(path=vm_entry.uuid, size=vm_hdd): |  | ||||||
|                     vm_entry.status = VMStatus.error |  | ||||||
|                 else: |  | ||||||
|                     logger.info("New VM Created") |  | ||||||
| 
 | 
 | ||||||
|     @capture_all_exception |     def get_qemu_args(self): | ||||||
|     def start(self, vm_entry: VMEntry, destination_host_key=None): |         command = ( | ||||||
|         _vm = self.get_vm(self.running_vms, vm_entry.key) |             '-name {owner}_{name}' | ||||||
|  |             ' -drive file={file},format=raw,if=virtio,cache=none' | ||||||
|  |             ' -device virtio-rng-pci' | ||||||
|  |             ' -m {memory} -smp cores={cores},threads={threads}' | ||||||
|  |         ).format(owner=self.vm['owner'], name=self.vm['name'], | ||||||
|  |                  memory=int(self.vm['specs']['ram'].to_MB()), cores=self.vm['specs']['cpu'], | ||||||
|  |                  threads=1, file=shared.storage_handler.qemu_path_string(self.uuid)) | ||||||
| 
 | 
 | ||||||
|         # VM already running. No need to proceed further. |         return command.split(' ') | ||||||
|         if _vm: | 
 | ||||||
|             logger.info("VM %s already running" % vm_entry.uuid) |     def start(self, destination_host_key=None): | ||||||
|             return |         migration = False | ||||||
|         else: |  | ||||||
|             logger.info("Trying to start %s" % vm_entry.uuid) |  | ||||||
|         if destination_host_key: |         if destination_host_key: | ||||||
|                 migration_port = find_free_port() |             migration = True | ||||||
|                 self.launch_vm(vm_entry, migration=True, migration_port=migration_port, |  | ||||||
|                                destination_host_key=destination_host_key) |  | ||||||
|             else: |  | ||||||
|                 self.create(vm_entry) |  | ||||||
|                 self.launch_vm(vm_entry) |  | ||||||
| 
 | 
 | ||||||
|     @capture_all_exception |         self.create() | ||||||
|     def stop(self, vm_entry): |  | ||||||
|         vm = self.get_vm(self.running_vms, vm_entry.key) |  | ||||||
|         vm.handle.shutdown() |  | ||||||
|         if not vm.handle.is_running(): |  | ||||||
|             vm_entry.add_log("Shutdown successfully") |  | ||||||
|             vm_entry.declare_stopped() |  | ||||||
|             shared.vm_pool.put(vm_entry) |  | ||||||
|             self.running_vms.remove(vm) |  | ||||||
|             delete_vm_network(vm_entry) |  | ||||||
| 
 |  | ||||||
|     @capture_all_exception |  | ||||||
|     def delete(self, vm_entry): |  | ||||||
|         logger.info("Deleting VM | %s", vm_entry) |  | ||||||
|         self.stop(vm_entry) |  | ||||||
| 
 |  | ||||||
|         if self.storage_handler.is_vm_image_exists(vm_entry.uuid): |  | ||||||
|             r_status = self.storage_handler.delete_vm_image(vm_entry.uuid) |  | ||||||
|             if r_status: |  | ||||||
|                 shared.etcd_client.client.delete(vm_entry.key) |  | ||||||
|         else: |  | ||||||
|             shared.etcd_client.client.delete(vm_entry.key) |  | ||||||
| 
 |  | ||||||
|     @capture_all_exception |  | ||||||
|     def transfer(self, request_event): |  | ||||||
|         # This function would run on source host i.e host on which the vm |  | ||||||
|         # is running initially. This host would be responsible for transferring |  | ||||||
|         # vm state to destination host. |  | ||||||
| 
 |  | ||||||
|         _host, _port = request_event.parameters["host"], request_event.parameters["port"] |  | ||||||
|         _uuid = request_event.uuid |  | ||||||
|         _destination = request_event.destination_host_key |  | ||||||
|         vm = self.get_vm(self.running_vms, join_path(settings['etcd']['vm_prefix'], _uuid)) |  | ||||||
| 
 |  | ||||||
|         if vm: |  | ||||||
|             tunnel = sshtunnel.SSHTunnelForwarder( |  | ||||||
|                 _host, |  | ||||||
|                 ssh_username=settings['ssh']['username'], |  | ||||||
|                 ssh_pkey=settings['ssh']['private_key_path'], |  | ||||||
|                 remote_bind_address=("127.0.0.1", _port), |  | ||||||
|                 ssh_proxy_enabled=True, |  | ||||||
|                 ssh_proxy=(_host, 22) |  | ||||||
|             ) |  | ||||||
|         try: |         try: | ||||||
|                 tunnel.start() |             network_args = self.create_network_dev() | ||||||
|             except sshtunnel.BaseSSHTunnelForwarderError: |         except Exception as err: | ||||||
|                 logger.exception("Couldn't establish connection to (%s, 22)", _host) |             declare_stopped(self.vm) | ||||||
|  |             self.vm['log'].append('Cannot Setup Network Properly') | ||||||
|  |             logger.error('Cannot Setup Network Properly for vm %s', self.uuid, exc_info=err) | ||||||
|         else: |         else: | ||||||
|                 vm.handle.command( |             self.vmm.start(uuid=self.uuid, migration=migration, | ||||||
|                     "migrate", uri="tcp:0.0.0.0:{}".format(tunnel.local_bind_port) |                            *self.get_qemu_args(), *network_args) | ||||||
|                 ) |  | ||||||
| 
 | 
 | ||||||
|                 status = vm.handle.command("query-migrate")["status"] |             status = self.vmm.get_status(self.uuid) | ||||||
|                 while status not in ["failed", "completed"]: |             if status == 'running': | ||||||
|                     time.sleep(2) |                 self.vm['status'] = VMStatus.running | ||||||
|                     status = vm.handle.command("query-migrate")["status"] |                 self.vm['vnc_socket'] = self.vmm.get_vnc(self.uuid) | ||||||
| 
 |             elif status == 'inmigrate': | ||||||
|                 with shared.vm_pool.get_put(request_event.uuid) as source_vm: |  | ||||||
|                     if status == "failed": |  | ||||||
|                         source_vm.add_log("Migration Failed") |  | ||||||
|                     elif status == "completed": |  | ||||||
|                         # If VM is successfully migrated then shutdown the VM |  | ||||||
|                         # on this host and update hostname to destination host key |  | ||||||
|                         source_vm.add_log("Successfully migrated") |  | ||||||
|                         source_vm.hostname = _destination |  | ||||||
|                         self.running_vms.remove(vm) |  | ||||||
|                         vm.handle.shutdown() |  | ||||||
|                     source_vm.in_migration = False  # VM transfer finished |  | ||||||
|             finally: |  | ||||||
|                 tunnel.close() |  | ||||||
| 
 |  | ||||||
|     @capture_all_exception |  | ||||||
|     def launch_vm(self, vm_entry, migration=False, migration_port=None, destination_host_key=None): |  | ||||||
|         logger.info("Starting %s" % vm_entry.key) |  | ||||||
| 
 |  | ||||||
|         vm = self.create_vm_object(vm_entry, migration=migration, migration_port=migration_port) |  | ||||||
|         try: |  | ||||||
|             vm.handle.launch() |  | ||||||
|         except Exception: |  | ||||||
|             logger.exception("Error Occured while starting VM") |  | ||||||
|             vm.handle.shutdown() |  | ||||||
| 
 |  | ||||||
|             if migration: |  | ||||||
|                 # We don't care whether MachineError or any other error occurred |  | ||||||
|                 pass |  | ||||||
|             else: |  | ||||||
|                 # Error during typical launch of a vm |  | ||||||
|                 vm.handle.shutdown() |  | ||||||
|                 vm_entry.declare_killed() |  | ||||||
|                 shared.vm_pool.put(vm_entry) |  | ||||||
|         else: |  | ||||||
|             vm_entry.vnc_socket = vm.vnc_socket_file.name |  | ||||||
|             self.running_vms.append(vm) |  | ||||||
| 
 |  | ||||||
|             if migration: |  | ||||||
|                 vm_entry.in_migration = True |  | ||||||
|                 r = RequestEntry.from_scratch( |                 r = RequestEntry.from_scratch( | ||||||
|                     type=RequestType.TransferVM, |                     type=RequestType.TransferVM,  # Transfer VM | ||||||
|                     hostname=vm_entry.hostname, |                     hostname=self.host_key,  # Which VM should get this request. It is source host | ||||||
|                     parameters={"host": get_ipv6_address(), "port": migration_port}, |                     uuid=self.uuid,  # uuid of VM | ||||||
|                     uuid=vm_entry.uuid, |                     destination_host_key=destination_host_key,  # Where source host transfer VM | ||||||
|                     destination_host_key=destination_host_key, |  | ||||||
|                     request_prefix=settings['etcd']['request_prefix'] |                     request_prefix=settings['etcd']['request_prefix'] | ||||||
|                 ) |                 ) | ||||||
|                 shared.request_pool.put(r) |                 shared.request_pool.put(r) | ||||||
|             else: |             else: | ||||||
|                 # Typical launching of a vm |                 self.stop() | ||||||
|                 vm_entry.status = VMStatus.running |                 declare_stopped(self.vm) | ||||||
|                 vm_entry.add_log("Started successfully") |  | ||||||
| 
 | 
 | ||||||
|             shared.vm_pool.put(vm_entry) |         self.sync() | ||||||
| 
 | 
 | ||||||
|     @capture_all_exception |     def stop(self): | ||||||
|     def maintenance(self, host): |         self.vmm.stop(self.uuid) | ||||||
|         # To capture vm running according to running_vms list |         self.delete_network_dev() | ||||||
|  |         declare_stopped(self.vm) | ||||||
|  |         self.sync() | ||||||
| 
 | 
 | ||||||
|         # This is to capture successful migration of a VM. |     def migrate(self, destination): | ||||||
|         # Suppose, this host is running "vm1" and user initiated |         self.vmm.transfer(src_uuid=self.uuid, dest_uuid=self.uuid, host=destination) | ||||||
|         # request to migrate this "vm1" to some other host. On, |  | ||||||
|         # successful migration the destination host would set |  | ||||||
|         # the vm hostname to itself. Thus, we are checking |  | ||||||
|         # whether this host vm is successfully migrated. If yes |  | ||||||
|         # then we shutdown "vm1" on this host. |  | ||||||
|         logger.debug("Starting Maintenance!!") |  | ||||||
|         to_be_removed = [] |  | ||||||
|         for running_vm in self.running_vms: |  | ||||||
|             with shared.vm_pool.get_put(running_vm.key) as vm_entry: |  | ||||||
|                 if vm_entry.hostname != host.key and not vm_entry.in_migration: |  | ||||||
|                     running_vm.handle.shutdown() |  | ||||||
|                     logger.info("VM migration not completed successfully.") |  | ||||||
|                     to_be_removed.append(running_vm) |  | ||||||
| 
 | 
 | ||||||
|         for r in to_be_removed: |     def create_network_dev(self): | ||||||
|             self.running_vms.remove(r) |         command = '' | ||||||
|  |         for network_mac_and_tap in self.vm['network']: | ||||||
|  |             network_name, mac, tap = network_mac_and_tap | ||||||
| 
 | 
 | ||||||
|         # To check vm running according to etcd entries |             _key = os.path.join(settings['etcd']['network_prefix'], self.vm['owner'], network_name) | ||||||
|         alleged_running_vms = shared.vm_pool.by_status("RUNNING", shared.vm_pool.by_host(host.key)) |             network = shared.etcd_client.get(_key, value_in_json=True) | ||||||
| 
 |             network_schema = NetworkSchema() | ||||||
|         for vm_entry in alleged_running_vms: |  | ||||||
|             _vm = self.get_vm(self.running_vms, vm_entry.key) |  | ||||||
|             # Whether, the allegedly running vm is in our |  | ||||||
|             # running_vms list or not if it is said to be |  | ||||||
|             # running on this host but it is not then we |  | ||||||
|             # need to shut it down |  | ||||||
| 
 |  | ||||||
|             # This is to capture poweroff/shutdown of a VM |  | ||||||
|             # initiated by user inside VM. OR crash of VM by some |  | ||||||
|             # user running process |  | ||||||
|             if (_vm and not _vm.handle.is_running()) or not _vm: |  | ||||||
|                 logger.debug("_vm = %s, is_running() = %s" % (_vm, _vm.handle.is_running())) |  | ||||||
|                 vm_entry.add_log("""{} is not running but is said to be running. |  | ||||||
|                                     So, shutting it down and declare it killed""".format(vm_entry.key)) |  | ||||||
|                 vm_entry.declare_killed() |  | ||||||
|                 shared.vm_pool.put(vm_entry) |  | ||||||
|                 if _vm: |  | ||||||
|                     self.running_vms.remove(_vm) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def resolve_network(network_name, network_owner): |  | ||||||
|     network = shared.etcd_client.get(join_path(settings['etcd']['network_prefix'], |  | ||||||
|                                         network_owner, |  | ||||||
|                                         network_name), |  | ||||||
|                               value_in_json=True) |  | ||||||
|     return network |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def delete_vm_network(vm_entry): |  | ||||||
|             try: |             try: | ||||||
|         for network in vm_entry.network: |                 network = network_schema.load(network.value) | ||||||
|  |             except ValidationError: | ||||||
|  |                 continue | ||||||
|  | 
 | ||||||
|  |             if network['type'] == "vxlan": | ||||||
|  |                 tap = create_vxlan_br_tap(_id=network['id'], | ||||||
|  |                                           _dev=settings['network']['vxlan_phy_dev'], | ||||||
|  |                                           tap_id=tap, | ||||||
|  |                                           ip=network['ipv6']) | ||||||
|  | 
 | ||||||
|  |                 all_networks = shared.etcd_client.get_prefix(settings['etcd']['network_prefix'], | ||||||
|  |                                                              value_in_json=True) | ||||||
|  | 
 | ||||||
|  |                 if ipaddress.ip_network(network['ipv6']).is_global: | ||||||
|  |                     update_radvd_conf(all_networks) | ||||||
|  | 
 | ||||||
|  |             command += '-netdev tap,id=vmnet{net_id},ifname={tap},script=no,downscript=no' \ | ||||||
|  |                        ' -device virtio-net-pci,netdev=vmnet{net_id},mac={mac}' \ | ||||||
|  |                        .format(tap=tap, net_id=network['id'], mac=mac) | ||||||
|  | 
 | ||||||
|  |         return command.split(' ') | ||||||
|  | 
 | ||||||
|  |     def delete_network_dev(self): | ||||||
|  |         try: | ||||||
|  |             for network in self.vm['network']: | ||||||
|                 network_name = network[0] |                 network_name = network[0] | ||||||
|             tap_mac = network[1] |                 _ = network[1]  # tap_mac | ||||||
|                 tap_id = network[2] |                 tap_id = network[2] | ||||||
| 
 | 
 | ||||||
|                 delete_network_interface('tap{}'.format(tap_id)) |                 delete_network_interface('tap{}'.format(tap_id)) | ||||||
| 
 | 
 | ||||||
|             owners_vms = shared.vm_pool.by_owner(vm_entry.owner) |                 owners_vms = shared.vm_pool.by_owner(self.vm['owner']) | ||||||
|                 owners_running_vms = shared.vm_pool.by_status(VMStatus.running, |                 owners_running_vms = shared.vm_pool.by_status(VMStatus.running, | ||||||
|                                                               _vms=owners_vms) |                                                               _vms=owners_vms) | ||||||
| 
 | 
 | ||||||
|  | @ -333,7 +146,7 @@ def delete_vm_network(vm_entry): | ||||||
|                 ) |                 ) | ||||||
|                 networks_in_use_by_user_vms = [vm[0] for vm in networks] |                 networks_in_use_by_user_vms = [vm[0] for vm in networks] | ||||||
|                 if network_name not in networks_in_use_by_user_vms: |                 if network_name not in networks_in_use_by_user_vms: | ||||||
|                 network_entry = resolve_network(network[0], vm_entry.owner) |                     network_entry = resolve_network(network[0], self.vm['owner']) | ||||||
|                     if network_entry: |                     if network_entry: | ||||||
|                         network_type = network_entry.value["type"] |                         network_type = network_entry.value["type"] | ||||||
|                         network_id = network_entry.value["id"] |                         network_id = network_entry.value["id"] | ||||||
|  | @ -343,6 +156,38 @@ def delete_vm_network(vm_entry): | ||||||
|         except Exception: |         except Exception: | ||||||
|             logger.exception("Exception in network interface deletion") |             logger.exception("Exception in network interface deletion") | ||||||
| 
 | 
 | ||||||
|  |     def create(self): | ||||||
|  |         if shared.storage_handler.is_vm_image_exists(self.uuid): | ||||||
|  |             # File Already exists. No Problem Continue | ||||||
|  |             logger.debug("Image for vm %s exists", self.uuid) | ||||||
|  |         else: | ||||||
|  |             if shared.storage_handler.make_vm_image(src=self.vm['image_uuid'], dest=self.uuid): | ||||||
|  |                 if not shared.storage_handler.resize_vm_image(path=self.uuid, | ||||||
|  |                                                               size=int(self.vm['specs']['os-ssd'].to_MB())): | ||||||
|  |                     self.vm['status'] = VMStatus.error | ||||||
|  |                 else: | ||||||
|  |                     logger.info("New VM Created") | ||||||
|  | 
 | ||||||
|  |     def sync(self): | ||||||
|  |         shared.etcd_client.put(self.key, self.schema.dump(self.vm), value_in_json=True) | ||||||
|  | 
 | ||||||
|  |     def delete(self): | ||||||
|  |         self.stop() | ||||||
|  | 
 | ||||||
|  |         if shared.storage_handler.is_vm_image_exists(self.uuid): | ||||||
|  |             r_status = shared.storage_handler.delete_vm_image(self.uuid) | ||||||
|  |             if r_status: | ||||||
|  |                 shared.etcd_client.client.delete(self.key) | ||||||
|  |         else: | ||||||
|  |             shared.etcd_client.client.delete(self.key) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def resolve_network(network_name, network_owner): | ||||||
|  |     network = shared.etcd_client.get( | ||||||
|  |         join_path(settings['etcd']['network_prefix'], network_owner, network_name), value_in_json=True | ||||||
|  |     ) | ||||||
|  |     return network | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def create_vxlan_br_tap(_id, _dev, tap_id, ip=None): | def create_vxlan_br_tap(_id, _dev, tap_id, ip=None): | ||||||
|     network_script_base = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'network') |     network_script_base = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'network') | ||||||
|  | @ -377,10 +222,12 @@ def update_radvd_conf(all_networks): | ||||||
|         ) |         ) | ||||||
|         for net in networks if networks.get(net) |         for net in networks if networks.get(net) | ||||||
|     ] |     ] | ||||||
| 
 |  | ||||||
|     with open('/etc/radvd.conf', 'w') as radvd_conf: |     with open('/etc/radvd.conf', 'w') as radvd_conf: | ||||||
|         radvd_conf.writelines(content) |         radvd_conf.writelines(content) | ||||||
|     try: |     try: | ||||||
|         sp.check_output(['systemctl', 'restart', 'radvd']) |         sp.check_output(['systemctl', 'restart', 'radvd']) | ||||||
|     except Exception: |     except sp.CalledProcessError: | ||||||
|  |         try: | ||||||
|             sp.check_output(['service', 'radvd', 'restart']) |             sp.check_output(['service', 'radvd', 'restart']) | ||||||
|  |         except sp.CalledProcessError as err: | ||||||
|  |             raise err.__class__('Cannot start/restart radvd service', err.cmd) from err | ||||||
|  |  | ||||||
|  | @ -95,7 +95,7 @@ def dead_host_mitigation(dead_hosts_keys): | ||||||
| 
 | 
 | ||||||
|         vms_hosted_on_dead_host = shared.vm_pool.by_host(host_key) |         vms_hosted_on_dead_host = shared.vm_pool.by_host(host_key) | ||||||
|         for vm in vms_hosted_on_dead_host: |         for vm in vms_hosted_on_dead_host: | ||||||
|             vm.declare_killed() |             vm.status = 'UNKNOWN' | ||||||
|             shared.vm_pool.put(vm) |             shared.vm_pool.put(vm) | ||||||
|         shared.host_pool.put(host) |         shared.host_pool.put(host) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -56,27 +56,6 @@ def main(): | ||||||
|                     continue |                     continue | ||||||
|                 shared.etcd_client.client.delete(request_entry.key)  # consume Request |                 shared.etcd_client.client.delete(request_entry.key)  # consume Request | ||||||
| 
 | 
 | ||||||
|                 # If the Request is about a VM which is labelled as "migration" |  | ||||||
|                 # and has a destination |  | ||||||
|                 if hasattr(request_entry, "migration") and request_entry.migration \ |  | ||||||
|                         and hasattr(request_entry, "destination") and request_entry.destination: |  | ||||||
|                     try: |  | ||||||
|                         get_suitable_host(vm_specs=vm_entry.specs, |  | ||||||
|                                           hosts=[shared.host_pool.get(request_entry.destination)]) |  | ||||||
|                     except NoSuitableHostFound: |  | ||||||
|                         logger.info("Requested destination host doesn't have enough capacity" |  | ||||||
|                                     "to hold %s" % vm_entry.uuid) |  | ||||||
|                     else: |  | ||||||
|                         r = RequestEntry.from_scratch(type=RequestType.InitVMMigration, |  | ||||||
|                                                       uuid=request_entry.uuid, |  | ||||||
|                                                       destination=request_entry.destination, |  | ||||||
|                                                       request_prefix=settings['etcd']['request_prefix']) |  | ||||||
|                         shared.request_pool.put(r) |  | ||||||
| 
 |  | ||||||
|                 # If the Request is about a VM that just want to get started/created |  | ||||||
|                 else: |  | ||||||
|                     # assign_host only returns None when we couldn't be able to assign |  | ||||||
|                     # a host to a VM because of resource constraints |  | ||||||
|                 try: |                 try: | ||||||
|                     assign_host(vm_entry) |                     assign_host(vm_entry) | ||||||
|                 except NoSuitableHostFound: |                 except NoSuitableHostFound: | ||||||
|  |  | ||||||
							
								
								
									
										181
									
								
								ucloud/vmm/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								ucloud/vmm/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,181 @@ | ||||||
|  | import os | ||||||
|  | import subprocess as sp | ||||||
|  | import logging | ||||||
|  | import socket | ||||||
|  | import json | ||||||
|  | import tempfile | ||||||
|  | import time | ||||||
|  | 
 | ||||||
|  | from contextlib import suppress | ||||||
|  | from multiprocessing import Process | ||||||
|  | from os.path import join as join_path | ||||||
|  | from os.path import isdir | ||||||
|  | 
 | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class VMQMPHandles: | ||||||
|  |     def __init__(self, path): | ||||||
|  |         self.path = path | ||||||
|  |         self.sock = socket.socket(socket.AF_UNIX) | ||||||
|  |         self.file = self.sock.makefile() | ||||||
|  | 
 | ||||||
|  |     def __enter__(self): | ||||||
|  |         self.sock.connect(self.path) | ||||||
|  | 
 | ||||||
|  |         # eat qmp greetings | ||||||
|  |         self.file.readline() | ||||||
|  | 
 | ||||||
|  |         # init qmp | ||||||
|  |         self.sock.sendall(b'{ "execute": "qmp_capabilities" }') | ||||||
|  |         self.file.readline() | ||||||
|  | 
 | ||||||
|  |         return self.sock, self.file | ||||||
|  | 
 | ||||||
|  |     def __exit__(self, exc_type, exc_val, exc_tb): | ||||||
|  |         self.file.close() | ||||||
|  |         self.sock.close() | ||||||
|  | 
 | ||||||
|  |         if exc_type: | ||||||
|  |             logger.error('Couldn\'t get handle for VM.', exc_type, exc_val, exc_tb) | ||||||
|  |             raise exc_type("Couldn't get handle for VM.") from exc_type | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TransferVM(Process): | ||||||
|  |     def __init__(self, src_uuid, dest_uuid, host, socket_dir): | ||||||
|  |         self.src_uuid = src_uuid | ||||||
|  |         self.dest_uuid = dest_uuid | ||||||
|  |         self.host = host | ||||||
|  |         self.src_sock_path = os.path.join(socket_dir, self.src_uuid) | ||||||
|  |         self.dest_sock_path = os.path.join(socket_dir, self.dest_uuid) | ||||||
|  | 
 | ||||||
|  |         super().__init__() | ||||||
|  | 
 | ||||||
|  |     def run(self): | ||||||
|  |         with suppress(FileNotFoundError): | ||||||
|  |             os.remove(self.src_sock_path) | ||||||
|  | 
 | ||||||
|  |         command = ['ssh', '-nNT', '-L', '{}:{}'.format(self.src_sock_path, self.dest_sock_path), | ||||||
|  |                    'root@{}'.format(self.host)] | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             p = sp.Popen(command) | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error('Couldn\' forward unix socks over ssh.', exc_info=e) | ||||||
|  |         else: | ||||||
|  |             time.sleep(2) | ||||||
|  |             vmm = VMM() | ||||||
|  |             logger.debug('Executing: ssh forwarding command: %s', command) | ||||||
|  |             vmm.execute_command(self.src_uuid, command='migrate', | ||||||
|  |                                 arguments={'uri': 'unix:{}'.format(self.src_sock_path)}) | ||||||
|  | 
 | ||||||
|  |             while p.poll() is None: | ||||||
|  |                 success, output = vmm.execute_command(self.src_uuid, command='query-migrate') | ||||||
|  |                 if success: | ||||||
|  |                     status = output['return']['status'] | ||||||
|  |                     if status != 'active': | ||||||
|  |                         print('Migration Status: ', status) | ||||||
|  |                         return | ||||||
|  |                     else: | ||||||
|  |                         print('Migration Status: ', status) | ||||||
|  |                 else: | ||||||
|  |                     return | ||||||
|  |                 time.sleep(0.2) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class VMM: | ||||||
|  |     # Virtual Machine Manager | ||||||
|  |     def __init__(self, qemu_path='/usr/bin/qemu-system-x86_64', | ||||||
|  |                  vmm_backend=os.path.expanduser('~/ucloud/vmm/')): | ||||||
|  |         self.qemu_path = qemu_path | ||||||
|  |         self.vmm_backend = vmm_backend | ||||||
|  |         self.socket_dir = os.path.join(self.vmm_backend, 'sock') | ||||||
|  | 
 | ||||||
|  |     def is_running(self, uuid): | ||||||
|  |         sock_path = os.path.join(self.vmm_backend, uuid) | ||||||
|  |         try: | ||||||
|  |             sock = socket.socket(socket.AF_UNIX) | ||||||
|  |             sock.connect(sock_path) | ||||||
|  |             recv = sock.recv(4096) | ||||||
|  |         except Exception as err: | ||||||
|  |             # unix sock doesn't exists or it is closed | ||||||
|  |             logger.info('VM %s sock either don\' exists or it is closed.', uuid, | ||||||
|  |                         'It mean VM is stopped.', exc_info=err) | ||||||
|  |         else: | ||||||
|  |             # if we receive greetings from qmp it mean VM is running | ||||||
|  |             if len(recv) > 0: | ||||||
|  |                 return True | ||||||
|  | 
 | ||||||
|  |         with suppress(FileNotFoundError): | ||||||
|  |             os.remove(sock_path) | ||||||
|  | 
 | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def start(self, *args, uuid, migration=False): | ||||||
|  |         # start --> sucess? | ||||||
|  |         migration_args = () | ||||||
|  |         if migration: | ||||||
|  |             migration_args = ('-incoming', 'unix:{}'.format(os.path.join(self.socket_dir, uuid))) | ||||||
|  | 
 | ||||||
|  |         if self.is_running(uuid): | ||||||
|  |             logger.warning('Cannot start VM. It is already running.') | ||||||
|  |         else: | ||||||
|  |             qmp_arg = ('-qmp', 'unix:{}/{},server,nowait'.format(self.vmm_backend, uuid)) | ||||||
|  |             vnc_arg = ('-vnc', 'unix:{}'.format(tempfile.NamedTemporaryFile().name)) | ||||||
|  | 
 | ||||||
|  |             command = [self.qemu_path, *args, *qmp_arg, *migration_args, *vnc_arg, '-daemonize'] | ||||||
|  |             try: | ||||||
|  |                 sp.check_output(command, stderr=sp.PIPE) | ||||||
|  |             except sp.CalledProcessError as err: | ||||||
|  |                 logger.exception('Error occurred while starting VM.\nDetail %s', err.stderr.decode('utf-8')) | ||||||
|  |             else: | ||||||
|  |                 time.sleep(2) | ||||||
|  | 
 | ||||||
|  |     def execute_command(self, uuid, command, **kwargs): | ||||||
|  |         # execute_command -> sucess?, output | ||||||
|  |         try: | ||||||
|  |             with VMQMPHandles(os.path.join(self.vmm_backend, uuid)) as (sock_handle, file_handle): | ||||||
|  |                 command_to_execute = { | ||||||
|  |                     'execute': command, | ||||||
|  |                     **kwargs | ||||||
|  |                 } | ||||||
|  |                 sock_handle.sendall(json.dumps(command_to_execute).encode('utf-8')) | ||||||
|  |                 output = file_handle.readline() | ||||||
|  |         except Exception as err: | ||||||
|  |             logger.exception('Error occurred while executing command and getting valid output from qmp') | ||||||
|  |         else: | ||||||
|  |             try: | ||||||
|  |                 output = json.loads(output) | ||||||
|  |             except: | ||||||
|  |                 logger.exception('QMP Output isn\'t valid JSON. %s', output) | ||||||
|  |             else: | ||||||
|  |                 return 'return' in output, output | ||||||
|  |         return False, None | ||||||
|  | 
 | ||||||
|  |     def stop(self, uuid): | ||||||
|  |         success, output = self.execute_command(command='quit', uuid=uuid) | ||||||
|  |         return success | ||||||
|  | 
 | ||||||
|  |     def get_status(self, uuid): | ||||||
|  |         success, output = self.execute_command(command='query-status', uuid=uuid) | ||||||
|  |         if success: | ||||||
|  |             return output['return']['status'] | ||||||
|  |         else: | ||||||
|  |             return 'STOPPED' | ||||||
|  | 
 | ||||||
|  |     def discover(self): | ||||||
|  |         vms = [ | ||||||
|  |             uuid for uuid in os.listdir(self.vmm_backend) | ||||||
|  |             if not isdir(join_path(self.vmm_backend, uuid)) | ||||||
|  |         ] | ||||||
|  |         return vms | ||||||
|  | 
 | ||||||
|  |     def get_vnc(self, uuid): | ||||||
|  |         success, output = self.execute_command(uuid, command='query-vnc') | ||||||
|  |         if success: | ||||||
|  |             return output['return']['service'] | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     def transfer(self, src_uuid, dest_uuid, host): | ||||||
|  |         p = TransferVM(src_uuid, dest_uuid, socket_dir=self.socket_dir, host=host) | ||||||
|  |         p.start() | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue