diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 9ccc8601d046..ec4b616887f1 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -107,6 +107,8 @@ import salt.utils.validate.net import salt.utils.versions import salt.utils.yaml + +from salt.utils.virt import check_remote, download_remote from salt.exceptions import CommandExecutionError, SaltInvocationError from salt.ext import six from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin @@ -120,6 +122,8 @@ ) ) +CACHE_DIR = '/var/lib/libvirt/saltinst' + VIRT_STATE_NAME_MAP = {0: 'running', 1: 'running', 2: 'running', @@ -533,6 +537,7 @@ def _gen_xml(name, os_type, arch, graphics=None, + boot=None, **kwargs): ''' Generate the XML string to define a libvirt VM @@ -569,11 +574,15 @@ def _gen_xml(name, else: context['boot_dev'] = ['hd'] + context['boot'] = boot if boot else {} + if os_type == 'xen': # Compute the Xen PV boot method if __grains__['os_family'] == 'Suse': - context['kernel'] = '/usr/lib/grub2/x86_64-xen/grub.xen' - context['boot_dev'] = [] + if not boot or not boot.get('kernel', None): + context['boot']['kernel'] = \ + '/usr/lib/grub2/x86_64-xen/grub.xen' + context['boot_dev'] = [] if 'serial_type' in kwargs: context['serial_type'] = kwargs['serial_type'] @@ -1141,6 +1150,34 @@ def _get_merged_nics(hypervisor, profile, interfaces=None, dmac=None): return nicp +def _handle_remote_boot_params(orig_boot): + """ + Checks if the boot parameters contain a remote path. If so, it will copy + the parameters, download the files specified in the remote path, and return + a new dictionary with updated paths containing the canonical path to the + kernel and/or initrd + + :param orig_boot: The original boot parameters passed to the init or update + functions. + """ + saltinst_dir = None + new_boot = orig_boot.copy() + + try: + for key in ['kernel', 'initrd']: + if check_remote(orig_boot.get(key)): + if saltinst_dir is None: + os.makedirs(CACHE_DIR) + saltinst_dir = CACHE_DIR + + new_boot[key] = download_remote(orig_boot.get(key), + saltinst_dir) + + return new_boot + except Exception as err: + raise err + + def init(name, cpu, mem, @@ -1162,6 +1199,7 @@ def init(name, graphics=None, os_type=None, arch=None, + boot=None, **kwargs): ''' Initialize a new vm @@ -1292,6 +1330,22 @@ def init(name, :param password: password to connect with, overriding defaults .. versionadded:: 2019.2.0 + :param boot: + Specifies kernel for the virtual machine, as well as boot parameters + for the virtual machine. This is an optionl parameter, and all of the + keys are optional within the dictionary. If a remote path is provided + to kernel or initrd, salt will handle the downloading of the specified + remote fild, and will modify the XML accordingly. + + .. code-block:: python + + { + 'kernel': '/root/f8-i386-vmlinuz', + 'initrd': '/root/f8-i386-initrd', + 'cmdline': 'console=ttyS0 ks=http://example.com/f8-i386/os/' + } + + .. versionadded:: neon .. _init-nic-def: @@ -1539,7 +1593,11 @@ def init(name, if arch is None: arch = 'x86_64' if 'x86_64' in arches else arches[0] - vm_xml = _gen_xml(name, cpu, mem, diskp, nicp, hypervisor, os_type, arch, graphics, **kwargs) + if boot is not None: + boot = _handle_remote_boot_params(boot) + + vm_xml = _gen_xml(name, cpu, mem, diskp, nicp, hypervisor, os_type, arch, + graphics, boot, **kwargs) conn = __get_conn(**kwargs) try: conn.defineXML(vm_xml) @@ -1718,6 +1776,7 @@ def update(name, interfaces=None, graphics=None, live=True, + boot=None, **kwargs): ''' Update the definition of an existing domain. @@ -1753,6 +1812,23 @@ def update(name, :param username: username to connect with, overriding defaults :param password: password to connect with, overriding defaults + :param boot: + Specifies kernel for the virtual machine, as well as boot parameters + for the virtual machine. This is an optionl parameter, and all of the + keys are optional within the dictionary. If a remote path is provided + to kernel or initrd, salt will handle the downloading of the specified + remote fild, and will modify the XML accordingly. + + .. code-block:: python + + { + 'kernel': '/root/f8-i386-vmlinuz', + 'initrd': '/root/f8-i386-initrd', + 'cmdline': 'console=ttyS0 ks=http://example.com/f8-i386/os/' + } + + .. versionadded:: neon + :return: Returns a dictionary indicating the status of what has been done. It is structured in @@ -1793,6 +1869,10 @@ def update(name, # Compute the XML to get the disks, interfaces and graphics hypervisor = desc.get('type') all_disks = _disk_profile(disk_profile, hypervisor, disks, name, **kwargs) + + if boot is not None: + boot = _handle_remote_boot_params(boot) + new_desc = ElementTree.fromstring(_gen_xml(name, cpu, mem, @@ -1802,6 +1882,7 @@ def update(name, domain.OSType(), desc.find('.//os/type').get('arch'), graphics, + boot, **kwargs)) # Update the cpu @@ -1811,6 +1892,48 @@ def update(name, cpu_node.set('current', six.text_type(cpu)) need_update = True + # Update the kernel boot parameters + boot_tags = ['kernel', 'initrd', 'cmdline'] + parent_tag = desc.find('os') + + # We need to search for each possible subelement, and update it. + for tag in boot_tags: + # The Existing Tag... + found_tag = desc.find(tag) + + # The new value + boot_tag_value = boot.get(tag, None) if boot else None + + # Existing tag is found and values don't match + if found_tag and found_tag.text != boot_tag_value: + + # If the existing tag is found, but the new value is None + # remove it. If the existing tag is found, and the new value + # doesn't match update it. In either case, mark for update. + if boot_tag_value is None \ + and boot is not None \ + and parent_tag is not None: + ElementTree.remove(parent_tag, tag) + else: + found_tag.text = boot_tag_value + + need_update = True + + # Existing tag is not found, but value is not None + elif found_tag is None and boot_tag_value is not None: + + # Need to check for parent tag, and add it if it does not exist. + # Add a subelement and set the value to the new value, and then + # mark for update. + if parent_tag is not None: + child_tag = ElementTree.SubElement(parent_tag, tag) + else: + new_parent_tag = ElementTree.Element('os') + child_tag = ElementTree.SubElement(new_parent_tag, tag) + + child_tag.text = boot_tag_value + need_update = True + # Update the memory, note that libvirt outputs all memory sizes in KiB for mem_node_name in ['memory', 'currentMemory']: mem_node = desc.find(mem_node_name) diff --git a/salt/states/virt.py b/salt/states/virt.py index fb3980e19610..53e67ea7e0ba 100644 --- a/salt/states/virt.py +++ b/salt/states/virt.py @@ -264,7 +264,8 @@ def running(name, username=None, password=None, os_type=None, - arch=None): + arch=None, + boot=None): ''' Starts an existing guest, or defines and starts a new VM with specified arguments. @@ -349,6 +350,23 @@ def running(name, .. versionadded:: Neon + :param boot: + Specifies kernel for the virtual machine, as well as boot parameters + for the virtual machine. This is an optionl parameter, and all of the + keys are optional within the dictionary. If a remote path is provided + to kernel or initrd, salt will handle the downloading of the specified + remote fild, and will modify the XML accordingly. + + .. code-block:: python + + { + 'kernel': '/root/f8-i386-vmlinuz', + 'initrd': '/root/f8-i386-initrd', + 'cmdline': 'console=ttyS0 ks=http://example.com/f8-i386/os/' + } + + .. versionadded:: neon + .. rubric:: Example States Make sure an already-defined virtual machine called ``domain_name`` is running: @@ -413,7 +431,8 @@ def running(name, live=False, connection=connection, username=username, - password=password) + password=password, + boot=boot) if status['definition']: action_msg = 'updated and started' __salt__['virt.start'](name) @@ -431,7 +450,8 @@ def running(name, graphics=graphics, connection=connection, username=username, - password=password) + password=password, + boot=boot) ret['changes'][name] = status if status.get('errors', None): ret['comment'] = 'Domain {0} updated, but some live update(s) failed'.format(name) @@ -466,7 +486,8 @@ def running(name, priv_key=priv_key, connection=connection, username=username, - password=password) + password=password, + boot=boot) ret['changes'][name] = 'Domain defined and started' ret['comment'] = 'Domain {0} defined and started'.format(name) except libvirt.libvirtError as err: diff --git a/salt/templates/virt/libvirt_domain.jinja b/salt/templates/virt/libvirt_domain.jinja index 0b4c3fc2d6eb..fdaea168f2bb 100644 --- a/salt/templates/virt/libvirt_domain.jinja +++ b/salt/templates/virt/libvirt_domain.jinja @@ -5,7 +5,17 @@ {{ mem }} {{ os_type }} - {% if kernel %}{{ kernel }}{% endif %} + {% if boot %} + {% if 'kernel' in boot %} + {{ boot.kernel }} + {% endif %} + {% if 'initrd' in boot %} + {{ boot.initrd }} + {% endif %} + {% if 'cmdline' in boot %} + {{ boot.cmdline }} + {% endif %} + {% endif %} {% for dev in boot_dev %} {% endfor %} diff --git a/salt/utils/virt.py b/salt/utils/virt.py index 9dad849c0e21..b36adba81c3e 100644 --- a/salt/utils/virt.py +++ b/salt/utils/virt.py @@ -6,16 +6,59 @@ # Import python libs import os +import re import time import logging +import hashlib + +# pylint: disable=E0611 +from salt.ext.six.moves.urllib.parse import urlparse +from salt.ext.six.moves.urllib import request # Import salt libs import salt.utils.files - log = logging.getLogger(__name__) +def download_remote(url, dir): + """ + Attempts to download a file specified by 'url' + + :param url: The full remote path of the file which should be downloaded. + :param dir: The path the file should be downloaded to. + """ + + try: + rand = hashlib.md5(os.urandom(32)).hexdigest() + remote_filename = urlparse(url).path.split('/')[-1] + full_directory = \ + os.path.join(dir, "{}-{}".format(rand, remote_filename)) + with salt.utils.files.fopen(full_directory, 'wb') as file,\ + request.urlopen(url) as response: + file.write(response.rease()) + + return full_directory + + except Exception as err: + raise err + + +def check_remote(cmdline_path): + """ + Checks to see if the path provided contains ftp, http, or https. Returns + the full path if it is found. + + :param cmdline_path: The path to the initrd image or the kernel + """ + regex = re.compile('^(ht|f)tps?\\b') + + if regex.match(urlparse(cmdline_path).scheme): + return True + + return False + + class VirtKey(object): ''' Used to manage key signing requests. diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py index 82cf26e15a3e..a04597285e40 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -10,6 +10,7 @@ import os import re import datetime +import shutil # Import Salt Testing libs from tests.support.mixins import LoaderModuleMockMixin @@ -23,6 +24,7 @@ from salt._compat import ElementTree as ET import salt.config import salt.syspaths +import tempfile from salt.exceptions import CommandExecutionError # Import third party libs @@ -30,7 +32,6 @@ # pylint: disable=import-error from salt.ext.six.moves import range # pylint: disable=redefined-builtin - # pylint: disable=invalid-name,protected-access,attribute-defined-outside-init,too-many-public-methods,unused-argument @@ -610,6 +611,7 @@ def test_gen_xml_for_xen_default_profile(self): 'xen', 'xen', 'x86_64', + boot=None ) root = ET.fromstring(xml_data) self.assertEqual(root.attrib['type'], 'xen') @@ -1123,6 +1125,67 @@ def test_init(self): self.assertFalse('