Source code for base_objects

import os
import sys
import math
import pickle
import pprint

import netaddr
from rich.console import Console

from firewheel.control.experiment_graph import Edge
from firewheel.vm_resource_manager.schedule_entry import ScheduleEntry


[docs] class AbstractWindowsEndpoint: """ This class is used to identify the generic OS for a :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` which can be useful for knowing what kinds of VM Resources (VMRs) can run on the system. Decoration with this object is mutually exclusive from being decorated with :py:class:`base_objects.AbstractUnixEndpoint`. """
[docs] def __init__(self): """Check for possible conflicts. Raises: TypeError: If the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` is already decorated with :py:class:`base_objects.AbstractUnixEndpoint`. """ if self.is_decorated_by(AbstractUnixEndpoint): raise TypeError( "AbstractUnixEndpoint cannot be decorated with AbstractWindowsEndpoint!" )
[docs] class AbstractUnixEndpoint: """ This class is used to identify the generic operating system (OS) for a :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` which can be useful for knowing what kinds of VMRs can run on the system. Decoration with this object is mutually exclusive from being decorated with :py:class:`base_objects.AbstractWindowsEndpoint`. """
[docs] def __init__(self): """Check for possible conflicts. Raises: TypeError: If the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` is already decorated with :py:class:`base_objects.AbstractWindowsEndpoint`. """ if self.is_decorated_by(AbstractWindowsEndpoint): raise TypeError( "AbstractWindowsEndpoint cannot be decorated with AbstractUnixEndpoint!" )
[docs] class AbstractServerEndpoint: """ This class is used to identify the generic type ``{server, desktop}`` for a :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` which can be useful for knowing what kinds of VMRs can run on the system. Decoration with this object is mutually exclusive from being decorated with :py:class:`base_objects.AbstractDesktopEndpoint`. """
[docs] def __init__(self): """Check for possible conflicts. Raises: TypeError: If the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` is already decorated with :py:class:`base_objects.AbstractDesktopEndpoint`. """ if self.is_decorated_by(AbstractDesktopEndpoint): raise TypeError( "AbstractDesktopEndpoint cannot be decorated with AbstractServerEndpoint!" )
[docs] class AbstractDesktopEndpoint: """ This class is used to identify the generic type ``{server, desktop}`` for a :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` which can be useful for knowing what kinds of VMRs can run on the system. Decoration with this object is mutually exclusive from being decorated with :py:class:`base_objects.AbstractServerEndpoint`. """
[docs] def __init__(self): """Check for possible conflicts. Raises: TypeError: If the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` is already decorated with :py:class:`base_objects.AbstractServerEndpoint`. """ if self.is_decorated_by(AbstractServerEndpoint): raise TypeError( "AbstractServerEndpoint cannot be decorated with AbstractDesktopEndpoint!" )
[docs] class FalseEdge: """This class is intended to indicate that an :py:class:`Edge <firewheel.control.experiment_graph.Edge>` is a "false" :py:class:`Edge <firewheel.control.experiment_graph.Edge>` in the graph. That is, the :py:class:`Edge <firewheel.control.experiment_graph.Edge>` exists in the graph but won't be used when the graph is instantiated. This is useful when using complex graph algorithms. """
[docs] def __init__(self): """Creates ``self.false`` and sets it to :py:data:`True`.""" self.false = True
[docs] class QoSEdge: """ This class can be used to add quality of service (QoS) attributes to a given :py:class:`Edge <firewheel.control.experiment_graph.Edge>`. It is important to note that these QoS constraints are only applied directionally on the egress side (e.g. transmitting) due to a limitation in `minimega <https://sandia-minimega.github.io/#header_5.47>`_. """
[docs] def __init__(self): """ Initialize the ``qos`` property for the :py:class:`Edge <firewheel.control.experiment_graph.Edge>`. """ self.qos = {}
[docs] def add_delay(self, delay): """ Set the :py:class:`Edge's <firewheel.control.experiment_graph.Edge>` egress delay (e.g. latency). Note: For emulation-based models, due to limitations of `tc <https://linux.die.net/man/8/tc>`_ you can only add rate OR loss/delay to a VM. Enabling loss or delay will disable rate and vice versa. Args: delay (str): The amount of egress delay to add for the link. This should be formatted like ``<delay><unit of delay>``. For example, ``100ms``. """ self.qos["delay"] = delay
[docs] def add_rate_limit(self, rate, unit=None): """ Set the :py:class:`Edge's <firewheel.control.experiment_graph.Edge>` egress rate (e.g. bandwidth). The rate is set as a multiple of bits **not** bytes. That is, a rate of ``1 kbit`` would equal 1000 bits, not 1000 bytes. For bytes, multiply the rate by 8 (e.g. 64 KBytes = 8 * 64 = 512 kbit). Note: For emulation-based models, due to limitations of `tc <https://linux.die.net/man/8/tc>`_ you can only add rate OR loss/delay to a VM. Enabling loss or delay will disable rate and vice versa. Args: rate (int): The requested maximum bandwidth as a multiple of bits. unit (str): The bandwidth unit (one of ``{"kbit", "mbit", "gbit"}``). Defaults to ``"mbit"``. Raises: TypeError: If the passed in unit is invalid. """ unit_types = ["kbit", "mbit", "gbit"] if unit is None: unit = "mbit" elif unit not in unit_types: raise TypeError(f"Invalid rate type. Expected one of: {unit_types}") self.qos["rate"] = (rate, unit)
[docs] def add_packet_loss_percent(self, packet_loss): """ Set the :py:class:`Edge's <firewheel.control.experiment_graph.Edge>` amount of egress packet loss (as a percentage). Note: For emulation-based models, due to limitations of `tc <https://linux.die.net/man/8/tc>`_ you can only add rate OR loss/delay to a VM. Enabling loss or delay will disable rate and vice versa. Args: packet_loss (int): The packet loss as a percentage. For example, ``packet_loss = 25`` is 25% packet loss. """ self.qos["loss"] = packet_loss
[docs] class Switch: """Decorate a :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` as a Switch. Switches represent a `Layer 2 <https://en.wikipedia.org/wiki/Data_link_layer>`_ network. VMs will appear like they are connected directly to a `network switch <https://en.wikipedia.org/wiki/Network_switch>`_. In order to connect two (or more) VMs, users must first create a switch to facilitate connection. Switches will not appear as VMs, but rather as Open vSwitch bridges. To use a physical VM as a "switch" users should refer to :ref:`layer2.ovs_mc` or :ref:`layer2.tap_mc`. """
[docs] def __init__(self, name=None): """ Initialize the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` as ``type=switch``. Args: name (str, optional): The name of the switch. Defaults to :py:data:`None`. This is typically set at the time of :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` creation. Raises: NameError: If the switch doesn't have a name. """ self.type = "switch" self.name = getattr(self, "name", name) if self.name is None: raise NameError("Name must be specified for switch!")
[docs] class VmResourceSchedule: """This object defines a VM resource schedule which will be used to keep track of all scheduled VM resources for a given VM. It provides methods to add new :py:class:`Schedule entries <firewheel.vm_resource_manager.schedule_entry.ScheduleEntry>`, retrieve the :ref:`vm-resource-schedule`, and serialize the schedule for use by the :ref:`vm-resource-handler`. """
[docs] def __init__(self): """ Create a new list to store :py:class:`schedule entries <firewheel.vm_resource_manager.schedule_entry.ScheduleEntry>`. """ self.schedule_list = []
[docs] def add_vm_resource(self, new_entry): """Add a new :py:class:`ScheduleEntry <firewheel.vm_resource_manager.schedule_entry.ScheduleEntry>` to the list. Args: new_entry (firewheel.vm_resource_manager.schedule_entry.ScheduleEntry): The new VMR schedule entry. This type can also be any sub class of :py:class:`firewheel.vm_resource_manager.schedule_entry.ScheduleEntry`. Raises: ValueError: If ``new_entry`` is not a subclass of :py:class:`firewheel.vm_resource_manager.schedule_entry.ScheduleEntry`. """ if not issubclass(type(new_entry), ScheduleEntry): raise ValueError( "Can only add children of ScheduleEntry to the VM resource schedule." ) self.schedule_list.append(new_entry)
[docs] def get_schedule(self): """Retrieve the schedule, but first walk through the schedule entries looking for any that place ``content`` into a VM. Because the ``content`` field can actually be a callable (see :py:class:`DropContentScheduleEntry <base_objects.DropContentScheduleEntry>` and :py:meth:`VMEndpoint's drop_content() <base_objects.VMEndpoint.drop_content>` for more details), this method will check to see if any ``content`` is callable, and if so, invoke it in order to generate the string-based content to be used by the VM resource. Lastly, this method modifies all :py:class:`ScheduleEntries <firewheel.vm_resource_manager.schedule_entry.ScheduleEntry>` to ensure that their class is a :py:class:`ScheduleEntry <firewheel.vm_resource_manager.schedule_entry.ScheduleEntry>` rather than any subclass type. This facilitates the :ref:`vm-resource-handler` to unpickle the objects correctly. Returns: list: The full list of schedule entries. """ # If the schedule is being asked for then walk through the # schedule entries looking for content to be loaded into a VM. # If the content is actually a callable, then call it in order # to generate the string content to be used by the vm_resource. for entry in self.schedule_list: try: for data in entry.data: if "content" in data and callable(data["content"]): # Replace the content value with the string that results # from using the callback that was passed to the # schedule entry data["content"] = data["content"]() except AttributeError: continue # Flip all schedule entries back to the parent class # so the resource handler can unpickle them entry.__class__ = ScheduleEntry return self.schedule_list
[docs] def get_serialized_schedule(self): """Serialize the schedule by pickling each schedule entry and then returning a tuple of pickled schedule entries. Returns: tuple: A tuple of pickled schedule entries. """ return (pickle.dumps(entry) for entry in self.schedule_list)
[docs] def __str__(self): """Create a human readable string representation of this object. Returns: str: A string representation of :py:class:`base_objects.VmResourceSchedule`. """ ret = "[\n" for entry in self.get_schedule(): ret += f"{entry!s},\n" ret += "]" return ret
[docs] class Interfaces: """This object represents a VMs network interfaces. It largely consists of a list of interfaces which are a dictionary of interface-related properties. """
[docs] def __init__(self, prefix="eth"): """Initialize the list of interfaces. Args: prefix (str, optional): The default name of the interface (e.g. ``ens``, ``eth``, etc.). Defaults to ``"eth"``. """ self.interfaces = [] self.prefix = prefix self.counter = 0
[docs] def add_interface( self, address, netmask, qos=None, switch=None, control_network=False, l2_connection=False, ): """Add a new interface to the list of interfaces. Args: address (str or netaddr.IPAddress): The IP address for the interface. An IP address is not strictly required in the case where a new interface is needed but it is not a Layer-3 connection. netmask (str or netaddr.IPAddress): The netmask for the connecting interface. The netmask can either be in dotted decimal or CIDR (without the slash) notation. That is, both ``"255.255.255.0"`` and ``"24"`` would represent the same netmask. qos (dict, optional): A dictionary of QoS-specific parameters. It can include any of the following keys: ``{"loss", "delay", "rate"}``. Defaults to :py:data:`None`. switch (base_objects.Switch, optional): The switch which will be connected to the interface. Defaults to :py:data:`None`. control_network (bool, optional): Identify if the interface will be part of the control network. Defaults to :py:data:`False`. l2_connection (bool): Identify if the interface should be a Layer-2 interface. Defaults to :py:data:`False`. Returns: dict: A new interface dictionary containing the interface name, address, netmask, network, switch, and QoS dictionary. """ name = f"{self.prefix}{self.counter}" # If this is only a L2 link then the address might # not be populated if address: address = netaddr.IPAddress(address) # We can improve performance if we pass in a tuple to create the # netaddr.IPNetwork. The tuple is the integer value for the IPAddress # and the Integer CIDR netmask. if isinstance(netmask, netaddr.IPAddress): netmask = netmask.netmask_bits() else: try: netmask = int(netmask) except ValueError: netmask = netaddr.IPAddress(netmask) netmask = netmask.netmask_bits() network = netaddr.IPNetwork((address.value, netmask)) else: network = None qos_dict = {"loss": None, "delay": None, "rate": None} if qos: qos_dict.update(qos) interface = { "name": name, "address": address, "netmask": netmask, "network": network, "switch": switch, "qos": qos_dict, "control_network": control_network, "l2_connection": l2_connection, } if control_network: self.interfaces.insert(0, interface) else: self.interfaces.append(interface) self.counter += 1 return interface
[docs] def del_interface(self, name): """Delete an interface based on the Interfaces name. Args: name (str): The name of an interface. """ self.interfaces = [ interface for interface in self.interfaces if "name" in interface and interface["name"] != name ]
[docs] def get_interface(self, name): """Retrieve the interface dictionary with the given name. Args: name (str): The name of the interface to return. Returns: dict: The requested interface dictionary. """ for interface in self.interfaces: if "name" in interface and interface["name"] == name: return interface return None
[docs] def rekey_interfaces(self): """A method to re-key the list of interfaces. That is, assuming the counter is 0, rename the interfaces in the list. This is useful after deleting an interface. """ tmp_list = [] counter = 0 for interface in self.interfaces: tmp_int = interface tmp_int["name"] = f"{self.prefix}{counter}" tmp_list.append(tmp_int) counter += 1 self.interfaces = tmp_list self.counter = counter
[docs] def __str__(self): """ A custom string method for Interface Objects. Returns: str: A string representation of an :py:class:`base_objects.Interfaces`. """ ret = "[\n" for interface in self.interfaces: new_int = dict(interface) new_int["address"] = str(new_int["address"]) new_int["netmask"] = str(new_int["netmask"]) new_int["network"] = str(new_int["network"]) new_int["switch"] = new_int["switch"].name ret += pprint.pformat(new_int) ret += ",\n" ret += "]" return ret
[docs] class VMEndpoint: """This class is the base class for all VM-based model components. It creates any necessary VM-based attributes and adds a :py:class:`base_objects.VmResourceSchedule` to the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>`. This class also provides methods which are useful for all VM objects. """
[docs] def __init__(self, name=None): """Initialize the :py:class:`VMEndpoint` and its associated attributes. Args: name (str, optional): The name of the VM. This can also be set on :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` creation. Defaults to :py:data:`None`. Raises: NameError: The :py:class:`VMEndpoint` does not have a name. ValueError: The name contains illegal characters (e.g. underscores, spaces, or commas). Attributes: name (str): Every VM must have a unique name. Names must follow conventions of valid hostnames (e.g. cannot contain underscores, spaces, or commas). vm (dict): A dictionary of VM-related properties. These will be used by other model components to instantiate the graph (e.g. :ref:`minimega.parse_experiment_graph_mc`). Most of these properties will be filled out be other model components either explicitly, or if not defined, a default value is provided. Common VM properties include .. code-block:: python :caption: An example VM property dictionary. { 'image_store': { # Location of where minimega should find the images 'path': '/tmp/minimega/files/images', 'name': 'images' }, 'architecture': 'x86_64', # The CPU architecture 'vcpu': { 'model': 'qemu64', # The QEMU vCPU model number 'sockets': 1, # The number of vCPU sockets 'cores': 1, # The number of vCPU cores 'threads': 1 # The number of vCPU threads }, 'mem': 256, # The amount of memory for the VM 'drives': [ # Information about the disk image { 'db_path': 'ubuntu-16.04.4-server-amd64.qcow2.xz', 'file': 'ubuntu-16.04.4-server-amd64.qcow2', 'interface': 'virtio', 'cache': 'writeback' } ], 'vga': 'std', # The type of VGA display to use 'image': 'ubuntu1604server' # A general image name. } type (str): The type of the VM. It should be one of ``{"host", "router", "switch"}``. Defaults to ``"host"``. coschedule (int): The number of other VMs allowed to be schedule on the same host as this VM. A value of ``0`` means this VM will have its own host, and ``-1`` (default) means there is no limit. vm_resource_schedule (base_objects.VmResourceSchedule): A new VMR schedule for the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>`. """ self.name = getattr(self, "name", name) if self.name is None: raise NameError("VMEndpoint must have a name!") if "_" in self.name or " " in self.name or "," in self.name: raise ValueError( f"Cannot set VM name: {self.name}.\n" "VM names must not contain underscores, spaces, or commas " "since those characters are not valid in hostnames!" ) self.vm = getattr(self, "vm", {}) self.type = getattr(self, "type", "host") # Set the coschedule attribute, how many VMs are allowed to be on the # same host as this VM. -1 means unlimited, 0 means the VM will get its # own host self.coschedule = getattr(self, "coschedule", -1) self.vm_resource_schedule = VmResourceSchedule()
[docs] def set_image(self, image_name): """ Add an image property to the VM dictionary. The name of the image is used by :ref:`minimega.resolve_vm_images_mc` to verify that the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` has a bootable image. This property should be set by all MCs which add an image to a given model component. .. seealso:: See the :ref:`image-creation-tutorial` for more details. Args: image_name (str): A generic name for the VM's image (e.g. ``"ubuntu1604server"``). """ try: self.vm["image"] = image_name except AttributeError: self.vm = {"image": image_name}
[docs] def add_vm_resource( self, start_time, vm_resource_name, dynamic_arg=None, static_arg=None ): """ This method adds a :py:class:`base_objects.VmResourceScheduleEntry` object to a :py:class:`Vertex's <firewheel.control.experiment_graph.Vertex>` :py:class:`base_objects.VmResourceSchedule`. This provides backwards compatibility for VMRs that were written for pre-2.0 versions of FIREWHEEL. For VMRs which use this method, they should expect to be passed three file names: ``dynamic``, ``static``, and ``reboot``. The first two files contain the content described in the ``dynamic_arg`` and ``static_arg``. Finally, the VMR gets passed the path to a ``reboot`` file as their last argument on the command line. If the VMR creates that ``reboot`` file then the *VM Resource Manager* will restart the VM upon completion of the VMR (see :ref:`vmr-rebooting` for more details). Args: start_time (int): The start time for the VMR. (See :ref:`start-time` for more details). vm_resource_name (str): The name of the VMR that should be executed. dynamic_arg (str, optional): Any is content that is dependent on the configuration of the graph which should be passed to the VMR. This information can change from experiment to experiment. It generally takes the form of an ASCII string or a serialized object (i.e. a dictionary dumped into JSON format via :py:func:`json.dumps`). This content gets written into a file inside the VM and the filename is then passed to the VMR as the first command line parameter. Defaults to :py:data:`None`. However if it is not specified, an empty ``dynamic`` file path is still passed to the VMR. static_arg (str, optional): The name of a file that needs to be loaded into the VM. That filename is then passed to the VMR as its second command line option. Static content generally takes the form of an installer or other binary blob that the VMR requires in order to accomplish its purpose. This is something that does not change regardless of the experiment that is using it. Defaults to :py:data:`None`. However if it is not specified, an empty ``static`` file path is still passed to the VMR. Returns: base_objects.VmResourceScheduleEntry: The newly created schedule entry. Examples: The following is a skeleton of an VMR which can use this method. It should be noted that there is no requirement that the VMR is a Python script. It can be any executable as long as it accepts and understands the three command line arguments explained above. .. code-block:: python :caption: Example VMR which can be used with the ``add_vm_resource()`` method. :linenos: #!/usr/bin/env python3 import sys class VmResource(object): def __init_(self, dynamic_content_filename, static_filename, reboot_filename): self.dynamic_content_filename = dynamic_content_filename self.static_filename = static_filename self.reboot_filename = reboot_filename def run(self): # VmResource logic implemented here return 0 if __name__ == '__main__': if len(sys.argv) != 4: print("VmResource did not receive expected arguments") sys.exit(1) dynamic_content_filename = sys.argv[1] static_filename = sys.argv[2] reboot_file = sys.argv[3] vm_resource = VmResource(dynamic_content_filename, static_filename, reboot_file) # Return with the result of the run method sys.exit(vm_resource.run()) Scheduling this example VMR on a :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` only requires that we pass the required information to the ``add_vm_resource()`` method for the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>`. In this particular case, we're assuming that the example VMR above is in a file called ``ex_vm_resource.py`` and the requirements in :ref:`using-vm-resources` have been satisfied. Since this is just a example, it doesn't use the arguments passed in, so there is no need to include them in the call to ``add_vm_resource()``. .. code-block:: python host = Vertex(self.g, "host") host.decorate(VMEndpoint) host.add_vm_resource(-10, 'ex_vm_resource.py') This will run the example VMR at configuration time ``-10`` and it will do nothing but return ``0``. """ # This is the default vm_resource vm_resource = VmResourceScheduleEntry( vm_resource_name, start_time, dynamic_arg, static_arg ) self.vm_resource_schedule.add_vm_resource(vm_resource) return vm_resource
[docs] def drop_content( self, start_time, location, content, executable=False, preload=True ): r""" This method is intended to take any string and write it to a specified location on a VM. This method adds a :py:class:`base_objects.DropContentScheduleEntry` object to a :py:class:`Vertex's <firewheel.control.experiment_graph.Vertex>` :py:class:`base_objects.VmResourceSchedule`. There are some use cases where ``content`` needs to be written to a file inside a VM, but the ``content`` can not be generated until all topology changes to the graph have been completed. To handle this situation, ``content`` can point to a callback function that returns a string instead of containing the string directly. Router configuration is a good example of this. The router needs to look at the networks that are connected to it and its routing peers in order to know what must be included in its configuration. Since a router can be created in one model component, but the topology can change in subsequent model components, the router does not have all its required information at creation time. In the case of the ``VyOS`` router, its plugin drops a configuration file where the ``content`` argument points at a function that will return a string. All callback functions are executed as part of the :py:meth:`base_objects.VmResourceSchedule.get_schedule` method (see :ref:`vm-resource-schedule`). The :ref:`vm_resource.schedule_mc` model component calls :py:meth:`base_objects.VmResourceSchedule.get_schedule` for each :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` in the graph. Args: start_time (int): The schedule time (positive or negative) of when to perform the content write. (See :ref:`start-time` for more details). location (str): The absolute Unix-style path (including filename) on the VM to write the content. For example, to write content to ``C:\Users\Administrator\Desktop\test.txt`` inside a Windows VM, the ``location`` would need to be ``/Users/Administrator/Documents/test.txt``. content (str): The content to write out. This can be either a string, a serialized object (via :py:mod:`json` or :py:mod:`pickle`), or a function pointer. Function pointers can be passed in the content field for delayed content generation (see the examples below). executable (bool, optional): Should the file be made executable. This flag is ignored for Windows VMs. Defaults to :py:data:`False`. preload (bool, optional): If set, instead of dropping the content directly into ``location`` at ``start_time``, the content will be preloaded into a temporary location prior to any schedule entries running and then moved to ``location`` at ``start_time``. Defaults to :py:data:`True`. This is particularly useful for placing files into directories which will be created by another VMR. Returns: base_objects.DropContentScheduleEntry: The newly created schedule entry. Examples: **Drop Content Example with a Callback Function** The following is an example of using ``drop_content()`` with a callback function. This is taken from the :ref:`vyos_mc` model component. The callback function is implemented in the model component and is called ``self._configure_vyos``. It returns a string containing the configuration file for the router. That string is then written to ``/opt/vyatta/etc/config/firewheel-config.sh``. .. code-block:: python # Path to write the configuration file inside the router configuration_location = '/opt/vyatta/etc/config/firewheel-config.sh' # Drop the configuration file on the router. # This is done by supplying a callback to the ScheduleEntry. # This will be called when the schedule is being generated to be uploaded self.drop_content(-100, configuration_location, self._configure_vyos, executable=True) **Drop Content Example with a String** The following example is a bit more straightforward. In this case the ``drop_content()`` function is provided a string to be written to a file in the VM. Since this example is using a Windows VM, the ``location`` for the content is a Unix-style path that translates to ``C:\Users\User\Desktop\hello.txt`` in the VM. .. code-block:: python host = Vertex(self.g, "host") host.decorate(WindowsHost) content = f"Hello World! My hostname is: {self.name}" host.drop_content(-200, "/Users/User/Desktop/hello.txt", content) """ if not os.path.splitext(location)[-1]: self.log.warning( "The drop_content location '%s' does not have " "a file extension. The drop_content method requires " "that you provide the full destination path to the " "file (including file name).", location, ) if preload: if self.is_decorated_by(AbstractWindowsEndpoint): preload_move_cmd = "move" else: preload_move_cmd = "mv" else: preload_move_cmd = None vm_resource = DropContentScheduleEntry( start_time, location, content, executable, preload_move_cmd ) self.vm_resource_schedule.add_vm_resource(vm_resource) return vm_resource
[docs] def drop_file(self, start_time, location, filename, executable=False, preload=True): """ This method is intended to take a file and load it on to a VM at a specified location. This method adds a :py:class:`base_objects.DropFileScheduleEntry` object to a :py:class:`Vertex's <firewheel.control.experiment_graph.Vertex>` :py:class:`base_objects.VmResourceSchedule`. Note: If the file happens to already exist on the VM, it will be overwritten. Args: start_time (int): The schedule time (positive or negative) of when to execute the dropping of the file. (See :ref:`start-time` for more details). location (str): The absolute path (including filename) on the VM to write the file. filename (str): The local name of the file (i.e. the name of the file within the model component). Since the file must live in the model component, the ``filename`` is not a path. Therefore, it should not include any directories in its name. executable (bool, optional): Should the file have its executable flag set. This flag is ignored for Windows VMs. Defaults to :py:data:`False`. preload (bool, optional): If set, instead of dropping the file directly into ``location`` at ``start_time``, the file will be preloaded into a temporary location prior to any schedule entries running and then moved to ``location`` at ``start_time``. Defaults to :py:data:`True`. This is particularly useful for placing files into directories which will be created by another VMR. Returns: base_objects.DropFileScheduleEntry: The newly created schedule entry. Examples: The following example shows dropping a static file on a VM. In this case, a ``vimrc`` file is being loaded onto a VM. This example assumes that the model component containing this code has declared ``vimrc`` as a VMR in its ``MANIFEST`` file and the ``vimrc`` file is located within the model component's directory. .. code-block:: python host = Vertex(self.g, "host") host.decorate(Ubuntu1604Server) host.drop_file(-200, "/home/ubuntu/.vimrc", "vimrc") """ if not os.path.splitext(location)[-1]: self.log.warning( "The drop_file location '%s' for file %s does not have " "a file extension. The drop_file method requires " "that you provide the full destination path to the " "file (including file name).", location, filename, ) if preload: if self.is_decorated_by(AbstractWindowsEndpoint): preload_move_cmd = "move" else: preload_move_cmd = "mv" else: preload_move_cmd = None vm_resource = DropFileScheduleEntry( start_time, location, filename, executable, preload_move_cmd ) self.vm_resource_schedule.add_vm_resource(vm_resource) return vm_resource
[docs] def run_executable(self, start_time, program, arguments=None, vm_resource=False): r""" This method allows a user to specify an executable to run and the arguments to pass to it on the command line. It supports both programs that are natively included in the VM (e.g. ``/sbin/ip`` or ``C:\windows\system32\ipconfig.exe``) as well as VM resources. This method adds a :py:class:`base_objects.RunExecutableScheduleEntry` object to a :py:class:`Vertex's <firewheel.control.experiment_graph.Vertex>` :py:class:`base_objects.VmResourceSchedule`. Args: start_time (int): The schedule time (positive or negative) of when to execute the specified program. (See :ref:`start-time` for more details). program (str): The name of the program or script to run. In general, it's safer to provide absolute paths for program names instead of relying on the environment of the VM to resolve the name. arguments (str or list, optional): This field allows a user to provide arguments for the program as either a single string or a list of strings. These get passed to the program on the command line. Defaults to :py:data:`None`. vm_resource (bool, optional): This parameter indicates if program is the name of a script that needs to be loaded on to the VM before execution. If ``vm_resource`` is :py:data:`True` then the specified program name is assumed to be the local filename of the file (i.e. not the full path, just the filename) to load on to the VM. That is, the file will be located within a model component. Defaults to :py:data:`False`. Returns: base_objects.RunExecutableScheduleEntry: The newly created schedule entry. Examples: **Native Executable Example** To run a program that is native to the VM, specify the Unix-style absolute path to the executable and optionally include arguments. For example, the ``ipconfig`` executable on a Windows VM is located at ``C:\Windows\system32\ipconfig.exe``. The required Unix-style path for ``ipconfig`` would be ``/windows/system32/ipconfig.exe``. The path being absolute is not strictly required, but it is generally safer than assuming the program is part of the VM's ``PATH``. For example, the following runs the ``hostname`` command on a Linux VM at negative time ``-100``, passing in the name of the host as an argument. .. code-block:: python host = Vertex(self.g, "host") host.decorate(LinuxHost) host.run_executable(-100, '/sbin/hostname', host.name) **VM Resource Executable Example** Running a VMR file on a VM where arguments can be passed to it via the command line is very similar. The ``program`` needs to be the name of the VMR file and the ``vm_resource`` flag needs to be set to :py:data:`True`. For example, instead of calling the ``hostname`` command directly, let's run a VMR that calls the ``hostname`` command and adds the hostname to the ``/etc/hosts`` file. The VMR file will be called ``set_hostname.sh``: .. code-block:: bash echo $1 > /etc/hostname sed -i '/127.0.0.1/127.0.0.1 localhost '$1 /etc/hosts Standard VMR requirements for model components apply (see :ref:`using-vm-resources`). Executing the ``set_hostname.sh`` VMR on a :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` at time ``-100`` passing in the name of the host as an argument is as follows: .. code-block:: python host = Vertex(self.g, "host") host.decorate(LinuxHost) host.run_executable(-100, 'set_hostname.sh', host.name, vm_resource=True) """ exec_vm_resource = RunExecutableScheduleEntry( start_time, program, arguments, vm_resource ) self.vm_resource_schedule.add_vm_resource(exec_vm_resource) return exec_vm_resource
[docs] def file_transfer(self, location, interval=60, start_time=1, destination=None): """ This method facilitates pulling a file or directory off of a VM at a regular time interval. If the VM is a Linux host then files only get pulled when they have changed (after the initial pull). If the VM is a Windows host then the file/directory will get pulled at every interval. Note: If specifying a destination, the FIREWHEEL group (if any) must have permissions to modify and write to that directory. See the :ref:`config-system` configuration options to add FIREWHEEL group permissions. Args: location (str): Absolute path inside the VM to the file or directory to be monitored and pulled off the VM. interval (int, optional): Time interval in seconds to pull file or directory from the VM. start_time (int, optional): The schedule time to transfer the file or directory out of the VM. destination (str, optional): Absolute path on compute node of the directory where transferred files are to be placed: ``<destination>/<vm_name>/<location>``. If no destination is provided, files will be written to ``<logging.root_dir>/transfers/``. See :py:meth:`_transfer_data <firewheel.vm_resource_manager.vm_resource_handler.VMResourceHandler._transfer_data>` for more details. Returns: base_objects.FileTransferScheduleEntry: The newly created schedule entry. """ # noqa: E501,W505 transfer_vm_resource = FileTransferScheduleEntry( location, interval, start_time, destination ) self.vm_resource_schedule.add_vm_resource(transfer_vm_resource) return transfer_vm_resource
[docs] def file_transfer_once(self, location, start_time=-1, destination=None): """ This method facilitates pulling a file or directory off of a VM in an experiment a single time. Note: If specifying a destination, the FIREWHEEL group (if any) must have permissions to modify and write to that directory. See the :ref:`config-system` configuration options to add FIREWHEEL group permissions. Args: location (str): Absolute path inside the VM to the file or directory to be monitored and pulled off the VM. start_time (int, optional): The schedule time to transfer the file or directory out of the VM destination (str, optional): Absolute path on compute node of the directory where transferred files are to be placed: ``<destination>/<vm_name>/<location>``. If no destination is provided, files will be written to ``<logging.root_dir>/transfers/``. See :py:meth:`_transfer_data <firewheel.vm_resource_manager.vm_resource_handler.VMResourceHandler._transfer_data>` for more details. Returns: base_objects.FileTransferScheduleEntry: The newly created schedule entry. """ # noqa: E501,W505 # An interval of None indicates that no looping should happen # and therefore the file only gets pulled once transfer_vm_resource = FileTransferScheduleEntry( location, None, start_time, destination ) self.vm_resource_schedule.add_vm_resource(transfer_vm_resource) return transfer_vm_resource
[docs] def set_pause(self, start_time, duration=0): """ Create a :py:attr:`PAUSE <firewheel.vm_resource_manager.schedule_event.ScheduleEventType.PAUSE>` event for a VM at a given start time for the given duration. Primarily, pausing will only stop a given VM's schedule for the specified duration which will enable users to inspect VM's at a certain point within the experiment. Once the pause is complete, the schedule will proceed as expected. More information about pausing can be found in :ref:`vm-resource-schedule`. Args: start_time (int): The time where the pause should happen. This should be one of negative :py:data:`math.inf`, 0, or any positive time. duration (int): The length of the pause. Returns: ScheduleEntry: The created schedule entry. """ if duration == 0: Console().print( f"[b yellow]Pause duration set at 0 seconds for [cyan]{self.name}[/cyan] " f"at start time [cyan]{start_time}[/cyan]; it will have no effect" "on the experiment timing!" ) pause_vm_resource = PauseScheduleEntry(start_time, duration) self.vm_resource_schedule.add_vm_resource(pause_vm_resource) return pause_vm_resource
[docs] def set_break(self, start_time): """ Create a break event for a VM at a given start time. A break event is an indefinitely long :py:attr:`PAUSE <firewheel.vm_resource_manager.schedule_event.ScheduleEventType.PAUSE>` event. Primarily, pausing will only stop a given VM's schedule until a :py:attr:`RESUME <firewheel.vm_resource_manager.schedule_event.ScheduleEventType.RESUME>` event is processed. To enable a user to trigger a resume (and thereby end the break), FIREWHEEL has a :ref:`helper_vm_resume` Helper. Breaks are useful for enabling users to inspect VMs at a certain point within the experiment. Once the break is complete, the schedule will proceed as expected. More information about breaking and pausing can be found in :ref:`vm-resource-schedule`. Args: start_time (int): The time where the break should happen. This should be one of negative :py:data:`math.inf`, 0, or any positive time. Returns: ScheduleEntry: The created schedule entry. """ pause_vm_resource = PauseScheduleEntry(start_time, math.inf) self.vm_resource_schedule.add_vm_resource(pause_vm_resource) return pause_vm_resource
[docs] def set_default_gateway(self, interface): """This method sets the ``default_gateway`` attribute for VMs associated with the given router's interface. If the VM host has a router as a neighbor than that router will become the VM's default gateway. This can be used by other MCs to add the gateway to the VMs interface. Args: interface (dict): An interface dictionary (see :py:class:`base_objects.Interfaces`). """ for neighbor in interface["switch"].get_neighbors(): # Looking for hosts if not neighbor.type == "host": continue try: for iface in neighbor.interfaces.interfaces: if iface["address"] in interface["network"]: neighbor.default_gateway = interface["address"] break except AttributeError: self.log.debug("No interfaces on host: %s", neighbor.name) continue
[docs] def connect( self, switch, ip, netmask, delay=None, rate=None, rate_unit=None, packet_loss=None, control_network=False, ): """Create a link between this VM and the given :py:class:`base_objects.Switch` using the given IP address. This method mostly relies on :py:meth:`_connect <base_objects.VMEndpoint._connect>` for the primary logic. Note: For emulation-based models, due to limitations of `tc <https://linux.die.net/man/8/tc>`_ you can only add rate OR loss/delay to a VM. Enabling loss or delay will disable rate and vice versa. Note: The rate is set as a multiple of bits **not** bytes. That is, a rate of ``1 kbit`` would equal 1000 bits, not 1000 bytes. For bytes, multiply the rate by 8 (e.g. 64 KBytes = 8 * 64 = 512 kbit). Args: switch (base_objects.Switch): The Switch object to connect to. ip (str or netaddr.IPAddress): IP address to use on the connecting interface. This will eventually become the IP address on the VM's interface. netmask (str or netaddr.IPAddress): The netmask for the connecting interface. The netmask can either be in dotted decimal or CIDR (without the slash) notation. That is, both ``"255.255.255.0"`` and ``"24"`` would represent the same netmask. delay (str): The amount of egress delay to add for the link. This should be formatted like ``<delay><unit of delay>``. For example, ``100ms``. You must add this in the opposing direction if you want it to be bidirectional. rate (int): The maximum egress transmission rate (e.g. bandwidth of this link) as a multiple of bits. The ``rate_unit`` should also be set if the unit is not ``mbit``. rate_unit (str): The bandwidth unit (one of ``{'kbit', 'mbit', 'gbit'}``). Defaults to ``"mbit"``. packet_loss (int): Percent of packet loss on the link. For example, ``packet_loss = 25`` is 25% packet loss. control_network (bool): Is this connection to the control network. Defaults to :py:data:`False`. Returns: tuple(str, firewheel.control.experiment_graph.Edge): A tuple containing the name of the newly created VM interface and the :py:class:`Edge <firewheel.control.experiment_graph.Edge>` which connects the VM to a :py:class:`Switch <base_objects.Switch>`. """ (interface, edge) = self._connect( switch, ip, netmask, delay, rate=rate, rate_unit=rate_unit, packet_loss=packet_loss, control_network=control_network, ) return (interface["name"], edge)
[docs] def _connect( self, switch, ip, netmask, delay, rate=None, rate_unit=None, packet_loss=None, control_network=False, ): """Create a link between this host and the given :py:class:`base_objects.Switch` using the given IP address. Note: For emulation-based models, due to limitations of `tc <https://linux.die.net/man/8/tc>`_ you can only add rate OR loss/delay to a VM. Enabling loss or delay will disable rate and vice versa. Note: The rate is set as a multiple of bits **not** bytes. That is, a rate of ``1 kbit`` would equal 1000 bits, not 1000 bytes. For bytes, multiply the rate by 8 (e.g. 64 KBytes = 8 * 64 = 512 kbit). Args: switch (base_objects.Switch): The switch object to connect to. ip (str or netaddr.IPAddress): IP address to use on the connecting interface. This will eventually become the IP address on the VM's interface. netmask (str or netaddr.IPAddress): The netmask for the connecting interface. The netmask can either be in dotted decimal or CIDR (without the slash) notation. That is, both ``"255.255.255.0"`` and ``"24"`` would represent the same netmask. delay (str): The amount of egress delay to add for the link. This should be formatted like ``<delay><unit of delay>``. For example, ``100ms``. You must add this in the opposing direction if you want it to be bidirectional. rate (int): The maximum egress transmission rate (e.g. bandwidth of this link) as a multiple of bits. The ``rate_unit`` should also be set if the unit is not ``mbit``. rate_unit (str): The bandwidth unit (one of ``{'kbit', 'mbit', 'gbit'}``). Defaults to ``"mbit"``. packet_loss (int): Percent of packet loss on the link. For example, ``packet_loss = 25`` is 25% packet loss. control_network (bool): Is this connection to the control network. Defaults to :py:data:`False`. Raises: TypeError: If the switch is not of type :py:class:`base_objects.Switch`. Returns: tuple(str, firewheel.control.experiment_graph.Edge): A tuple containing the name of the newly created VM interface and the :py:class:`Edge <firewheel.control.experiment_graph.Edge>` which connects the VM to a :py:class:`Switch <base_objects.Switch>`. """ if not switch.is_decorated_by(Switch): raise TypeError("switch parameter must be (decorated by) a Switch.") try: interface = self.interfaces.add_interface( ip, netmask, None, switch, control_network ) except AttributeError: self.interfaces = Interfaces() interface = self.interfaces.add_interface( ip, netmask, None, switch, control_network ) edge = Edge(self, switch) edge.dst_ip = interface["address"] edge.dst_network = interface["network"] # Calling decorate is slow, so only apply QoSEdge if we need it. if delay is not None or rate is not None or packet_loss is not None: edge.decorate(QoSEdge) if delay is not None: edge.add_delay(delay) interface["qos"]["delay"] = delay if rate is not None: edge.add_rate_limit(rate, rate_unit) rate, rate_unit = edge.qos["rate"] interface["qos"]["rate"] = rate interface["qos"]["unit"] = rate_unit if packet_loss is not None: edge.add_packet_loss_percent(packet_loss) interface["qos"]["loss"] = packet_loss return (interface, edge)
[docs] def l2_connect(self, switch, mac=None): """Create a Layer 2 link between this host and the given switch using a "blank" IP address. Args: switch (base_objects.Switch): The switch object to connect to. mac (str, optional): A specific MAC address for the interface. Defaults to :py:data:`None`. Raises: TypeError: If the switch is not of type :py:class:`base_objects.Switch`. Returns: tuple(str, firewheel.control.experiment_graph.Edge): A tuple containing the name of the newly created VM interface and the :py:class:`Edge <firewheel.control.experiment_graph.Edge>` which connects the VM to a :py:class:`Switch <base_objects.Switch>`. """ if not switch.is_decorated_by(Switch): raise TypeError("switch parameter must be (decorated by) a Switch.") try: interface = self.interfaces.add_interface( None, None, None, switch, l2_connection=True ) except AttributeError: self.interfaces = Interfaces() interface = self.interfaces.add_interface( None, None, None, switch, l2_connection=True ) if mac: interface["mac"] = mac edge = Edge(self, switch) edge.dst_ip = "0.0.0.0" # noqa: S104 return (interface, edge)
[docs] class VmResourceScheduleEntry(ScheduleEntry): """ This class provides backwards compatibility for VMRs that were written for pre-2.0 versions of FIREWHEEL. Each VMR is provided with three command line arguments: ``dynamic_content``, ``static_content``, and a reboot file. """
[docs] def __init__( self, vm_resource_name, start_time, dynamic_contents=None, static_filename=None ): """ Create a VmResourceScheduleEntry Args: vm_resource_name (str): Name of VM resource to run. This VMR must be available to the experiment (i.e. must be specified in a model component's ``MANIFEST`` file). start_time (int): Start time of the VM resource as an integer. dynamic_contents (str, optional): Content to be passed to the VM resource. Dynamic content gets written to a file and the filename gets passed to the VM resource as the first parameter. Defaults to :py:data:`None`. static_filename (str, optional): File to be passed to the VM resource. The file must be in the VM resource's database (like the VM resource itself). The file is loaded into the VM and then the file's path is passed to the VM resource as the second parameter. Defaults to :py:data:`None`. """ super().__init__(start_time) # The vm_resource needs to be loaded into the VM self.add_file(vm_resource_name, vm_resource_name, executable=True) arguments = [] if dynamic_contents: self.add_content("dynamic", dynamic_contents) arguments.append("dynamic") else: arguments.append("None") if static_filename: self.add_file(static_filename, static_filename) arguments.append(static_filename) else: arguments.append("None") arguments.append("reboot") self.set_executable(vm_resource_name, arguments)
[docs] class DropContentScheduleEntry(ScheduleEntry): """ This class facilitates writing content to a file within the VM. This content is generally dynamically generated based off the current graph. """
[docs] def __init__( self, start_time, location, content, executable=False, preload_move_cmd=None ): """ Create a DropContentScheduleEntry. Args: start_time (int): Start time of the schedule entry as an integer. location (str): Absolute, Unix-style path inside the VM to write the provided content, including filename. If the VM is Windows then omit the drive letter. (e.g. ``/windows/system32``) content (str): Either the content to be written as a string or a :obj:`Callable <collections.abc.Callable>` object (e.g. a function pointer to a function) that returns a string. The callback function gets called during the :py:meth:`base_objects.VmResourceSchedule.get_schedule` method. The :ref:`vm_resource.schedule_mc` model component which then calls :py:meth:`get_schedule() <base_objects.VmResourceSchedule.get_schedule>` for every :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` in the graph. executable (bool, optional): Set the new file's executable flag. Default is :py:data:`False`. preload_move_cmd (str, optional): If set, instead of dropping the content directly into ``location`` at ``start_time``, the content will be preloaded into a temporary location prior to any schedule entries running and then moved to ``location`` at ``start_time`` with ``preload_move_cmd``. Defaults to :py:data:`None`. This is particularly useful for placing content into directories which will be created by another VMR. """ super().__init__(start_time) if preload_move_cmd: self.set_executable(preload_move_cmd, f"preloaded_content {location}") self.add_content("preloaded_content", content, executable) return self.add_content(location, content, executable)
[docs] class DropFileScheduleEntry(ScheduleEntry): """ This class facilitates loading a given file into the VM. """
[docs] def __init__( self, start_time, location, filename, executable=False, preload_move_cmd=None ): """ Create the DropFileScheduleEntry. Args: start_time (int): Start time of the schedule entry as an integer. location (str): Absolute, Unix-style path inside the VM to write the provided content, including filename. If the VM is Windows then omit the drive letter. (e.g. ``/windows/system32``) filename (str): Name of file to be written. This file needs to be available to FIREWHEEL. This only happens when the files are included in the list of "vm_resources" that are specified in a model component's ``MANIFEST`` file. executable (bool, optional): Set the new file's executable flag. Default is :py:data:`False`. preload_move_cmd (str, optional): If set, instead of dropping the file directly into ``location`` at ``start_time``, the file will be preloaded into a temporary location prior to any schedule entries running and then moved to ``location`` at ``start_time`` with ``preload_move_cmd``. Defaults to :py:data:`None`. This is particularly useful for placing files into directories which will be created by another VMR. """ super().__init__(start_time) if preload_move_cmd: self.set_executable(preload_move_cmd, f"{filename} {location}") self.add_file(filename, filename, executable) return self.add_file(location, filename, executable)
[docs] class RunExecutableScheduleEntry(ScheduleEntry): """ RunExecutableScheduleEntry facilitates specifying a program to run within the VM. """
[docs] def __init__(self, start_time, program, arguments=None, vm_resource=False): """ Create a :py:class:`base_objects.RunExecutableScheduleEntry` with the given parameters. Args: start_time (int): Start time of the schedule entry as an integer. program (str): The program to run. Unless ``vm_resource`` is set, it is safest to specify the absolute path of the program (in case the program is not on the system's ``PATH``). arguments (str or list, optional): Arguments to pass on the command line to the program. Must be a string or list of strings. Defaults to :py:data:`None`. vm_resource (bool, optional): If the program is a VM resource, then the VM resource file needs to be loaded into the VM before it is run. Defaults to :py:data:`False`. """ super().__init__(start_time) self.set_executable(program, arguments) # If the program is a VM resource, then the vm_resource file needs to be loaded into # the VM. Add it as a file with a relative path of just its name if vm_resource: self.add_file(program, program, executable=True)
[docs] class FileTransferScheduleEntry(ScheduleEntry): """ Facilitates programmatically pulling data out of a VM in an experiment. This is primarily accomplished by leveraging the :py:meth:`add_file_transfer <firewheel.vm_resource_manager.schedule_entry.ScheduleEntry.add_file_transfer>` method. """
[docs] def __init__( self, in_vm_location, interval=10, start_time=-1000000, out_host_destination=None, ): """Schedule extracting a file from a VM at a specified interval. If the VM is a Linux host then files only get pulled when they have changed (after the initial pull). If the VM is a Windows host then the file/directory will get pulled at every interval. Note: If specifying a destination, the FIREWHEEL group (if any) must have permissions to modify and write to that directory. See the :ref:`config-system` configuration options to add FIREWHEEL group permissions. Args: in_vm_location (str): Path inside the VM to the file or directory to be monitored and extracted from the VM. interval (int, optional): Interval specifying how often to check for file or directory updates. Defaults to ``10``. This enables extracting files which are constantly updating (e.g. logs). start_time (int, optional): When to schedule the transfer. Defaults to ``-1000000``. Because of the highly negative start time, this will almost always run immediately. out_host_destination (str, optional): Absolute path on compute node of the directory where transferred files are to be placed: ``<destination>/<vm_name>/<location>``. If no destination is provided, files will be written to ``<logging.root_dir>/transfers/``. See :py:meth:`_transfer_data <firewheel.vm_resource_manager.vm_resource_handler.VMResourceHandler._transfer_data>` for more details. """ # noqa: E501,W505 super().__init__(start_time) self.add_file_transfer(in_vm_location, interval, out_host_destination)
[docs] class PauseScheduleEntry(ScheduleEntry): """ Create a :py:attr:`PAUSE <firewheel.vm_resource_manager.schedule_event.ScheduleEventType.PAUSE>` schedule entry for the given VM. """
[docs] def __init__(self, start_time, duration=0): """ Create a :py:class:`base_objects.PauseScheduleEntry` with the given parameters. While a start time of 0 *is* permitted for this schedule entry, in practice, this entry is really converted to the minimum representable positive normalized float via `sys.float_info.min <https://docs.python.org/3/library/sys.html#sys.float_info.min>`_. Note: When support for Python 3.8 is dropped, this could be converted to the smallest positive denormalized representable float via :py:func:`math.ulp` (e.g., ``math.ulp(0.0)``). Args: start_time (int): Start time of the schedule entry as an integer. duration (int): The length of the pause which should happen. If the duration is :py:data:`math.inf` than this counts as a *break*. Raises: ValueError: If the start time is invalid (i.e., less than 0 and not infinity). ValueError: If the duration is not positive. """ valid_start_string = str( "Valid start times for pause and break only include " "negative infinity or greater than or equal to 0." ) # We need to verify valid start times if start_time < 0 and not math.isinf(start_time): raise ValueError(f"Invalid start time! {valid_start_string}") if start_time == 0: self.log.debug("Converting a 0 start time to `sys.float_info.min`.") start_time = sys.float_info.min super().__init__(start_time) # We need to verify valid durations if duration < 0: raise ValueError("The duration for a pause/break must not be negative.") self.add_pause(duration)