[docs]classMinimegaEmulatedEntity:""" This object adds a UUID to the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>`. """
[docs]def__init__(self):"""Adding a new UUID and casting it to a string."""self.uuid=str(uuid.uuid4())
[docs]@require_class(MinimegaEmulatedEntity)classMinimegaEmulatedVM:""" An object which adds methods to a :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` to collect all configuration options needed to launch the VM with minimega. Attributes: interface_defaults (dict): A dictionary containing default values for interfaces. This includes the type of interface and the required driver. drive_defaults (dict): A dictionary containing default values for VM drives. This includes the drive interface and cache type. cpu_defaults (dict): A dictionary containing default values for VM CPUs. This includes the model type, and number of sockets, cores, and threads. """interface_defaults={"type":"tap","driver":"virtio-net-pci"}drive_defaults={"interface":"virtio","cache":"writeback",}cpu_defaults={"model":"qemu64","sockets":1,"cores":1,"threads":1,}
[docs]def_generate_nic_configs(self,config):"""Create a finished configuration for each NIC. The finished config for a NIC resembles: .. code-block:: python { "name": "nic", "id": "unique", "type": "tap", "interface": "tap0", "driver": "virtio-net-pci", "bus": "pci.0", "bus_addr": "5", "mac": "00:00:00:00:00:00" } Where the fields are described as: * *name* - minimega device name. (e.g. ``"nic"``). * *id* - Some value unique within the scope of this VM. * *type* - Type of interface. Supported: ``"tap"``. This corresponds to both a QEMU type and a mapping to a minimega port object. * *interface* - Name of the tap interface on the underlying host system. This must meet the restrictions placed by Linux on the naming of network interfaces. * *bus* - PCI bus number. * *bus_addr* - PCI bus address. * *mac* - MAC address for this interface. Args: config (dict): The configuration for the VM. Raises: RuntimeError: If a required configuration key is missing from an interface. """config_list=[]try:forinterfaceinself.interfaces.interfaces:iface_conf={"name":"nic","id":f"if{len(config_list)}",}# Apply defaults.fordefault_key,valueinself.interface_defaults.items():try:interface[default_key]exceptKeyError:interface[default_key]=value# Finish building the config.iface_conf["switch_name"]=interface["switch"].namekeys=["type","driver","mac","qos"]forkinkeys:try:iface_conf[k]=interface[k]exceptKeyErrorasexc:self.log.error('Missing required key "%s" on VM "%s". Have: %s',k,self.name,interface,)raiseRuntimeError(f'Required key "{k}" not found on interface for VM "{self.name}".')fromexctry:iface_conf["ip"]=str(interface["network"])exceptKeyError:passconfig_list.append(iface_conf)exceptAttributeError:# There's nothing that says a VM must have NICs.passself.log.debug('VM "%s" generated %s NIC configs.',self.name,len(config_list))config["aux"]["nic"]=config_list
[docs]def_generate_drive_configs(self,config):"""Create a finished configuration for each drive. Note: The first disk is assumed to be the image for the VM. Args: config (dict): The configuration for the VM. Raises: RuntimeError: If a required configuration key is missing from an interface. Returns: list: A list of drives for the VM. """config_list=[]fordriveinself.vm["drives"]:conf={"name":"drive","id":f"drv{len(config_list)}","file":drive["file"],"path":os.path.join(self.vm["image_store"]["name"],drive["file"]),"db_path":drive["db_path"],}# Apply defaults.fordefault_key,valueinself.drive_defaults.items():try:drive[default_key]exceptKeyError:drive[default_key]=value# Finish building the configkeys=["interface","cache"]forkinkeys:try:conf[k]=drive[k]exceptKeyErrorasexc:raiseRuntimeError(f'Required key "{k}" not found on drive for VM "{self.name}".')fromexcconfig_list.append(conf)config["aux"]["disks"]=config_listself.log.debug('VM "%s" generated %s drive configs.',self.name,len(config_list))returnconfig_list
[docs]def_generate_vcpu_config(self,config):"""Create a finished configuration for the VMs vCPUs. Args: config (dict): The configuration for the VM. Returns: list: A list containing the ``cpu_config``, the ``smp_config``, and the modified VM config. """cpu_config={"name":"cpu","id":"cpu",}smp_config={"name":"smp","id":"smp",}try:cpu_config["model"]=self.vm["vcpu"]["model"]exceptKeyError:self.log.debug('Using default VCPU model ("%s") for VM "%s".',self.cpu_defaults["model"],self.name,)cpu_config["model"]=self.cpu_defaults["model"]if"vcpu"notinself.vm:self.vm["vcpu"]={}self.vm["vcpu"]["model"]=self.cpu_defaults["model"]config["vm"]["vcpu_model"]=self.vm["vcpu"]["model"]def__handle_smp_key(key,default_value):"""Set the value for a single SMP key on the VM if it has not been configured. Args: key (str): Which SMP value to change (e.g. "sockets", "cores", "threads", etc.) default_value (int): The value for the SMP key. """try:smp_config[key]=self.vm["vcpu"][key]exceptKeyError:self.log.debug('Using default value %d for SMP parameter "%s" on VM "%s".',default_value,key,self.name,)smp_config[key]=default_valueif"vcpu"notinself.vm:self.vm["vcpu"]={}self.vm["vcpu"][key]=default_value__handle_smp_key("sockets",self.cpu_defaults["sockets"])__handle_smp_key("cores",self.cpu_defaults["cores"])__handle_smp_key("threads",self.cpu_defaults["threads"])forsmp_key,smp_valinsmp_config.items():full_key=f"smp_{smp_key}"config["vm"][full_key]=smp_valreturn[cpu_config,smp_config,config]
[docs]def_generate_mem_config(self,config):"""Create a finished configuration for the VMs memory. Args: config (dict): The configuration for the VM. """try:memory=str(self.vm["mem"])exceptKeyError:memory="512"config["vm"]["memory"]=memory
[docs]def_generate_vga_config(self,config):"""Create a finished configuration for the VMs VGA display. Args: config (dict): The configuration for the VM. """default_vga_model="std"vga_config={"name":"vga","id":"minimegavga",}try:vga_config["model"]=self.vm["vga"]exceptKeyError:vga_config["model"]=default_vga_modelself.vm["vga"]=default_vga_modelconfig["vm"]["vga_model"]=self.vm["vga"]
[docs]def_generate_vm_resource_handler_communication_config(self,config,minimega_type):"""Create the finished configuration which will be used to enable communication between the :ref:`vm-resource-handler` and the VM. Args: config (dict): The configuration for the VM. minimega_type (str): The type of the VM. Currently, there is only one type ``QemuVM``. Returns: dict: The ``qga_config`` dictionary. """# First check that the VM needs resource handler communicationtry:ifnotself.vm_resource_schedule:return{}exceptAttributeError:return{}# Handle vm_resource communication devices based off VM typeifminimega_type=="QemuVM":qga_config={"name":"serial","id":"minimegaqga","path":os.path.join(fw_config["minimega"]["base_dir"],"namespaces",fw_config["minimega"]["namespace"],self.uuid,"virtio-serial0",),}self.log.debug("new qga path is %s",qga_config["path"])config["aux"]["qga_config"]=qga_configreturnqga_configreturn{}
[docs]def_generate_vm_resource_handler_process_config(self,config):""" Create a configuration for launching a :ref:`vm-resource-handler` process for each VM. This method assumes that FIREWHEEL has been installed in the same location on all :ref:`cluster-compute-nodes`. Args: config (dict): The configuration for the VM. Returns: dict: The configuration to launch a new :ref:`vm-resource-handler` process. """try:ifnotself.vm_resource_schedule:returnNoneexceptAttributeError:returnNoneprocess_config={"type":"Process","engine":config["vm"]["type"],"uuid":str(uuid.uuid4()),"vm_name":config["vm"]["name"],"vm_uuid":config["vm"]["uuid"],"binary_name":firewheel.vm_resource_manager.vm_resource_handler.__file__,}ifconfig["vm"]["type"]=="QemuVM":if"qga_config"inconfig["aux"]andconfig["aux"]["qga_config"]:process_config["path"]=config["aux"]["qga_config"]["path"]if"path"notinprocess_config:returnNoneconfig["aux"]["handler_process"]=process_configreturnprocess_config
[docs]def_generate_bios_config(self,config):"""Create a finished configuration for the VMs BIOS. Args: config (dict): The configuration for the VM. """bios=self.vm.get("bios",None)ifbios:config["aux"]["qemu_append"]["bios"]=os.path.join(self.vm["image_store"]["path"],bios)
[docs]def_generate_qemu_append_str(self,config):""" Some QEMU arguments are not natively supported by minimega's vm config function. To set those arguments, we can use the ``qemu_append`` option for the VM config. In this method, we take all of the ``qemu_append`` mappings and parse them into a string to be used by minimega. For example, BIOS can currently only be set in this way. In :py:meth:`_generate_bios_config <minimega.emulated_entities.MinimegaEmulatedVM._generate_bios_config>`, we grab the value for BIOS from ``self.vm`` and set it in the ``qemu_append`` dictionary. Then this parses: ``{"bios" : "seabios_rel-1.14.0.0"}`` into ``-bios seabios_rel-1.14.0.0``. Args: config (dict): The configuration for the VM. """append_str=" ".join([f"-{k}{v}"fork,vinconfig["aux"]["qemu_append"].items()])config["aux"]["qemu_append_str"]=append_str
[docs]defgenerate_minimega_config(self):"""Generate a minimega VM config based on this :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>`. Raises: KeyError: Every VM must define an architecture. Returns: dict: The minimega configuration dictionary with all required information. """config={}config["aux"]={}config["aux"]["qemu_append"]=self.vm.get("qemu_append",{})config["vm"]={}config["vm"]["uuid"]=self.uuidconfig["vm"]["type"]="QemuVM"config["coschedule"]=self.coscheduleself.log.debug('VM "%s" has UUID %s.',self.name,self.uuid)try:config["aux"]["control_ip"]=str(self.control_ip)exceptAttributeError:self.control_ip=""config["aux"]["control_ip"]=""self.log.debug('VM "%s" has control_ip %s.',self.name,config["aux"]["control_ip"])try:config["vm"]["architecture"]=self.vm["architecture"]exceptKeyError:self.log.critical("VM %s must define an architecture.",self.name)raiseconfig["vm"]["name"]=self.nameconfig["vm"]["image"]=self.vm["image"]if"initial_power_state"inself.vm:config["aux"]["power_state"]=self.vm["initial_power_state"]else:config["aux"]["power_state"]="running"self._generate_nic_configs(config)self._generate_bios_config(config)self._generate_drive_configs(config)self._generate_vcpu_config(config)self._generate_mem_config(config)self._generate_vga_config(config)self._generate_qemu_append_str(config)self._generate_vm_resource_handler_communication_config(config,config["vm"]["type"])try:config["aux"]["raw_device_configs"]=self.vm["raw_device_configs"]self.log.debug('Including raw device configs for VM "%s".',self.name)exceptKeyError:pass# We must have a QMP socket.default_qmp_id="minimegaqmp"qmp_dev={"name":"qmp","id":default_qmp_id,"path":os.path.join(fw_config["minimega"]["base_dir"],"namespaces",fw_config["minimega"]["namespace"],self.uuid,"qmp",),}config["aux"][default_qmp_id]=qmp_devself.log.debug("new qmp path is %s",qmp_dev["path"])try:config["tags"]=self.mm_tagsexceptAttributeError:config["tags"]={}ifconfig["aux"]["qga_config"]:self._generate_vm_resource_handler_process_config(config)returnconfig