diff --git a/nova/conf/libvirt.py b/nova/conf/libvirt.py index 9d59beed833..cec163100d1 100644 --- a/nova/conf/libvirt.py +++ b/nova/conf/libvirt.py @@ -104,7 +104,7 @@ """), cfg.StrOpt('virt_type', default='kvm', - choices=('kvm', 'lxc', 'qemu', 'parallels'), + choices=('kvm', 'lxc', 'qemu', 'parallels', 'ch'), help=""" Describes the virtualization type (or so called domain type) libvirt should use. diff --git a/nova/privsep/libvirt.py b/nova/privsep/libvirt.py index 6ca99b98b42..08618eab99a 100644 --- a/nova/privsep/libvirt.py +++ b/nova/privsep/libvirt.py @@ -146,25 +146,62 @@ def unplug_plumgrid_vif(dev): processutils.execute('ifc_ctl', 'gateway', 'ifdown', dev) processutils.execute('ifc_ctl', 'gateway', 'del_port', dev) +def readpty_once(path): + import select + + data = bytearray() + epoll = select.epoll() + + with open(path, "w") as f: + # Writing a null seems to trigger new output while not being + # recognized as a newline or similar by the sender. + f.write("\0") + f.flush() + + with open(path, "rb") as f: + os.set_blocking(f.fileno(), False) + epoll.register(f.fileno(), select.EPOLLIN) + poll_list = epoll.poll(0.1) + for _ in poll_list: + data += f.read() + epoll.unregister(f.fileno()) + epoll.close() + + return data.decode("utf-8", errors="ignore") + @nova.privsep.sys_admin_pctxt.entrypoint def readpty(path): - # TODO(mikal): I'm not a huge fan that we don't enforce a valid pty path - # here, but I haven't come up with a great way of doing that. + """ + The pty created by Cloud Hypervisor is a bit tricky. It seems to require + some kind of input to deliver output again. Therefore, we send in a '\0' + byte before reading (the null byte does not seem to trigger any + interactions with the pty like a newline would do). + The reading is done in a loop and we cancel reading when no data is + returned anymore. Doing so has proven to deliver all the output available + in the pty. + """ + # We track how many times we read zero bytes form the pty + consecutive_zero = 0 + data = "" + try: + # We finish reading once we read zero multiple times continuously. The range + # is just a safeguard to finish reading if the terminal happens to have new + # data all the time. + for _ in range(1000): + out = readpty_once(path) - # NOTE(mikal): I am deliberately not catching the ImportError - # exception here... Some platforms (I'm looking at you Windows) - # don't have a fcntl and we may as well let them know that - # with an ImportError, not that they should be calling this at all. - import fcntl + if len(out) == 0: + consecutive_zero += 1 + else: + consecutive_zero = 0 - try: - with open(path, 'r') as f: - current_flags = fcntl.fcntl(f.fileno(), fcntl.F_GETFL) - fcntl.fcntl(f.fileno(), fcntl.F_SETFL, - current_flags | os.O_NONBLOCK) + data += out + + if consecutive_zero > 10: + return data - return f.read() + return data except Exception as exc: # NOTE(mikal): dear internet, I see you looking at me with your diff --git a/nova/virt/libvirt/blockinfo.py b/nova/virt/libvirt/blockinfo.py index 4efc6fbaeb1..124aa1b7e5c 100644 --- a/nova/virt/libvirt/blockinfo.py +++ b/nova/virt/libvirt/blockinfo.py @@ -95,6 +95,7 @@ 'qemu': ['virtio', 'scsi', 'ide', 'usb', 'fdc', 'sata'], 'kvm': ['virtio', 'scsi', 'ide', 'usb', 'fdc', 'sata'], 'lxc': ['lxc'], + 'ch': ['virtio'], 'parallels': ['ide', 'scsi'], # we no longer support UML or Xen, but we keep track of their bus types so # we can reject them for other virt types @@ -276,6 +277,8 @@ def get_disk_bus_for_device_type(instance, return "ide" elif device_type == "disk": return "scsi" + elif virt_type == "ch": + return "virtio" else: # If virt-type not in list then it is unsupported raise exception.UnsupportedVirtType(virt=virt_type) diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 71dca0410ac..43d26c9a21e 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -1025,7 +1025,7 @@ def _check_cpu_compatibility(self): mode = CONF.libvirt.cpu_mode models = CONF.libvirt.cpu_models - if (CONF.libvirt.virt_type not in ("kvm", "qemu") and + if (CONF.libvirt.virt_type not in ("kvm", "qemu", "ch") and mode not in (None, 'none')): msg = _("Config requested an explicit CPU model, but " "the current libvirt hypervisor '%s' does not " @@ -1379,6 +1379,8 @@ def _uri(): uri = CONF.libvirt.connection_uri or 'lxc:///' elif CONF.libvirt.virt_type == 'parallels': uri = CONF.libvirt.connection_uri or 'parallels:///system' + elif CONF.libvirt.virt_type == 'ch': + uri = CONF.libvirt.connection_uri or 'ch:///system' else: uri = CONF.libvirt.connection_uri or 'qemu:///system' return uri @@ -1389,6 +1391,7 @@ def _live_migration_uri(dest): 'kvm': 'qemu+%(scheme)s://%(dest)s/system', 'qemu': 'qemu+%(scheme)s://%(dest)s/system', 'parallels': 'parallels+tcp://%(dest)s/system', + 'ch': 'ch+%(scheme)s://%(dest)s/system', } dest = oslo_netutils.escape_ipv6(dest) @@ -4494,7 +4497,8 @@ def get_console_output(self, context, instance): path_sources = [ ('file', "./devices/console[@type='file']/source[@path]", 'path'), ('tcp', "./devices/console[@type='tcp']/log[@file]", 'file'), - ('pty', "./devices/console[@type='pty']/source[@path]", 'path')] + ('pty', "./devices/console[@type='pty']/source[@path]", 'path'), + ('pty', "./devices/serial[@type='pty']/source[@path]", 'path'), ] console_type = "" console_path = "" for c_type, epath, attrib in path_sources: @@ -5405,6 +5409,10 @@ def _get_guest_cpu_model_config(self, flavor=None, arch=None): if not models: models = ['max'] + elif CONF.libvirt.virt_type == "ch": + # Currently, we only support host-passthrough with Cloud Hypervisor. This + # changes as soon as we support proper CPU profiles. + mode = "host-passthrough" else: if mode is None or mode == "none": return None @@ -6735,6 +6743,9 @@ def _configure_guest_by_virt_type( guest.os_init_path = "/sbin/init" guest.os_cmdline = CONSOLE guest.os_init_env["product_name"] = "OpenStack Nova" + elif CONF.libvirt.virt_type == "ch": + guest.virt_type = 'kvm' + guest.os_kernel = "/usr/share/cloud-hypervisor/CLOUDHV_EFI.fd" elif CONF.libvirt.virt_type == "parallels": if guest.os_type == fields.VMMode.EXE: guest.os_init_path = "/sbin/init" @@ -6780,6 +6791,11 @@ def _create_consoles(self, guest_cfg, instance, flavor, image_meta): self._create_pty_device( guest_cfg, vconfig.LibvirtConfigGuestConsole, log_path=log_path) + elif CONF.libvirt.virt_type == "ch": + consolepty = vconfig.LibvirtConfigGuestSerial() + consolepty.type = "pty" + guest_cfg.add_device(consolepty) + else: # qemu, kvm if self._is_s390x_guest(image_meta): self._create_consoles_s390x( @@ -6969,6 +6985,8 @@ def _guest_add_usb_root_controller(self, guest, image_meta): here explicitly so that we can _disable_ it (by setting the model to 'none') if it's not necessary. """ + if CONF.libvirt.virt_type == "ch": + return usbhost = vconfig.LibvirtConfigGuestUSBHostController() usbhost.index = 0 # an unset model means autodetect, while 'none' means don't add a @@ -7378,6 +7396,8 @@ def _guest_add_accel_pci_devices(self, guest, accel_info): def _guest_add_video_device(guest): if CONF.libvirt.virt_type == 'lxc': return False + elif CONF.libvirt.virt_type == "ch": + return False # NB some versions of libvirt support both SPICE and VNC # at the same time. We're not trying to second guess which @@ -8678,7 +8698,8 @@ def _has_numa_support(self): if (caps.host.cpu.arch in (fields.Architecture.I686, fields.Architecture.X86_64, fields.Architecture.AARCH64) and - self._host.has_min_version(hv_type=host.HV_DRIVER_QEMU)): + (self._host.has_min_version(hv_type=host.HV_DRIVER_QEMU) or + self._host.has_min_version(hv_type=host.HV_DRIVER_CH))): return True elif (caps.host.cpu.arch in (fields.Architecture.PPC64, fields.Architecture.PPC64LE)): diff --git a/nova/virt/libvirt/host.py b/nova/virt/libvirt/host.py index f67ccf9bbf7..3599b6ff78d 100644 --- a/nova/virt/libvirt/host.py +++ b/nova/virt/libvirt/host.py @@ -85,6 +85,7 @@ # This list is for libvirt hypervisor drivers that need special handling. # This is *not* the complete list of supported hypervisor drivers. HV_DRIVER_QEMU = "QEMU" +HV_DRIVER_CH = "CH" SEV_KERNEL_PARAM_FILE = '/sys/module/kvm_amd/parameters/sev' @@ -1241,6 +1242,7 @@ def write_instance_config(self, xml): :returns: an instance of Guest """ + LOG.info(xml) domain = self.get_connection().defineXML(xml) return libvirt_guest.Guest(domain) diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py index 6e9069fa50f..30456e678d2 100644 --- a/nova/virt/libvirt/vif.py +++ b/nova/virt/libvirt/vif.py @@ -78,6 +78,9 @@ network_model.VIF_MODEL_RTL8139, network_model.VIF_MODEL_E1000, ], + 'ch': [ + network_model.VIF_MODEL_VIRTIO, + ], } @@ -172,7 +175,7 @@ def get_vif_model(self, image_meta=None, vif_model=None): # If the virt type is KVM/QEMU/VZ(Parallels), then use virtio according # to the global config parameter if (model is None and CONF.libvirt.virt_type in - ('kvm', 'qemu', 'parallels') and + ('kvm', 'qemu', 'parallels', 'ch') and CONF.libvirt.use_virtio_for_bridges): model = network_model.VIF_MODEL_VIRTIO