diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 27b0129b022c..9aa11948676e 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -694,16 +694,32 @@ def _gen_net_xml(name, def _gen_pool_xml(name, ptype, - target, - source=None): + target=None, + permissions=None, + source_devices=None, + source_dir=None, + source_adapter=None, + source_hosts=None, + source_auth=None, + source_name=None, + source_format=None): ''' Generate the XML string to define a libvirt storage pool ''' + hosts = [host.split(':') for host in source_hosts or []] context = { 'name': name, 'ptype': ptype, - 'target': target, - 'source': source, + 'target': {'path': target, 'permissions': permissions}, + 'source': { + 'devices': source_devices or [], + 'dir': source_dir, + 'adapter': source_adapter, + 'hosts': [{'name': host[0], 'port': host[1] if len(host) > 1 else None} for host in hosts], + 'auth': source_auth, + 'name': source_name, + 'format': source_format + } } fn_ = 'libvirt_pool.jinja' try: @@ -3909,7 +3925,7 @@ def cpu_baseline(full=False, migratable=False, out='libvirt', **kwargs): return cpu.toxml() -def net_define(name, bridge, forward, **kwargs): +def network_define(name, bridge, forward, **kwargs): ''' Create libvirt network. @@ -3928,7 +3944,7 @@ def net_define(name, bridge, forward, **kwargs): .. code-block:: bash - salt '*' virt.net_define network main bridge openvswitch + salt '*' virt.network_define network main bridge openvswitch .. versionadded:: Fluorine ''' @@ -4146,85 +4162,166 @@ def network_set_autostart(name, state='on', **kwargs): conn.close() -def pool_define_build(name, **kwargs): +def pool_define(name, + ptype, + target=None, + permissions=None, + source_devices=None, + source_dir=None, + source_adapter=None, + source_hosts=None, + source_auth=None, + source_name=None, + source_format=None, + transient=False, + start=True, # pylint: disable=redefined-outer-name + **kwargs): ''' Create libvirt pool. :param name: Pool name - :param ptype: Pool type - :param target: Pool path target - :param source: Pool dev source - :param autostart: Pool autostart (default True) + :param ptype: + Pool type. See `libvirt documentation `_ for the + possible values. + :param target: Pool full path target + :param permissions: + Permissions to set on the target folder. This is mostly used for filesystem-based + pool types. See :ref:`pool-define-permissions` for more details on this structure. + :param source_devices: + List of source devices for pools backed by physical devices. (Default: ``None``) + + Each item in the list is a dictionary with ``path`` and optionally ``part_separator`` + keys. The path is the qualified name for iSCSI devices. + + Report to `this libvirt page `_ + for more informations on the use of ``part_separator`` + :param source_dir: + Path to the source directory for pools of type ``dir``, ``netfs`` or ``gluster``. + (Default: ``None``) + :param source_adapter: + SCSI source definition. The value is a dictionary with ``type``, ``name``, ``parent``, + ``managed``, ``parent_wwnn``, ``parent_wwpn``, ``parent_fabric_wwn``, ``wwnn``, ``wwpn`` + and ``parent_address`` keys. + + The ``parent_address`` value is a dictionary with ``unique_id`` and ``address`` keys. + The address represents a PCI address and is itself a dictionary with ``domain``, ``bus``, + ``slot`` and ``function`` properties. + Report to `this libvirt page `_ + for the meaning and possible values of these properties. + :param source_hosts: + List of source for pools backed by storage from remote servers. Each item is the hostname + optionally followed by the port separated by a colon. (Default: ``None``) + :param source_auth: + Source authentication details. (Default: ``None``) + + The value is a dictionary with ``type``, ``username`` and ``secret`` keys. The type + can be one of ``ceph`` for Ceph RBD or ``chap`` for iSCSI sources. + + The ``secret`` value links to a libvirt secret object. It is a dictionary with + ``type`` and ``value`` keys. The type value can be either ``uuid`` or ``usage``. + + Examples: + + .. code-block:: python + + source_auth={ + 'type': 'ceph', + 'username': 'admin', + 'secret': { + 'type': 'uuid', + 'uuid': '2ec115d7-3a88-3ceb-bc12-0ac909a6fd87' + } + } + + .. code-block:: python + + source_auth={ + 'type': 'chap', + 'username': 'myname', + 'secret': { + 'type': 'usage', + 'uuid': 'mycluster_myname' + } + } + + :param source_name: + Identifier of name-based sources. + :param source_format: + String representing the source format. The possible values are depending on the + source type. See `libvirt documentation `_ for + the possible values. :param start: Pool start (default True) + :param transient: + When ``True``, the pool will be automatically undefined after being stopped. + Note that a transient pool will force ``start`` to ``True``. (Default: ``False``) :param connection: libvirt connection URI, overriding defaults :param username: username to connect with, overriding defaults :param password: password to connect with, overriding defaults - CLI Example: + .. _pool-define-permissions: + + .. rubric:: Permissions definition + + The permissions are described by a dictionary containing the following keys: + + mode + The octal representation of the permissions. (Default: `0711`) + + owner + the numeric user ID of the owner. (Default: from the parent folder) + + group + the numeric ID of the group. (Default: from the parent folder) + + label + the SELinux label. (Default: `None`) + + + .. rubric:: CLI Example: + + Local folder pool: + + .. code-block:: bash + + salt '*' virt.pool_define somepool dir target=/srv/mypool \ + permissions="{'mode': '0744' 'ower': 107, 'group': 107 }" + + CIFS backed pool: .. code-block:: bash - salt '*' virt.pool_define base logical base + salt '*' virt.pool_define myshare netfs source_format=cifs \ + source_dir=samba_share source_hosts="['example.com']" target=/mnt/cifs .. versionadded:: Fluorine ''' - exist = False - update = False conn = __get_conn(**kwargs) - ptype = kwargs.pop('ptype', None) - target = kwargs.pop('target', None) - source = kwargs.pop('source', None) - autostart = kwargs.pop('autostart', True) - starting = kwargs.pop('start', True) pool_xml = _gen_pool_xml( name, ptype, target, - source, + permissions=permissions, + source_devices=source_devices, + source_dir=source_dir, + source_adapter=source_adapter, + source_hosts=source_hosts, + source_auth=source_auth, + source_name=source_name, + source_format=source_format ) try: - conn.storagePoolDefineXML(pool_xml) - except libvirtError as err: - log.warning(err) - if err.get_error_code() == libvirt.VIR_ERR_STORAGE_POOL_BUILT or libvirt.VIR_ERR_OPERATION_FAILED: - exist = True + if transient: + pool = conn.storagePoolCreateXML(pool_xml) else: - conn.close() - raise err # a real error we should report upwards - try: - pool = conn.storagePoolLookupByName(name) + pool = conn.storagePoolDefineXML(pool_xml) + if start: + pool.create() except libvirtError as err: - log.warning(err) - conn.close() raise err # a real error we should report upwards - - if pool is None: + finally: conn.close() - return False - - if (starting is True or autostart is True) and pool.isActive() != 1: - if exist is True: - update = True - pool.create() - else: - pool.create(libvirt.VIR_STORAGE_POOL_CREATE_WITH_BUILD) - - if autostart is True and pool.autostart() != 1: - if exist is True: - update = True - pool.setAutostart(int(autostart)) - elif autostart is False and pool.autostart() == 1: - if exist is True: - update = True - pool.setAutostart(int(autostart)) - - conn.close() - - if exist is True: - if update is True: - return (True, 'Pool exist', 'Pool update') - return (True, 'Pool exist') + # libvirt function will raise a libvirtError in case of failure return True diff --git a/salt/states/virt.py b/salt/states/virt.py index 792ba0fe6c32..28eaa585deb7 100644 --- a/salt/states/virt.py +++ b/salt/states/virt.py @@ -27,6 +27,7 @@ import salt.utils.args import salt.utils.files import salt.utils.stringutils +import salt.utils.versions from salt.exceptions import CommandExecutionError # Import 3rd-party libs @@ -144,7 +145,8 @@ def keys(name, basepath='/etc/pki', **kwargs): return ret -def _virt_call(domain, function, section, comment, **kwargs): +def _virt_call(domain, function, section, comment, + connection=None, username=None, password=None, **kwargs): ''' Helper to call the virt functions. Wildcards supported. @@ -160,7 +162,11 @@ def _virt_call(domain, function, section, comment, **kwargs): ignored_domains = list() for targeted_domain in targeted_domains: try: - response = __salt__['virt.{0}'.format(function)](targeted_domain, **kwargs) + response = __salt__['virt.{0}'.format(function)](targeted_domain, + connection=connection, + username=username, + password=password, + **kwargs) if isinstance(response, dict): response = response['name'] changed_domains.append({'domain': targeted_domain, function: response}) @@ -178,54 +184,183 @@ def _virt_call(domain, function, section, comment, **kwargs): return ret -def stopped(name): +def stopped(name, connection=None, username=None, password=None): ''' Stops a VM by shutting it down nicely. .. versionadded:: 2016.3.0 + :param connection: libvirt connection URI, overriding defaults + + .. versionadded:: Fluorine + :param username: username to connect with, overriding defaults + + .. versionadded:: Fluorine + :param password: password to connect with, overriding defaults + + .. versionadded:: Fluorine + .. code-block:: yaml domain_name: virt.stopped ''' - return _virt_call(name, 'shutdown', 'stopped', "Machine has been shut down") + return _virt_call(name, 'shutdown', 'stopped', "Machine has been shut down", + connection=connection, username=username, password=password) -def powered_off(name): +def powered_off(name, connection=None, username=None, password=None): ''' Stops a VM by power off. .. versionadded:: 2016.3.0 + :param connection: libvirt connection URI, overriding defaults + + .. versionadded:: Fluorine + :param username: username to connect with, overriding defaults + + .. versionadded:: Fluorine + :param password: password to connect with, overriding defaults + + .. versionadded:: Fluorine + .. code-block:: yaml domain_name: virt.stopped ''' - return _virt_call(name, 'stop', 'unpowered', 'Machine has been powered off') - - -def running(name, **kwargs): + return _virt_call(name, 'stop', 'unpowered', 'Machine has been powered off', + connection=connection, username=username, password=password) + + +def running(name, + cpu=None, + mem=None, + image=None, + vm_type=None, + disk_profile=None, + disks=None, + nic_profile=None, + interfaces=None, + graphics=None, + seed=True, + install=True, + pub_key=None, + priv_key=None, + connection=None, + username=None, + password=None): ''' Starts an existing guest, or defines and starts a new VM with specified arguments. .. versionadded:: 2016.3.0 + :param name: name of the virtual machine to run + :param cpu: number of CPUs for the virtual machine to create + :param mem: amount of memory in MiB for the new virtual machine + :param image: disk image to use for the first disk of the new VM + + .. deprecated:: Fluorine + :param vm_type: force virtual machine type for the new VM. The default value is taken from + the host capabilities. This could be useful for example to use ``'qemu'`` type instead + of the ``'kvm'`` one. + + .. versionadded:: Fluorine + :param disk_profile: + Name of the disk profile to use for the new virtual machine + + .. versionadded:: Fluorine + :param disks: + List of disk to create for the new virtual machine. + See :ref:`init-disk-def` for more details on the items on this list. + + .. versionadded:: Fluorine + :param nic_profile: + Name of the network interfaces profile to use for the new virtual machine + + .. versionadded:: Fluorine + :param interfaces: + List of network interfaces to create for the new virtual machine. + See :ref:`init-nic-def` for more details on the items on this list. + + .. versionadded:: Fluorine + :param graphics: + Graphics device to create for the new virtual machine. + See :ref:`init-graphics-def` for more details on this dictionary + + .. versionadded:: Fluorine + :param saltenv: + Fileserver environment (Default: ``'base'``). + See :mod:`cp module for more details ` + + .. versionadded:: Fluorine + :param seed: ``True`` to seed the disk image. Only used when the ``image`` parameter is provided. + (Default: ``True``) + + .. versionadded:: Fluorine + :param install: install salt minion if absent (Default: ``True``) + + .. versionadded:: Fluorine + :param pub_key: public key to seed with (Default: ``None``) + + .. versionadded:: Fluorine + :param priv_key: public key to seed with (Default: ``None``) + + .. versionadded:: Fluorine + :param seed_cmd: Salt command to execute to seed the image. (Default: ``'seed.apply'``) + + .. versionadded:: Fluorine + :param connection: libvirt connection URI, overriding defaults + + .. versionadded:: Fluorine + :param username: username to connect with, overriding defaults + + .. versionadded:: Fluorine + :param password: password to connect with, overriding defaults + + .. versionadded:: Fluorine + + .. rubric:: Example States + + Make sure an already-defined virtual machine called ``domain_name`` is running: + .. code-block:: yaml domain_name: virt.running + Do the same, but define the virtual machine if needed: + .. code-block:: yaml domain_name: virt.running: - cpu: 2 - mem: 2048 - - eth0_mac: 00:00:6a:53:00:e3 + - disk_profile: prod + - disks: + - name: system + size: 8192 + overlay_image: True + pool: default + image: /path/to/image.qcow2 + - name: data + size: 16834 + - nic_profile: prod + - interfaces: + - name: eth0 + mac: 01:23:45:67:89:AB + - name: eth1 + type: network + source: admin + - graphics: + - type: spice + listen: + - type: address + address: 192.168.0.125 ''' @@ -235,11 +370,6 @@ def running(name, **kwargs): 'comment': '{0} is running'.format(name) } - kwargs = salt.utils.args.clean_kwargs(**kwargs) - cpu = kwargs.pop('cpu', False) - mem = kwargs.pop('mem', False) - image = kwargs.pop('image', False) - try: try: __salt__['virt.vm_state'](name) @@ -250,8 +380,29 @@ def running(name, **kwargs): else: ret['comment'] = 'Domain {0} exists and is running'.format(name) except CommandExecutionError: - kwargs = salt.utils.args.clean_kwargs(**kwargs) - __salt__['virt.init'](name, cpu=cpu, mem=mem, image=image, **kwargs) + if image: + salt.utils.versions.warn_until( + 'Sodium', + '\'image\' parameter has been deprecated. Rather use the \'disks\' parameter ' + 'to override or define the image. \'image\' will be removed in {version}.' + ) + __salt__['virt.init'](name, + cpu=cpu, + mem=mem, + image=image, + hypervisor=vm_type, + disk=disk_profile, + disks=disks, + nic=nic_profile, + interfaces=interfaces, + graphics=graphics, + seed=seed, + install=install, + pub_key=pub_key, + priv_key=priv_key, + connection=connection, + username=username, + password=password) ret['changes'][name] = 'Domain defined and started' ret['comment'] = 'Domain {0} defined and started'.format(name) except libvirt.libvirtError as err: @@ -262,12 +413,22 @@ def running(name, **kwargs): return ret -def snapshot(name, suffix=None): +def snapshot(name, suffix=None, connection=None, username=None, password=None): ''' Takes a snapshot of a particular VM or by a UNIX-style wildcard. .. versionadded:: 2016.3.0 + :param connection: libvirt connection URI, overriding defaults + + .. versionadded:: Fluorine + :param username: username to connect with, overriding defaults + + .. versionadded:: Fluorine + :param password: password to connect with, overriding defaults + + .. versionadded:: Fluorine + .. code-block:: yaml domain_name: @@ -279,21 +440,32 @@ def snapshot(name, suffix=None): - suffix: periodic ''' - return _virt_call(name, 'snapshot', 'saved', 'Snapshot has been taken', suffix=suffix) + return _virt_call(name, 'snapshot', 'saved', 'Snapshot has been taken', suffix=suffix, + connection=connection, username=username, password=password) # Deprecated states -def rebooted(name): +def rebooted(name, connection=None, username=None, password=None): ''' Reboots VMs .. versionadded:: 2016.3.0 :param name: - :return: + + :param connection: libvirt connection URI, overriding defaults + + .. versionadded:: Fluorine + :param username: username to connect with, overriding defaults + + .. versionadded:: Fluorine + :param password: password to connect with, overriding defaults + + .. versionadded:: Fluorine ''' - return _virt_call(name, 'reboot', 'rebooted', "Machine has been rebooted") + return _virt_call(name, 'reboot', 'rebooted', "Machine has been rebooted", + connection=connection, username=username, password=password) def unpowered(name): @@ -396,10 +568,28 @@ def reverted(name, snapshot=None, cleanup=False): # pylint: disable=redefined-o return ret -def network_define(name, bridge, forward, **kwargs): +def network_running(name, + bridge, + forward, + vport=None, + tag=None, + autostart=True, + connection=None, + username=None, + password=None): ''' Defines and starts a new network with specified arguments. + :param connection: libvirt connection URI, overriding defaults + + .. versionadded:: Fluorine + :param username: username to connect with, overriding defaults + + .. versionadded:: Fluorine + :param password: password to connect with, overriding defaults + + .. versionadded:: Fluorine + .. code-block:: yaml domain_name: @@ -414,42 +604,75 @@ def network_define(name, bridge, forward, **kwargs): - vport: openvswitch - tag: 180 - autostart: True - - start: True ''' ret = {'name': name, 'changes': {}, - 'result': False, + 'result': True, 'comment': '' } - kwargs = salt.utils.args.clean_kwargs(**kwargs) - vport = kwargs.pop('vport', None) - tag = kwargs.pop('tag', None) - autostart = kwargs.pop('autostart', True) - start = kwargs.pop('start', True) - try: - result = __salt__['virt.net_define'](name, bridge, forward, vport, tag=tag, autostart=autostart, start=start) - if result: - ret['changes'][name] = 'Network {0} has been created'.format(name) - ret['result'] = True + info = __salt__['virt.network_info'](name, connection=connection, username=username, password=password) + if info: + if info['active']: + ret['comment'] = 'Network {0} exists and is running'.format(name) + else: + __salt__['virt.network_start'](name, connection=connection, username=username, password=password) + ret['changes'][name] = 'Network started' + ret['comment'] = 'Network {0} started'.format(name) else: - ret['comment'] = 'Network {0} created fail'.format(name) + __salt__['virt.network_define'](name, + bridge, + forward, + vport, + tag=tag, + autostart=autostart, + start=True, + connection=connection, + username=username, + password=password) + ret['changes'][name] = 'Network defined and started' + ret['comment'] = 'Network {0} defined and started'.format(name) except libvirt.libvirtError as err: - if err.get_error_code() == libvirt.VIR_ERR_NETWORK_EXIST or libvirt.VIR_ERR_OPERATION_FAILED: - ret['result'] = True - ret['comment'] = 'The network already exist' - else: - ret['comment'] = err.get_error_message() + ret['result'] = False + ret['comment'] = err.get_error_message() return ret -def pool_define(name, **kwargs): +def pool_running(name, + ptype=None, + target=None, + permissions=None, + source=None, + transient=False, + autostart=True, + connection=None, + username=None, + password=None): ''' Defines and starts a new pool with specified arguments. + .. versionadded:: Fluorine + + :param ptype: libvirt pool type + :param target: full path to the target device or folder. (Default: ``None``) + :param permissions: + target permissions. See :ref:`pool-define-permissions` for more details on this structure. + :param source: + dictionary containing keys matching the ``source_*`` parameters in function + :func:`salt.modules.virt.pool_define`. + :param transient: + when set to ``True``, the pool will be automatically undefined after being stopped. (Default: ``False``) + :param autostart: + Whether to start the pool when booting the host. (Default: ``True``) + :param start: + When ``True``, define and start the pool, otherwise the pool will be left stopped. + :param connection: libvirt connection URI, overriding defaults + :param username: username to connect with, overriding defaults + :param password: password to connect with, overriding defaults + .. code-block:: yaml pool_name: @@ -459,44 +682,73 @@ def pool_define(name, **kwargs): pool_name: virt.pool_define: - - ptype: logical - - target: pool - - source: sda1 + - ptype: netfs + - target: /mnt/cifs + - permissions: + - mode: 0770 + - owner: 1000 + - group: 100 + - source: + - dir: samba_share + - hosts: + one.example.com + two.example.com + - format: cifs - autostart: True - - start: True ''' ret = {'name': name, 'changes': {}, - 'result': False, + 'result': True, 'comment': '' } - kwargs = salt.utils.args.clean_kwargs(**kwargs) - ptype = kwargs.pop('ptype', None) - target = kwargs.pop('target', None) - source = kwargs.pop('source', None) - autostart = kwargs.pop('autostart', True) - start = kwargs.pop('start', True) - try: - result = __salt__['virt.pool_define_build'](name, ptype=ptype, target=target, - source=source, autostart=autostart, start=start) - if result: - if 'Pool exist' in result: - if 'Pool update' in result: - ret['changes'][name] = 'Pool {0} has been updated'.format(name) - else: - ret['comment'] = 'Pool {0} already exist'.format(name) + info = __salt__['virt.pool_info'](name, connection=connection, username=username, password=password) + if info: + if info['state'] == 'running': + ret['comment'] = 'Pool {0} exists and is running'.format(name) else: - ret['changes'][name] = 'Pool {0} has been created'.format(name) - ret['result'] = True + __salt__['virt.pool_start'](name, connection=connection, username=username, password=password) + ret['changes'][name] = 'Pool started' + ret['comment'] = 'Pool {0} started'.format(name) else: - ret['comment'] = 'Pool {0} created fail'.format(name) + __salt__['virt.pool_define'](name, + ptype=ptype, + target=target, + permissions=permissions, + source_devices=(source or {}).get('devices', None), + source_dir=(source or {}).get('dir', None), + source_adapter=(source or {}).get('adapter', None), + source_hosts=(source or {}).get('hosts', None), + source_auth=(source or {}).get('auth', None), + source_name=(source or {}).get('name', None), + source_format=(source or {}).get('format', None), + transient=transient, + start=True, + connection=connection, + username=username, + password=password) + if autostart: + __salt__['virt.pool_set_autostart'](name, + state='on' if autostart else 'off', + connection=connection, + username=username, + password=password) + + __salt__['virt.pool_build'](name, + connection=connection, + username=username, + password=password) + + __salt__['virt.pool_start'](name, + connection=connection, + username=username, + password=password) + ret['changes'][name] = 'Pool defined and started' + ret['comment'] = 'Pool {0} defined and started'.format(name) except libvirt.libvirtError as err: - if err.get_error_code() == libvirt.VIR_ERR_STORAGE_POOL_BUILT or libvirt.VIR_ERR_OPERATION_FAILED: - ret['result'] = True - ret['comment'] = 'The pool already exist' ret['comment'] = err.get_error_message() + ret['result'] = False return ret diff --git a/salt/templates/virt/libvirt_pool.jinja b/salt/templates/virt/libvirt_pool.jinja index b662d1840583..58c82f717788 100644 --- a/salt/templates/virt/libvirt_pool.jinja +++ b/salt/templates/virt/libvirt_pool.jinja @@ -1,9 +1,64 @@ {{ name }} + {% if target.path or target.permissions %} - /dev/{{ target }} - {% if source != None %} + {% if target.path %} + {{ target.path }} + {% endif %} + {% if target.permissions %} + + {% for key in ['mode', 'owner', 'group', 'label'] %} + {% if key in target.permissions %} + <{{ key }}>{{ target.permissions[key] }} + {% endif %} + {% endfor %} + + {% endif %} + + {% endif %} + {% if source %} - - {% endif %} - \ No newline at end of file + {% if ptype in ['fs', 'logical', 'disk', 'iscsi', 'zfs', 'vstorage'] %} + {% for device in source.devices %} + + {% endfor %} + {% elif ptype in ['dir', 'netfs', 'gluster'] %} + + {% elif ptype == 'scsi' %} + + {% if 'parent_address' in source.adapter %} + +
+ + {% endif %} + + {% endif %} + {% if ptype in ['netfs', 'iscsi', 'rbd', 'sheepdog', 'gluster'] and source.hosts %} + {% for host in source.hosts %} + + {% endfor %} + {% endif %} + {% if ptype in ['iscsi', 'rbd'] and source.auth %} + + + + {% endif %} + {% if ptype in ['logical', 'rbd', 'sheepdog', 'gluster'] and source.name %} + {{ source.name }} + {% endif %} + {% if ptype in ['fs', 'netfs', 'logical', 'disk'] and source.format %} + + {% endif %} + + {% endif %} + diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py index e01fd873c361..cde0ce9f4886 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -1581,7 +1581,7 @@ def test_pool(self): ''' Test virt._gen_pool_xml() ''' - xml_data = virt._gen_pool_xml('pool', 'logical', 'base') + xml_data = virt._gen_pool_xml('pool', 'logical', '/dev/base') root = ET.fromstring(xml_data) self.assertEqual(root.find('name').text, 'pool') self.assertEqual(root.attrib['type'], 'logical') @@ -1591,13 +1591,120 @@ def test_pool_with_source(self): ''' Test virt._gen_pool_xml() with a source device ''' - xml_data = virt._gen_pool_xml('pool', 'logical', 'base', 'sda') + xml_data = virt._gen_pool_xml('pool', 'logical', '/dev/base', source_devices=[{'path': '/dev/sda'}]) root = ET.fromstring(xml_data) self.assertEqual(root.find('name').text, 'pool') self.assertEqual(root.attrib['type'], 'logical') self.assertEqual(root.find('target/path').text, '/dev/base') self.assertEqual(root.find('source/device').attrib['path'], '/dev/sda') + def test_pool_with_scsi(self): + ''' + Test virt._gen_pool_xml() with a SCSI source + ''' + xml_data = virt._gen_pool_xml('pool', + 'scsi', + '/dev/disk/by-path', + source_devices=[{'path': '/dev/sda'}], + source_adapter={ + 'type': 'scsi_host', + 'parent_address': { + 'unique_id': 5, + 'address': { + 'domain': '0x0000', + 'bus': '0x00', + 'slot': '0x1f', + 'function': '0x2' + } + } + }, + source_name='srcname') + root = ET.fromstring(xml_data) + self.assertEqual(root.find('name').text, 'pool') + self.assertEqual(root.attrib['type'], 'scsi') + self.assertEqual(root.find('target/path').text, '/dev/disk/by-path') + self.assertEqual(root.find('source/device'), None) + self.assertEqual(root.find('source/name'), None) + self.assertEqual(root.find('source/adapter').attrib['type'], 'scsi_host') + self.assertEqual(root.find('source/adapter/parentaddr').attrib['unique_id'], '5') + self.assertEqual(root.find('source/adapter/parentaddr/address').attrib['domain'], '0x0000') + self.assertEqual(root.find('source/adapter/parentaddr/address').attrib['bus'], '0x00') + self.assertEqual(root.find('source/adapter/parentaddr/address').attrib['slot'], '0x1f') + self.assertEqual(root.find('source/adapter/parentaddr/address').attrib['function'], '0x2') + + def test_pool_with_rbd(self): + ''' + Test virt._gen_pool_xml() with an RBD source + ''' + xml_data = virt._gen_pool_xml('pool', + 'rbd', + source_devices=[{'path': '/dev/sda'}], + source_hosts=['1.2.3.4', 'my.ceph.monitor:69'], + source_auth={ + 'type': 'ceph', + 'username': 'admin', + 'secret': { + 'type': 'uuid', + 'value': 'someuuid' + } + }, + source_name='srcname', + source_adapter={'type': 'scsi_host', 'name': 'host0'}, + source_dir='/some/dir', + source_format='fmt') + root = ET.fromstring(xml_data) + self.assertEqual(root.find('name').text, 'pool') + self.assertEqual(root.attrib['type'], 'rbd') + self.assertEqual(root.find('target'), None) + self.assertEqual(root.find('source/device'), None) + self.assertEqual(root.find('source/name').text, 'srcname') + self.assertEqual(root.find('source/adapter'), None) + self.assertEqual(root.find('source/dir'), None) + self.assertEqual(root.find('source/format'), None) + self.assertEqual(root.findall('source/host')[0].attrib['name'], '1.2.3.4') + self.assertTrue('port' not in root.findall('source/host')[0].attrib) + self.assertEqual(root.findall('source/host')[1].attrib['name'], 'my.ceph.monitor') + self.assertEqual(root.findall('source/host')[1].attrib['port'], '69') + self.assertEqual(root.find('source/auth').attrib['type'], 'ceph') + self.assertEqual(root.find('source/auth').attrib['username'], 'admin') + self.assertEqual(root.find('source/auth/secret').attrib['type'], 'uuid') + self.assertEqual(root.find('source/auth/secret').attrib['uuid'], 'someuuid') + + def test_pool_with_netfs(self): + ''' + Test virt._gen_pool_xml() with a netfs source + ''' + xml_data = virt._gen_pool_xml('pool', + 'netfs', + target='/path/to/target', + permissions={ + 'mode': '0770', + 'owner': 1000, + 'group': 100, + 'label': 'seclabel' + }, + source_devices=[{'path': '/dev/sda'}], + source_hosts=['nfs.host'], + source_name='srcname', + source_adapter={'type': 'scsi_host', 'name': 'host0'}, + source_dir='/some/dir', + source_format='nfs') + root = ET.fromstring(xml_data) + self.assertEqual(root.find('name').text, 'pool') + self.assertEqual(root.attrib['type'], 'netfs') + self.assertEqual(root.find('target/path').text, '/path/to/target') + self.assertEqual(root.find('target/permissions/mode').text, '0770') + self.assertEqual(root.find('target/permissions/owner').text, '1000') + self.assertEqual(root.find('target/permissions/group').text, '100') + self.assertEqual(root.find('target/permissions/label').text, 'seclabel') + self.assertEqual(root.find('source/device'), None) + self.assertEqual(root.find('source/name'), None) + self.assertEqual(root.find('source/adapter'), None) + self.assertEqual(root.find('source/dir').attrib['path'], '/some/dir') + self.assertEqual(root.find('source/format').attrib['type'], 'nfs') + self.assertEqual(root.find('source/host').attrib['name'], 'nfs.host') + self.assertEqual(root.find('source/auth'), None) + def test_list_pools(self): ''' Test virt.list_pools() diff --git a/tests/unit/states/test_libvirt.py b/tests/unit/states/test_libvirt.py index c97f4aad4afc..3cef512211d9 100644 --- a/tests/unit/states/test_libvirt.py +++ b/tests/unit/states/test_libvirt.py @@ -23,6 +23,10 @@ # Import Salt Libs import salt.states.virt as virt import salt.utils.files +from salt.exceptions import CommandExecutionError + +# Import 3rd-party libs +from salt.ext import six class LibvirtMock(MagicMock): # pylint: disable=too-many-ancestors @@ -35,6 +39,12 @@ class libvirtError(Exception): # pylint: disable=invalid-name libvirt error mockup ''' + def get_error_message(self): + ''' + Fake function return error message + ''' + return six.text_type(self) + @skipIf(NO_MOCK, NO_MOCK_REASON) class LibvirtTestCase(TestCase, LoaderModuleMockMixin): @@ -230,9 +240,421 @@ def test_running(self): 'comment': 'Domain myvm started'}) self.assertDictEqual(virt.running('myvm'), ret) + init_mock = MagicMock(return_value=True) + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.vm_state': MagicMock(side_effect=CommandExecutionError('not found')), + 'virt.init': init_mock, + 'virt.start': MagicMock(return_value=0) + }): + ret.update({'changes': {'myvm': 'Domain defined and started'}, + 'comment': 'Domain myvm defined and started'}) + self.assertDictEqual(virt.running('myvm', + cpu=2, + mem=2048, + image='/path/to/img.qcow2'), ret) + init_mock.assert_called_with('myvm', cpu=2, mem=2048, image='/path/to/img.qcow2', + disk=None, disks=None, nic=None, interfaces=None, + graphics=None, hypervisor=None, + seed=True, install=True, pub_key=None, priv_key=None, + connection=None, username=None, password=None) + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.vm_state': MagicMock(side_effect=CommandExecutionError('not found')), + 'virt.init': init_mock, + 'virt.start': MagicMock(return_value=0) + }): + ret.update({'changes': {'myvm': 'Domain defined and started'}, + 'comment': 'Domain myvm defined and started'}) + disks = [{ + 'name': 'system', + 'size': 8192, + 'overlay_image': True, + 'pool': 'default', + 'image': '/path/to/image.qcow2' + }, + { + 'name': 'data', + 'size': 16834 + }] + ifaces = [{ + 'name': 'eth0', + 'mac': '01:23:45:67:89:AB' + }, + { + 'name': 'eth1', + 'type': 'network', + 'source': 'admin' + }] + graphics = {'type': 'spice', 'listen': {'type': 'address', 'address': '192.168.0.1'}} + self.assertDictEqual(virt.running('myvm', + cpu=2, + mem=2048, + vm_type='qemu', + disk_profile='prod', + disks=disks, + nic_profile='prod', + interfaces=ifaces, + graphics=graphics, + seed=False, + install=False, + pub_key='/path/to/key.pub', + priv_key='/path/to/key', + connection='someconnection', + username='libvirtuser', + password='supersecret'), ret) + init_mock.assert_called_with('myvm', + cpu=2, + mem=2048, + image=None, + disk='prod', + disks=disks, + nic='prod', + interfaces=ifaces, + graphics=graphics, + hypervisor='qemu', + seed=False, + install=False, + pub_key='/path/to/key.pub', + priv_key='/path/to/key', + connection='someconnection', + username='libvirtuser', + password='supersecret') + with patch.dict(virt.__salt__, { # pylint: disable=no-member 'virt.vm_state': MagicMock(return_value='stopped'), 'virt.start': MagicMock(side_effect=[self.mock_libvirt.libvirtError('libvirt error msg')]) }): ret.update({'changes': {}, 'result': False, 'comment': 'libvirt error msg'}) self.assertDictEqual(virt.running('myvm'), ret) + + def test_stopped(self): + ''' + stopped state test cases. + ''' + ret = {'name': 'myvm', + 'changes': {}, + 'result': True} + + shutdown_mock = MagicMock(return_value=True) + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.shutdown': shutdown_mock + }): + ret.update({'changes': { + 'stopped': [{'domain': 'myvm', 'shutdown': True}] + }, + 'comment': 'Machine has been shut down'}) + self.assertDictEqual(virt.stopped('myvm'), ret) + shutdown_mock.assert_called_with('myvm', connection=None, username=None, password=None) + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.shutdown': shutdown_mock, + }): + self.assertDictEqual(virt.stopped('myvm', + connection='myconnection', + username='user', + password='secret'), ret) + shutdown_mock.assert_called_with('myvm', connection='myconnection', username='user', password='secret') + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.shutdown': MagicMock(side_effect=self.mock_libvirt.libvirtError('Some error')) + }): + ret.update({'changes': {'ignored': [{'domain': 'myvm', 'issue': 'Some error'}]}, + 'result': False, + 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.stopped('myvm'), ret) + + with patch.dict(virt.__salt__, {'virt.list_domains': MagicMock(return_value=[])}): # pylint: disable=no-member + ret.update({'changes': {}, 'result': False, 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.stopped('myvm'), ret) + + def test_powered_off(self): + ''' + powered_off state test cases. + ''' + ret = {'name': 'myvm', + 'changes': {}, + 'result': True} + + stop_mock = MagicMock(return_value=True) + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.stop': stop_mock + }): + ret.update({'changes': { + 'unpowered': [{'domain': 'myvm', 'stop': True}] + }, + 'comment': 'Machine has been powered off'}) + self.assertDictEqual(virt.powered_off('myvm'), ret) + stop_mock.assert_called_with('myvm', connection=None, username=None, password=None) + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.stop': stop_mock, + }): + self.assertDictEqual(virt.powered_off('myvm', + connection='myconnection', + username='user', + password='secret'), ret) + stop_mock.assert_called_with('myvm', connection='myconnection', username='user', password='secret') + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.stop': MagicMock(side_effect=self.mock_libvirt.libvirtError('Some error')) + }): + ret.update({'changes': {'ignored': [{'domain': 'myvm', 'issue': 'Some error'}]}, + 'result': False, + 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.powered_off('myvm'), ret) + + with patch.dict(virt.__salt__, {'virt.list_domains': MagicMock(return_value=[])}): # pylint: disable=no-member + ret.update({'changes': {}, 'result': False, 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.powered_off('myvm'), ret) + + def test_snapshot(self): + ''' + snapshot state test cases. + ''' + ret = {'name': 'myvm', + 'changes': {}, + 'result': True} + + snapshot_mock = MagicMock(return_value=True) + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.snapshot': snapshot_mock + }): + ret.update({'changes': { + 'saved': [{'domain': 'myvm', 'snapshot': True}] + }, + 'comment': 'Snapshot has been taken'}) + self.assertDictEqual(virt.snapshot('myvm'), ret) + snapshot_mock.assert_called_with('myvm', suffix=None, connection=None, username=None, password=None) + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.snapshot': snapshot_mock, + }): + self.assertDictEqual(virt.snapshot('myvm', + suffix='snap', + connection='myconnection', + username='user', + password='secret'), ret) + snapshot_mock.assert_called_with('myvm', + suffix='snap', + connection='myconnection', + username='user', + password='secret') + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.snapshot': MagicMock(side_effect=self.mock_libvirt.libvirtError('Some error')) + }): + ret.update({'changes': {'ignored': [{'domain': 'myvm', 'issue': 'Some error'}]}, + 'result': False, + 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.snapshot('myvm'), ret) + + with patch.dict(virt.__salt__, {'virt.list_domains': MagicMock(return_value=[])}): # pylint: disable=no-member + ret.update({'changes': {}, 'result': False, 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.snapshot('myvm'), ret) + + def test_rebooted(self): + ''' + rebooted state test cases. + ''' + ret = {'name': 'myvm', + 'changes': {}, + 'result': True} + + reboot_mock = MagicMock(return_value=True) + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.reboot': reboot_mock + }): + ret.update({'changes': { + 'rebooted': [{'domain': 'myvm', 'reboot': True}] + }, + 'comment': 'Machine has been rebooted'}) + self.assertDictEqual(virt.rebooted('myvm'), ret) + reboot_mock.assert_called_with('myvm', connection=None, username=None, password=None) + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.reboot': reboot_mock, + }): + self.assertDictEqual(virt.rebooted('myvm', + connection='myconnection', + username='user', + password='secret'), ret) + reboot_mock.assert_called_with('myvm', + connection='myconnection', + username='user', + password='secret') + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), + 'virt.reboot': MagicMock(side_effect=self.mock_libvirt.libvirtError('Some error')) + }): + ret.update({'changes': {'ignored': [{'domain': 'myvm', 'issue': 'Some error'}]}, + 'result': False, + 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.rebooted('myvm'), ret) + + with patch.dict(virt.__salt__, {'virt.list_domains': MagicMock(return_value=[])}): # pylint: disable=no-member + ret.update({'changes': {}, 'result': False, 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.rebooted('myvm'), ret) + + def test_network_running(self): + ''' + network_running state test cases. + ''' + ret = {'name': 'mynet', 'changes': {}, 'result': True, 'comment': ''} + define_mock = MagicMock(return_value=True) + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.network_info': MagicMock(return_value={}), + 'virt.network_define': define_mock + }): + ret.update({'changes': {'mynet': 'Network defined and started'}, + 'comment': 'Network mynet defined and started'}) + self.assertDictEqual(virt.network_running('mynet', + 'br2', + 'bridge', + vport='openvswitch', + tag=180, + autostart=False, + connection='myconnection', + username='user', + password='secret'), ret) + define_mock.assert_called_with('mynet', + 'br2', + 'bridge', + 'openvswitch', + tag=180, + autostart=False, + start=True, + connection='myconnection', + username='user', + password='secret') + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.network_info': MagicMock(return_value={'active': True}), + 'virt.network_define': define_mock, + }): + ret.update({'changes': {}, 'comment': 'Network mynet exists and is running'}) + self.assertDictEqual(virt.network_running('mynet', 'br2', 'bridge'), ret) + + start_mock = MagicMock(return_value=True) + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.network_info': MagicMock(return_value={'active': False}), + 'virt.network_start': start_mock, + 'virt.network_define': define_mock, + }): + ret.update({'changes': {'mynet': 'Network started'}, 'comment': 'Network mynet started'}) + self.assertDictEqual(virt.network_running('mynet', + 'br2', + 'bridge', + connection='myconnection', + username='user', + password='secret'), ret) + start_mock.assert_called_with('mynet', connection='myconnection', username='user', password='secret') + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.network_info': MagicMock(return_value={}), + 'virt.network_define': MagicMock(side_effect=self.mock_libvirt.libvirtError('Some error')) + }): + ret.update({'changes': {}, 'comment': 'Some error', 'result': False}) + self.assertDictEqual(virt.network_running('mynet', 'br2', 'bridge'), ret) + + def test_pool_running(self): + ''' + pool_running state test cases. + ''' + ret = {'name': 'mypool', 'changes': {}, 'result': True, 'comment': ''} + mocks = {mock: MagicMock(return_value=True) for mock in ['define', 'autostart', 'build', 'start']} + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={}), + 'virt.pool_define': mocks['define'], + 'virt.pool_build': mocks['build'], + 'virt.pool_start': mocks['start'], + 'virt.pool_set_autostart': mocks['autostart'] + }): + ret.update({'changes': {'mypool': 'Pool defined and started'}, + 'comment': 'Pool mypool defined and started'}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + permissions={'mode': '0770', + 'owner': 1000, + 'group': 100, + 'label': 'seclabel'}, + source={'devices': [{'path': '/dev/sda'}]}, + transient=True, + autostart=True, + connection='myconnection', + username='user', + password='secret'), ret) + mocks['define'].assert_called_with('mypool', + ptype='logical', + target='/dev/base', + permissions={'mode': '0770', + 'owner': 1000, + 'group': 100, + 'label': 'seclabel'}, + source_devices=[{'path': '/dev/sda'}], + source_dir=None, + source_adapter=None, + source_hosts=None, + source_auth=None, + source_name=None, + source_format=None, + transient=True, + start=True, + connection='myconnection', + username='user', + password='secret') + mocks['autostart'].assert_called_with('mypool', + state='on', + connection='myconnection', + username='user', + password='secret') + mocks['build'].assert_called_with('mypool', + connection='myconnection', + username='user', + password='secret') + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={'state': 'running'}), + }): + ret.update({'changes': {}, 'comment': 'Pool mypool exists and is running'}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + source={'devices': [{'path': '/dev/sda'}]}), ret) + + for mock in mocks: + mocks[mock].reset_mock() + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={'state': 'stopped'}), + 'virt.pool_build': mocks['build'], + 'virt.pool_start': mocks['start'] + }): + ret.update({'changes': {'mypool': 'Pool started'}, 'comment': 'Pool mypool started'}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + source={'devices': [{'path': '/dev/sda'}]}), ret) + mocks['start'].assert_called_with('mypool', connection=None, username=None, password=None) + mocks['build'].assert_not_called() + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={}), + 'virt.pool_define': MagicMock(side_effect=self.mock_libvirt.libvirtError('Some error')) + }): + ret.update({'changes': {}, 'comment': 'Some error', 'result': False}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + source={'devices': [{'path': '/dev/sda'}]}), ret)