From 618fecb73fe3bc77f43d567219a88f3c5cb19b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Tue, 28 Jan 2020 14:38:07 +0100 Subject: [PATCH 1/2] Initial implementation (no networking) of uncloud-oneshot --- scripts/uncloud | 1 + uncloud/oneshot/__init__.py | 3 ++ uncloud/oneshot/main.py | 65 ++++++++++++++++++++++++++++ uncloud/oneshot/virtualmachine.py | 70 +++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 uncloud/oneshot/__init__.py create mode 100644 uncloud/oneshot/main.py create mode 100644 uncloud/oneshot/virtualmachine.py diff --git a/scripts/uncloud b/scripts/uncloud index d565954..7d38e42 100755 --- a/scripts/uncloud +++ b/scripts/uncloud @@ -16,6 +16,7 @@ ETCD_COMPONENTS = ['api', 'scheduler', 'host', 'filescanner', 'imagescanner', 'metadata', 'configure', 'hack'] ALL_COMPONENTS = ETCD_COMPONENTS.copy() +ALL_COMPONENTS.append('oneshot') #ALL_COMPONENTS.append('cli') diff --git a/uncloud/oneshot/__init__.py b/uncloud/oneshot/__init__.py new file mode 100644 index 0000000..eea436a --- /dev/null +++ b/uncloud/oneshot/__init__.py @@ -0,0 +1,3 @@ +import logging + +logger = logging.getLogger(__name__) diff --git a/uncloud/oneshot/main.py b/uncloud/oneshot/main.py new file mode 100644 index 0000000..20f22e4 --- /dev/null +++ b/uncloud/oneshot/main.py @@ -0,0 +1,65 @@ +import argparse +import os + +from pathlib import Path +from uncloud.vmm import VMM + +from . import virtualmachine, logger + +arg_parser = argparse.ArgumentParser('oneshot', add_help=False) +arg_parser.add_argument('--workdir', default=Path.home()) +arg_parser.add_argument('--list-vms', action='store_true') +arg_parser.add_argument('--start-vm', action='store_true') +arg_parser.add_argument('--stop-vm', action='store_true') +arg_parser.add_argument('--name') +arg_parser.add_argument('--image') +arg_parser.add_argument('--uuid') +arg_parser.add_argument('--mac') +arg_parser.add_argument('--get_vm_status', action='store_true') +arg_parser.add_argument('--setup-network') + +def setup_network(): + print("Not implemented yet.") + exit(1) + +def require_with(arguments, required, mode): + if not arguments[required]: + print("--{} is required with the {} flag. Exiting.".format(required, mode)) + exit(1) + +def main(arguments): + # Initialize VMM + workdir = arguments['workdir'] + vmm = VMM(vmm_backend=workdir) + + # Initialize workdir directory. + # TODO: copy ifup, ifdown. + + # Build VM configuration. + vm_config = {} + for spec in ['uuid', 'memory', 'cores', 'threads', 'image', 'image_format', 'name']: + if arguments.get(spec): + vm_config[spec] = arguments[spec] + + # Execute requested VM action. + vm = virtualmachine.VM(vmm, vm_config) + if arguments['setup_network']: + setup_network() + elif arguments['start_vm']: + require_with(arguments, 'image', 'start_vm') + vm.start() + logger.info("Created VM {}".format(vm.get_uuid)) + elif arguments['get_vm_status']: + require_with(arguments, 'uuid', 'get_vm_status') + print("VM: {} {} {}".format(vm.get_uuid(), vm.get_name(), vm.get_status())) + elif arguments['stop_vm']: + require_with(arguments, 'uuid', 'stop_vm') + vm.stop() + elif arguments['list_vms']: + discovered = vmm.discover() + print("Found {} VMs.".format(len(discovered))) + for uuid in vmm.discover(): + vmi = virtualmachine.VM(vmm, {'uuid': uuid}) + print("VM: {} {} {}".format(vmi.get_uuid, vmi.get_name, vmi.get_status)) + else: + print('No action requested. Exiting.') diff --git a/uncloud/oneshot/virtualmachine.py b/uncloud/oneshot/virtualmachine.py new file mode 100644 index 0000000..47365d5 --- /dev/null +++ b/uncloud/oneshot/virtualmachine.py @@ -0,0 +1,70 @@ +import uuid +import os + +from uncloud.oneshot import logger + +class VM(object): + def __init__(self, vmm, config): + self.config = config + self.vmm = vmm + + # Extract VM specs/metadata from configuration. + self.name = config.get('name') + self.memory = config.get('memory', 1024) + self.cores = config.get('cores', 1) + self.threads = config.get('threads', 1) + self.image_format = config.get('image_format', 'qcow2') + self.image = config.get('image') + self.uuid = config.get('uuid', uuid.uuid4()) + self.mac = config.get('mac', 'spuik') + + # Harcoded & generated values. + self.image_format='qcow2' + self.accel = 'kvm' + + def get_qemu_args(self): + command = ( + "-uuid {uuid} -name {name}" + " -drive file={image},format={image_format},if=virtio" + " -device virtio-rng-pci" + " -m {memory} -smp cores={cores},threads={threads}" + ).format( + uuid=self.uuid, name=self.name, + image=self.image, image_format=self.image_format, + memory=self.memory, cores=self.cores, threads=self.threads, + ) + + return command.split(" ") + + def start(self): + # Check that VM image is available. + if not os.path.isfile(self.image): + logger.error("Image {} does not exist. Aborting.".format(self.image)) + + # Generate config for and run QEMU. + qemu_args = self.get_qemu_args() + logger.warning("QEMU args for VM {}: {}".format(self.uuid, qemu_args)) + self.vmm.start( + uuid=self.uuid, + migration=False, + *qemu_args + ) + + def stop(self): + self.vmm.stop(self.uuid) + + def get_status(self): + return self.vmm.get_status(self.uuid) + + def get_vnc_addr(self): + return self.vmm.get_vnc(self.uuid) + + def get_uuid(self): + return self.uuid + + def get_name(self): + success, json = self.vmm.execute_command(uuid, 'query-name') + if success: + return json['return']['name'] + + return None From 3e69fb275fb152cc842582e3a173cdbea8e2e155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Tue, 28 Jan 2020 17:44:53 +0100 Subject: [PATCH 2/2] Oneshot: cleanup CLI, initial networking support --- uncloud/oneshot/main.py | 140 +++++++++++++++++++++--------- uncloud/oneshot/virtualmachine.py | 27 ++++-- 2 files changed, 119 insertions(+), 48 deletions(-) diff --git a/uncloud/oneshot/main.py b/uncloud/oneshot/main.py index 20f22e4..0e94a81 100644 --- a/uncloud/oneshot/main.py +++ b/uncloud/oneshot/main.py @@ -1,65 +1,123 @@ import argparse import os + from pathlib import Path from uncloud.vmm import VMM +from uncloud.host.virtualmachine import update_radvd_conf, create_vxlan_br_tap from . import virtualmachine, logger +### +# Argument parser loaded by scripts/uncloud. arg_parser = argparse.ArgumentParser('oneshot', add_help=False) -arg_parser.add_argument('--workdir', default=Path.home()) -arg_parser.add_argument('--list-vms', action='store_true') -arg_parser.add_argument('--start-vm', action='store_true') -arg_parser.add_argument('--stop-vm', action='store_true') -arg_parser.add_argument('--name') -arg_parser.add_argument('--image') -arg_parser.add_argument('--uuid') -arg_parser.add_argument('--mac') -arg_parser.add_argument('--get_vm_status', action='store_true') -arg_parser.add_argument('--setup-network') -def setup_network(): - print("Not implemented yet.") - exit(1) +# Actions. +arg_parser.add_argument('--list', action='store_true', + help='list UUID and name of running VMs') +arg_parser.add_argument('--start', nargs=3, + metavar=('IMAGE', 'UPSTREAM_INTERFACE', 'NETWORK'), + help='start a VM using the OS IMAGE (full path), configuring networking on NETWORK IPv6 prefix') +arg_parser.add_argument('--stop', metavar='UUID', + help='stop a VM') +arg_parser.add_argument('--get-status', metavar='UUID', + help='return the status of the VM') +arg_parser.add_argument('--get-vnc', metavar='UUID', + help='return the path of the VNC socket of the VM') +arg_parser.add_argument('--reconfigure-radvd', metavar='NETWORK', + help='regenerate and reload RADVD configuration for NETWORK IPv6 prefix') -def require_with(arguments, required, mode): - if not arguments[required]: - print("--{} is required with the {} flag. Exiting.".format(required, mode)) - exit(1) +# Arguments. +arg_parser.add_argument('--workdir', default=Path.home(), + help='Working directory, defaulting to $HOME') +arg_parser.add_argument('--mac', + help='MAC address of the VM to create (--start)') +arg_parser.add_argument('--memory', type=int, + help='Memory (MB) to allocate (--start)') +arg_parser.add_argument('--cores', type=int, + help='Number of cores to allocate (--start)') +arg_parser.add_argument('--threads', type=int, + help='Number of threads to allocate (--start)') +arg_parser.add_argument('--image-format', choices=['raw', 'qcow2'], + help='Format of OS image (--start)') +arg_parser.add_argument('--accel', choices=['kvm', 'tcg'], default='tcg', + help='QEMU acceleration to use (--start)') +arg_parser.add_argument('--upstream-interface', default='eth0', + help='Name of upstream interface (--start)') + +### +# Helpers. + +# XXX: check if it is possible to use the type returned by ETCD queries. +class UncloudEntryWrapper: + def __init__(self, value): + self.value = value + + def value(self): + return self.value + +def status_line(vm): + return "VM: {} {} {}".format(vm.get_uuid(), vm.get_name(), vm.get_status()) + +### +# Entrypoint. def main(arguments): - # Initialize VMM + # Initialize VMM. workdir = arguments['workdir'] vmm = VMM(vmm_backend=workdir) - # Initialize workdir directory. - # TODO: copy ifup, ifdown. + # Harcoded debug values. + net_id = 0 # Build VM configuration. vm_config = {} - for spec in ['uuid', 'memory', 'cores', 'threads', 'image', 'image_format', 'name']: - if arguments.get(spec): - vm_config[spec] = arguments[spec] + vm_options = [ + 'mac', 'memory', 'cores', 'threads', 'image', 'image_format', + '--upstream_interface', 'upstream_interface', 'network' + ] + for option in vm_options: + if arguments.get(option): + vm_config[option] = arguments[option] + + vm_config['net_id'] = net_id # Execute requested VM action. - vm = virtualmachine.VM(vmm, vm_config) - if arguments['setup_network']: - setup_network() - elif arguments['start_vm']: - require_with(arguments, 'image', 'start_vm') + if arguments['reconfigure_radvd']: + # TODO: check that RADVD is available. + prefix = arguments['reconfigure_radvd'] + network = UncloudEntryWrapper({ + 'id': net_id, + 'ipv6': prefix + }) + + # Make use of uncloud.host.virtualmachine for network configuration. + update_radvd_conf([network]) + elif arguments['start']: + # Extract from --start positional arguments. Quite fragile. + vm_config['image'] = arguments['start'][0] + vm_config['network'] = arguments['start'][1] + vm_config['upstream_interface'] = arguments['start'][2] + + vm_config['tap_interface'] = "uc{}".format(len(vmm.discover())) + vm = virtualmachine.VM(vmm, vm_config) vm.start() - logger.info("Created VM {}".format(vm.get_uuid)) - elif arguments['get_vm_status']: - require_with(arguments, 'uuid', 'get_vm_status') - print("VM: {} {} {}".format(vm.get_uuid(), vm.get_name(), vm.get_status())) - elif arguments['stop_vm']: - require_with(arguments, 'uuid', 'stop_vm') + elif arguments['stop']: + vm = virtualmachine.VM(vmm, {'uuid': arguments['stop']}) + vm = virtualmachine.VM(vmm, vm_config) vm.stop() - elif arguments['list_vms']: - discovered = vmm.discover() - print("Found {} VMs.".format(len(discovered))) - for uuid in vmm.discover(): - vmi = virtualmachine.VM(vmm, {'uuid': uuid}) - print("VM: {} {} {}".format(vmi.get_uuid, vmi.get_name, vmi.get_status)) + elif arguments['get_status']: + vm = virtualmachine.VM(vmm, {'uuid': arguments['get_status']}) + print(status_line(vm)) + elif arguments['get_vnc']: + vm = virtualmachine.VM(vmm, {'uuid': arguments['get_vnc']}) + print(vm.get_vnc_addr()) + elif arguments['list']: + vms = vmm.discover() + print("Found {} VMs.".format(len(vms))) + for uuid in vms: + vm = virtualmachine.VM(vmm, {'uuid': uuid}) + print(status_line(vm)) else: - print('No action requested. Exiting.') + print('Please specify an action: --start, --stop, --list,\ +--get-status, --get-vnc, --reconfigure-radvd') diff --git a/uncloud/oneshot/virtualmachine.py b/uncloud/oneshot/virtualmachine.py index 47365d5..1388d49 100644 --- a/uncloud/oneshot/virtualmachine.py +++ b/uncloud/oneshot/virtualmachine.py @@ -1,6 +1,7 @@ import uuid import os +from uncloud.host.virtualmachine import create_vxlan_br_tap from uncloud.oneshot import logger class VM(object): @@ -9,29 +10,36 @@ class VM(object): self.vmm = vmm # Extract VM specs/metadata from configuration. - self.name = config.get('name') + self.name = config.get('name', 'no-name') self.memory = config.get('memory', 1024) self.cores = config.get('cores', 1) self.threads = config.get('threads', 1) self.image_format = config.get('image_format', 'qcow2') self.image = config.get('image') - self.uuid = config.get('uuid', uuid.uuid4()) - self.mac = config.get('mac', 'spuik') + self.uuid = config.get('uuid', str(uuid.uuid4())) + self.mac = config.get('mac') + + self.net_id = config.get('net_id', 0) + self.upstream_interface = config.get('upstream_interface', 'eth0') + self.tap_interface = config.get('tap_interface', 'uc0') + self.network = config.get('network') # Harcoded & generated values. - self.image_format='qcow2' self.accel = 'kvm' def get_qemu_args(self): command = ( - "-uuid {uuid} -name {name}" + "-uuid {uuid} -name {name} -machine pc,accel={accel}" " -drive file={image},format={image_format},if=virtio" " -device virtio-rng-pci" " -m {memory} -smp cores={cores},threads={threads}" + " -netdev tap,id=vmnet{net_id},ifname={tap},script=no,downscript=no" + " -device virtio-net-pci,netdev=vmnet{net_id},mac={mac}" ).format( - uuid=self.uuid, name=self.name, + uuid=self.uuid, name=self.name, accel=self.accel, image=self.image, image_format=self.image_format, memory=self.memory, cores=self.cores, threads=self.threads, + net_id=self.net_id, tap=self.tap_interface, mac=self.mac ) return command.split(" ") @@ -41,9 +49,14 @@ class VM(object): if not os.path.isfile(self.image): logger.error("Image {} does not exist. Aborting.".format(self.image)) + # Create Bridge, VXLAN and tap interface for VM. + create_vxlan_br_tap( + self.net_id, self.upstream_interface, self.tap_interface, self.network + ) + # Generate config for and run QEMU. qemu_args = self.get_qemu_args() - logger.warning("QEMU args for VM {}: {}".format(self.uuid, qemu_args)) + logger.debug("QEMU args for VM {}: {}".format(self.uuid, qemu_args)) self.vmm.start( uuid=self.uuid, migration=False,