Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-15-SP1:GA
salt.14915
virt-adding-kernel-boot-parameters-to-libvirt-x...
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File virt-adding-kernel-boot-parameters-to-libvirt-xml-55.patch of Package salt.14915
From ce8d431fad81547d1e76f264a172d27e2b4fccb1 Mon Sep 17 00:00:00 2001 From: Larry Dewey <ldewey@suse.com> Date: Tue, 7 Jan 2020 02:48:11 -0700 Subject: [PATCH] virt: adding kernel boot parameters to libvirt xml #55245 (#197) * virt: adding kernel boot parameters to libvirt xml SUSE's autoyast and Red Hat's kickstart take advantage of kernel paths, initrd paths, and kernel boot command line parameters. These changes provide the option of using these, and will allow salt and autoyast/kickstart to work together. Signed-off-by: Larry Dewey <ldewey@suse.com> * virt: Download linux and initrd Signed-off-by: Larry Dewey <ldewey@suse.com> --- salt/modules/virt.py | 129 ++++++++++++++++++++++- salt/states/virt.py | 29 ++++- salt/templates/virt/libvirt_domain.jinja | 12 ++- salt/utils/virt.py | 45 +++++++- tests/unit/modules/test_virt.py | 79 +++++++++++++- tests/unit/states/test_virt.py | 19 +++- 6 files changed, 302 insertions(+), 11 deletions(-) diff --git a/salt/modules/virt.py b/salt/modules/virt.py index dedcf8cb6f..0f62856f5c 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -106,6 +106,8 @@ import salt.utils.templates 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 @@ -119,6 +121,8 @@ JINJA = jinja2.Environment( ) ) +CACHE_DIR = '/var/lib/libvirt/saltinst' + VIRT_STATE_NAME_MAP = {0: 'running', 1: 'running', 2: 'running', @@ -532,6 +536,7 @@ def _gen_xml(name, os_type, arch, graphics=None, + boot=None, **kwargs): ''' Generate the XML string to define a libvirt VM @@ -568,11 +573,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'] @@ -1115,6 +1124,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, @@ -1136,6 +1173,7 @@ def init(name, graphics=None, os_type=None, arch=None, + boot=None, **kwargs): ''' Initialize a new vm @@ -1266,6 +1304,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: @@ -1513,7 +1567,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) @@ -1692,6 +1750,7 @@ def update(name, interfaces=None, graphics=None, live=True, + boot=None, **kwargs): ''' Update the definition of an existing domain. @@ -1727,6 +1786,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 @@ -1767,6 +1843,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, @@ -1776,6 +1856,7 @@ def update(name, domain.OSType(), desc.find('.//os/type').get('arch'), graphics, + boot, **kwargs)) # Update the cpu @@ -1785,6 +1866,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 68e9ac6fb6..c700cae849 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 0b4c3fc2d6..fdaea168f2 100644 --- a/salt/templates/virt/libvirt_domain.jinja +++ b/salt/templates/virt/libvirt_domain.jinja @@ -5,7 +5,17 @@ <currentMemory unit='KiB'>{{ mem }}</currentMemory> <os> <type arch='{{ arch }}'>{{ os_type }}</type> - {% if kernel %}<kernel>{{ kernel }}</kernel>{% endif %} + {% if boot %} + {% if 'kernel' in boot %} + <kernel>{{ boot.kernel }}</kernel> + {% endif %} + {% if 'initrd' in boot %} + <initrd>{{ boot.initrd }}</initrd> + {% endif %} + {% if 'cmdline' in boot %} + <cmdline>{{ boot.cmdline }}</cmdline> + {% endif %} + {% endif %} {% for dev in boot_dev %} <boot dev='{{ dev }}' /> {% endfor %} diff --git a/salt/utils/virt.py b/salt/utils/virt.py index 9dad849c0e..b36adba81c 100644 --- a/salt/utils/virt.py +++ b/salt/utils/virt.py @@ -6,16 +6,59 @@ from __future__ import absolute_import, print_function, unicode_literals # 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 6f594a8ff3..4bdb933a2d 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, print_function, unicode_literals import os import re import datetime +import shutil # Import Salt Testing libs from tests.support.mixins import LoaderModuleMockMixin @@ -23,6 +24,7 @@ import salt.modules.config as config 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 @@ from salt.ext import six # 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 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): 'xen', 'xen', 'x86_64', + boot=None ) root = ET.fromstring(xml_data) self.assertEqual(root.attrib['type'], 'xen') @@ -1123,6 +1125,67 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): self.assertFalse('<interface' in definition) self.assertFalse('<disk' in definition) + # Ensure the init() function allows creating VM without NIC and + # disk but with boot parameters. + + defineMock.reset_mock() + mock_run.reset_mock() + boot = { + 'kernel': '/root/f8-i386-vmlinuz', + 'initrd': '/root/f8-i386-initrd', + 'cmdline': + 'console=ttyS0 ks=http://example.com/f8-i386/os/' + } + retval = virt.init('test vm boot params', + 2, + 1234, + nic=None, + disk=None, + seed=False, + start=False, + boot=boot) + definition = defineMock.call_args_list[0][0][0] + self.assertEqual('<kernel' in definition, True) + self.assertEqual('<initrd' in definition, True) + self.assertEqual('<cmdline' in definition, True) + self.assertEqual(retval, True) + + # Verify that remote paths are downloaded and the xml has been + # modified + mock_response = MagicMock() + mock_response.read = MagicMock(return_value='filecontent') + cache_dir = tempfile.mkdtemp() + + with patch.dict(virt.__dict__, {'CACHE_DIR': cache_dir}): + with patch('salt.ext.six.moves.urllib.request.urlopen', + MagicMock(return_value=mock_response)): + with patch('salt.utils.files.fopen', + return_value=mock_response): + + defineMock.reset_mock() + mock_run.reset_mock() + boot = { + 'kernel': + 'https://www.example.com/download/vmlinuz', + 'initrd': '', + 'cmdline': + 'console=ttyS0 ' + 'ks=http://example.com/f8-i386/os/' + } + + retval = virt.init('test remote vm boot params', + 2, + 1234, + nic=None, + disk=None, + seed=False, + start=False, + boot=boot) + definition = defineMock.call_args_list[0][0][0] + self.assertEqual(cache_dir in definition, True) + + shutil.rmtree(cache_dir) + # Test case creating disks defineMock.reset_mock() mock_run.reset_mock() @@ -1222,6 +1285,20 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual(setxml.find('vcpu').text, '2') self.assertEqual(setvcpus_mock.call_args[0][0], 2) + boot = { + 'kernel': '/root/f8-i386-vmlinuz', + 'initrd': '/root/f8-i386-initrd', + 'cmdline': + 'console=ttyS0 ks=http://example.com/f8-i386/os/' + } + + # Update with boot parameter case + self.assertEqual({ + 'definition': True, + 'disk': {'attached': [], 'detached': []}, + 'interface': {'attached': [], 'detached': []} + }, virt.update('my vm', boot=boot)) + # Update memory case setmem_mock = MagicMock(return_value=0) domain_mock.setMemoryFlags = setmem_mock diff --git a/tests/unit/states/test_virt.py b/tests/unit/states/test_virt.py index 2af5caca1b..109faf5fba 100644 --- a/tests/unit/states/test_virt.py +++ b/tests/unit/states/test_virt.py @@ -249,7 +249,7 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): mem=2048, image='/path/to/img.qcow2'), ret) init_mock.assert_called_with('myvm', cpu=2, mem=2048, image='/path/to/img.qcow2', - os_type=None, arch=None, + os_type=None, arch=None, boot=None, disk=None, disks=None, nic=None, interfaces=None, graphics=None, hypervisor=None, seed=True, install=True, pub_key=None, priv_key=None, @@ -314,6 +314,7 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): graphics=graphics, hypervisor='qemu', seed=False, + boot=None, install=False, pub_key='/path/to/key.pub', priv_key='/path/to/key', @@ -338,6 +339,22 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): 'comment': 'Domain myvm updated, restart to fully apply the changes'}) self.assertDictEqual(virt.running('myvm', update=True, cpu=2), ret) + # Working update case when running with boot params + boot = { + 'kernel': '/root/f8-i386-vmlinuz', + 'initrd': '/root/f8-i386-initrd', + 'cmdline': 'console=ttyS0 ks=http://example.com/f8-i386/os/' + } + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.vm_state': MagicMock(return_value={'myvm': 'running'}), + 'virt.update': MagicMock(return_value={'definition': True, 'cpu': True}) + }): + ret.update({'changes': {'myvm': {'definition': True, 'cpu': True}}, + 'result': True, + 'comment': 'Domain myvm updated, restart to fully apply the changes'}) + self.assertDictEqual(virt.running('myvm', update=True, boot=boot), ret) + # Working update case when stopped with patch.dict(virt.__salt__, { # pylint: disable=no-member 'virt.vm_state': MagicMock(return_value={'myvm': 'stopped'}), -- 2.23.0
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor