From 563440b6ecd2a0e7a618059ba2ace34ecbfc4d3e Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Wed, 6 Mar 2019 17:07:44 +0100 Subject: [PATCH] Backport from develop features required by yomi * Add root and no_recommends parameters in the public interface for Zypper and RPM (lowpkg) https://github.com/saltstack/salt/pull/50125 * Add root parameter to useradd, shadow and groupadd https://github.com/saltstack/salt/pull/50175 * cmd: Add root parameter for wait and run states https://github.com/saltstack/salt/pull/50302 * systemd: add optional root parameter https://github.com/saltstack/salt/pull/50380 * Add new chroot module https://github.com/openSUSE/salt/pull/50418 * Add new module freezer https://github.com/saltstack/salt/pull/50452 * btrfs: add all subvolume commands https://github.com/saltstack/salt/pull/50541 * btrfs: add new btrfs state https://github.com/saltstack/salt/pull/50635 * zypper: demote log from error to warning https://github.com/saltstack/salt/pull/50671 * blkid: add search by token https://github.com/saltstack/salt/pull/50706 * mount: add fstab_{present,absent} states https://github.com/saltstack/salt/pull/50725 * btrfs: add option to not set subvolumes as default https://github.com/saltstack/salt/pull/50801 * Add disk_set and disk_toggle functions, and update valid partition flags https://github.com/saltstack/salt/pull/50834 * disk: support setting FAT size for format_ https://github.com/saltstack/salt/pull/51074 * parted: fix set_ valid flags comment. https://github.com/saltstack/salt/pull/51704 * grains/core: ignore HOST_NOT_FOUND errno in fqdns() https://github.com/saltstack/salt/pull/51706 * cmdmod: add 'binds' parameter in run_chroot https://github.com/saltstack/salt/pull/51871 * mount: fix extra -t parameter https://github.com/saltstack/salt/pull/51905 * lvm: be quiet when a pv, lv or vg is not expected https://github.com/saltstack/salt/pull/51929 * linux_lvm: clean error in pvcreate and pvremove https://github.com/saltstack/salt/pull/51954 * blockdev: hide blkid errors when are expected https://github.com/saltstack/salt/pull/51956 * partially unify public functions signature for pkg and lowpkg https://github.com/saltstack/salt/pull/51973 * extmods: add utils directories in sys.path https://github.com/saltstack/salt/pull/52001 --- salt/grains/core.py | 6 +- salt/modules/aixpkg.py | 2 +- salt/modules/apkpkg.py | 12 +- salt/modules/aptpkg.py | 14 +- salt/modules/btrfs.py | 496 +++++++++++++++- salt/modules/chroot.py | 165 ++++++ salt/modules/cmdmod.py | 44 +- salt/modules/disk.py | 26 +- salt/modules/dpkg_lowpkg.py | 6 +- salt/modules/ebuildpkg.py | 8 +- salt/modules/freebsdpkg.py | 6 +- salt/modules/freezer.py | 294 ++++++++++ salt/modules/groupadd.py | 177 +++++- salt/modules/linux_lvm.py | 36 +- salt/modules/mac_brew_pkg.py | 8 +- salt/modules/mac_portspkg.py | 6 +- salt/modules/mount.py | 13 +- salt/modules/openbsdpkg.py | 2 +- salt/modules/pacmanpkg.py | 10 +- salt/modules/parted_partition.py | 91 ++- salt/modules/pkgin.py | 8 +- salt/modules/pkgng.py | 4 +- salt/modules/rpm_lowpkg.py | 101 +++- salt/modules/shadow.py | 300 +++++++--- salt/modules/solarisipspkg.py | 4 +- salt/modules/solarispkg.py | 2 +- salt/modules/systemd_service.py | 209 ++++--- salt/modules/useradd.py | 390 ++++++++++--- salt/modules/xbpspkg.py | 12 +- salt/modules/yumpkg.py | 34 +- salt/modules/zypperpkg.py | 382 +++++++++---- salt/states/blockdev.py | 3 +- salt/states/btrfs.py | 258 +++++++++ salt/states/cmd.py | 18 +- salt/states/lvm.py | 16 +- salt/states/mount.py | 292 ++++++++++ salt/states/pkg.py | 28 +- salt/states/pkgrepo.py | 14 +- salt/utils/extmods.py | 7 + tests/unit/modules/test_btrfs.py | 370 +++++++++++- tests/unit/modules/test_chroot.py | 184 ++++++ tests/unit/modules/test_freezer.py | 274 +++++++++ tests/unit/modules/test_groupadd.py | 16 +- tests/unit/modules/test_mount.py | 36 ++ tests/unit/modules/test_parted_partition.py | 17 + tests/unit/modules/test_rpm_lowpkg.py | 92 ++- tests/unit/modules/test_systemd_service.py | 4 +- tests/unit/modules/test_useradd.py | 5 +- tests/unit/modules/test_zypperpkg.py | 45 +- tests/unit/states/test_btrfs.py | 512 +++++++++++++++++ tests/unit/states/test_mount.py | 593 ++++++++++++++++++++ tests/unit/states/test_pkg.py | 7 +- 52 files changed, 5081 insertions(+), 578 deletions(-) create mode 100644 salt/modules/chroot.py create mode 100644 salt/modules/freezer.py create mode 100644 salt/states/btrfs.py create mode 100644 tests/unit/modules/test_chroot.py create mode 100644 tests/unit/modules/test_freezer.py create mode 100644 tests/unit/states/test_btrfs.py diff --git a/salt/grains/core.py b/salt/grains/core.py index fec7b204bc4..8e8ebca2548 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -108,6 +108,10 @@ def linux_distribution(**kwargs): _INTERFACES = {} +# Possible value for h_errno defined in netdb.h +HOST_NOT_FOUND = 1 +NO_DATA = 4 + def _windows_cpudata(): ''' @@ -2208,7 +2212,7 @@ def _lookup_fqdn(ip): name, aliaslist, addresslist = socket.gethostbyaddr(ip) return [socket.getfqdn(name)] + [als for als in aliaslist if salt.utils.network.is_fqdn(als)] except socket.herror as err: - if err.errno == 0: + if err.errno in (0, HOST_NOT_FOUND, NO_DATA): # No FQDN for this IP address, so we don't need to know this all the time. log.debug("Unable to resolve address %s: %s", ip, err) else: diff --git a/salt/modules/aixpkg.py b/salt/modules/aixpkg.py index 4f9852b504d..d35946f397f 100644 --- a/salt/modules/aixpkg.py +++ b/salt/modules/aixpkg.py @@ -400,7 +400,7 @@ def latest_version(*names, **kwargs): available_version = salt.utils.functools.alias_function(latest_version, 'available_version') -def upgrade_available(name): +def upgrade_available(name, **kwargs): ''' Check whether or not an upgrade is available for a given package diff --git a/salt/modules/apkpkg.py b/salt/modules/apkpkg.py index 2e9a2a952e7..4f84642e024 100644 --- a/salt/modules/apkpkg.py +++ b/salt/modules/apkpkg.py @@ -83,7 +83,7 @@ def version(*names, **kwargs): return __salt__['pkg_resource.version'](*names, **kwargs) -def refresh_db(): +def refresh_db(**kwargs): ''' Updates the package list @@ -425,7 +425,7 @@ def remove(name=None, pkgs=None, purge=False, **kwargs): # pylint: disable=unus return ret -def upgrade(name=None, pkgs=None, refresh=True): +def upgrade(name=None, pkgs=None, refresh=True, **kwargs): ''' Upgrades all packages via ``apk upgrade`` or a specific package if name or pkgs is specified. Name is ignored if pkgs is specified @@ -485,7 +485,7 @@ def upgrade(name=None, pkgs=None, refresh=True): return ret -def list_upgrades(refresh=True): +def list_upgrades(refresh=True, **kwargs): ''' List all available package upgrades. @@ -524,7 +524,7 @@ def list_upgrades(refresh=True): return ret -def file_list(*packages): +def file_list(*packages, **kwargs): ''' List the files that belong to a package. Not specifying any packages will return a list of _every_ file on the system's package database (not @@ -541,7 +541,7 @@ def file_list(*packages): return file_dict(*packages) -def file_dict(*packages): +def file_dict(*packages, **kwargs): ''' List the files that belong to a package, grouped by package. Not specifying any packages will return a list of _every_ file on the system's @@ -580,7 +580,7 @@ def file_dict(*packages): return {'errors': errors, 'packages': ret} -def owner(*paths): +def owner(*paths, **kwargs): ''' Return the name of the package that owns the file. Multiple file paths can be passed. Like :mod:`pkg.version pkg2. Return None if there was a problem @@ -1633,7 +1633,7 @@ def _skip_source(source): return False -def list_repos(): +def list_repos(**kwargs): ''' Lists all repos in the sources.list (and sources.lists.d) files @@ -2400,7 +2400,7 @@ def mod_repo(repo, saltenv='base', **kwargs): } -def file_list(*packages): +def file_list(*packages, **kwargs): ''' List the files that belong to a package. Not specifying any packages will return a list of _every_ file on the system's package database (not @@ -2417,7 +2417,7 @@ def file_list(*packages): return __salt__['lowpkg.file_list'](*packages) -def file_dict(*packages): +def file_dict(*packages, **kwargs): ''' List the files that belong to a package, grouped by package. Not specifying any packages will return a list of _every_ file on the system's @@ -2701,7 +2701,7 @@ def _resolve_deps(name, pkgs, **kwargs): return -def owner(*paths): +def owner(*paths, **kwargs): ''' .. versionadded:: 2014.7.0 diff --git a/salt/modules/btrfs.py b/salt/modules/btrfs.py index 36bfaeb12e7..6fd2ac58a4c 100644 --- a/salt/modules/btrfs.py +++ b/salt/modules/btrfs.py @@ -20,11 +20,11 @@ # Import Python libs from __future__ import absolute_import, print_function, unicode_literals +import itertools import os import re import uuid - # Import Salt libs import salt.utils.fsutils import salt.utils.platform @@ -673,3 +673,497 @@ def properties(obj, type=None, set=None): ret[prop]['value'] = value and value.split("=")[-1] or "N/A" return ret + + +def subvolume_exists(path): + ''' + Check if a subvolume is present in the filesystem. + + path + Mount point for the subvolume (full path) + + CLI Example: + + .. code-block:: bash + + salt '*' btrfs.subvolume_exists /mnt/var + + ''' + cmd = ['btrfs', 'subvolume', 'show', path] + return __salt__['cmd.retcode'](cmd, ignore_retcode=True) == 0 + + +def subvolume_create(name, dest=None, qgroupids=None): + ''' + Create subvolume `name` in `dest`. + + Return True if the subvolume is created, False is the subvolume is + already there. + + name + Name of the new subvolume + + dest + If not given, the subvolume will be created in the current + directory, if given will be in /dest/name + + qgroupids + Add the newly created subcolume to a qgroup. This parameter + is a list + + CLI Example: + + .. code-block:: bash + + salt '*' btrfs.subvolume_create var + salt '*' btrfs.subvolume_create var dest=/mnt + salt '*' btrfs.subvolume_create var qgroupids='[200]' + + ''' + if qgroupids and type(qgroupids) is not list: + raise CommandExecutionError('Qgroupids parameter must be a list') + + if dest: + name = os.path.join(dest, name) + + # If the subvolume is there, we are done + if subvolume_exists(name): + return False + + cmd = ['btrfs', 'subvolume', 'create'] + if type(qgroupids) is list: + cmd.append('-i') + cmd.extend(qgroupids) + cmd.append(name) + + res = __salt__['cmd.run_all'](cmd) + salt.utils.fsutils._verify_run(res) + return True + + +def subvolume_delete(name=None, names=None, commit=None): + ''' + Delete the subvolume(s) from the filesystem + + The user can remove one single subvolume (name) or multiple of + then at the same time (names). One of the two parameters needs to + specified. + + Please, refer to the documentation to understand the implication + on the transactions, and when the subvolume is really deleted. + + Return True if the subvolume is deleted, False is the subvolume + was already missing. + + name + Name of the subvolume to remove + + names + List of names of subvolumes to remove + + commit + * 'after': Wait for transaction commit at the end + * 'each': Wait for transaction commit after each delete + + CLI Example: + + .. code-block:: bash + + salt '*' btrfs.subvolume_delete /var/volumes/tmp + salt '*' btrfs.subvolume_delete /var/volumes/tmp commit=after + + ''' + if not name and not (names and type(names) is list): + raise CommandExecutionError('Provide a value for the name parameter') + + if commit and commit not in ('after', 'each'): + raise CommandExecutionError('Value for commit not recognized') + + # Filter the names and take the ones that are still there + names = [n for n in itertools.chain([name], names or []) + if n and subvolume_exists(n)] + + # If the subvolumes are gone, we are done + if not names: + return False + + cmd = ['btrfs', 'subvolume', 'delete'] + if commit == 'after': + cmd.append('--commit-after') + elif commit == 'each': + cmd.append('--commit-each') + cmd.extend(names) + + res = __salt__['cmd.run_all'](cmd) + salt.utils.fsutils._verify_run(res) + return True + + +def subvolume_find_new(name, last_gen): + ''' + List the recently modified files in a subvolume + + name + Name of the subvolume + + last_gen + Last transid marker from where to compare + + CLI Example: + + .. code-block:: bash + + salt '*' btrfs.subvolume_find_new /var/volumes/tmp 1024 + + ''' + cmd = ['btrfs', 'subvolume', 'find-new', name, last_gen] + + res = __salt__['cmd.run_all'](cmd) + salt.utils.fsutils._verify_run(res) + + lines = res['stdout'].splitlines() + # Filenames are at the end of each inode line + files = [l.split()[-1] for l in lines if l.startswith('inode')] + # The last transid is in the last line + transid = lines[-1].split()[-1] + return { + 'files': files, + 'transid': transid, + } + + +def subvolume_get_default(path): + ''' + Get the default subvolume of the filesystem path + + path + Mount point for the subvolume + + CLI Example: + + .. code-block:: bash + + salt '*' btrfs.subvolume_get_default /var/volumes/tmp + + ''' + cmd = ['btrfs', 'subvolume', 'get-default', path] + + res = __salt__['cmd.run_all'](cmd) + salt.utils.fsutils._verify_run(res) + + line = res['stdout'].strip() + # The ID is the second parameter, and the name the last one, or + # '(FS_TREE)' + # + # When the default one is set: + # ID 5 (FS_TREE) + # + # When we manually set a different one (var): + # ID 257 gen 8 top level 5 path var + # + id_ = line.split()[1] + name = line.split()[-1] + return { + 'id': id_, + 'name': name, + } + + +def _pop(line, key, use_rest): + ''' + Helper for the line parser. + + If key is a prefix of line, will remove ir from the line and will + extract the value (space separation), and the rest of the line. + + If use_rest is True, the value will be the rest of the line. + + Return a tuple with the value and the rest of the line. + ''' + value = None + if line.startswith(key): + line = line[len(key):].strip() + if use_rest: + value = line + line = '' + else: + value, line = line.split(' ', 1) + return value, line.strip() + + +def subvolume_list(path, parent_id=False, absolute=False, + ogeneration=False, generation=False, + subvolumes=False, uuid=False, parent_uuid=False, + sent_subvolume_uuid=False, snapshots=False, + readonly=False, deleted=False, generation_cmp=None, + ogeneration_cmp=None, sort=None): + ''' + List the subvolumes present in the filesystem. + + path + Mount point for the subvolume + + parent_id + Print parent ID + + absolute + Print all the subvolumes in the filesystem and distinguish + between absolute and relative path with respect to the given + + + ogeneration + Print the ogeneration of the subvolume + + generation + Print the generation of the subvolume + + subvolumes + Print only subvolumes below specified + + uuid + Print the UUID of the subvolume + + parent_uuid + Print the parent uuid of subvolumes (and snapshots) + + sent_subvolume_uuid + Print the UUID of the sent subvolume, where the subvolume is + the result of a receive operation + + snapshots + Only snapshot subvolumes in the filesystem will be listed + + readonly + Only readonly subvolumes in the filesystem will be listed + + deleted + Only deleted subvolumens that are ye not cleaned + + generation_cmp + List subvolumes in the filesystem that its generation is >=, + <= or = value. '+' means >= value, '-' means <= value, If + there is neither '+' nor '-', it means = value + + ogeneration_cmp + List subvolumes in the filesystem that its ogeneration is >=, + <= or = value + + sort + List subvolumes in order by specified items. Possible values: + * rootid + * gen + * ogen + * path + You can add '+' or '-' in front of each items, '+' means + ascending, '-' means descending. The default is ascending. You + can combite it in a list. + + CLI Example: + + .. code-block:: bash + + salt '*' btrfs.subvolume_list /var/volumes/tmp + salt '*' btrfs.subvolume_list /var/volumes/tmp path=True + salt '*' btrfs.subvolume_list /var/volumes/tmp sort='[-rootid]' + + ''' + if sort and type(sort) is not list: + raise CommandExecutionError('Sort parameter must be a list') + + valid_sorts = [ + ''.join((order, attrib)) for order, attrib in itertools.product( + ('-', '', '+'), ('rootid', 'gen', 'ogen', 'path')) + ] + if sort and not all(s in valid_sorts for s in sort): + raise CommandExecutionError('Value for sort not recognized') + + cmd = ['btrfs', 'subvolume', 'list'] + + params = ((parent_id, '-p'), + (absolute, '-a'), + (ogeneration, '-c'), + (generation, '-g'), + (subvolumes, '-o'), + (uuid, '-u'), + (parent_uuid, '-q'), + (sent_subvolume_uuid, '-R'), + (snapshots, '-s'), + (readonly, '-r'), + (deleted, '-d')) + cmd.extend(p[1] for p in params if p[0]) + + if generation_cmp: + cmd.extend(['-G', generation_cmp]) + + if ogeneration_cmp: + cmd.extend(['-C', ogeneration_cmp]) + + # We already validated the content of the list + if sort: + cmd.append('--sort={}'.format(','.join(sort))) + + cmd.append(path) + + res = __salt__['cmd.run_all'](cmd) + salt.utils.fsutils._verify_run(res) + + # Parse the output. ID and gen are always at the begining, and + # path is always at the end. There is only one column that + # contains space (top level), and the path value can also have + # spaces. The issue is that we do not know how many spaces do we + # have in the path name, so any classic solution based on split + # will fail. + # + # This list is in order. + columns = ('ID', 'gen', 'cgen', 'parent', 'top level', 'otime', + 'parent_uuid', 'received_uuid', 'uuid', 'path') + result = [] + for line in res['stdout'].splitlines(): + table = {} + for key in columns: + value, line = _pop(line, key, key == 'path') + if value: + table[key.lower()] = value + # If line is not empty here, we are not able to parse it + if not line: + result.append(table) + + return result + + +def subvolume_set_default(subvolid, path): + ''' + Set the subvolume as default + + subvolid + ID of the new default subvolume + + path + Mount point for the filesystem + + CLI Example: + + .. code-block:: bash + + salt '*' btrfs.subvolume_set_default 257 /var/volumes/tmp + + ''' + cmd = ['btrfs', 'subvolume', 'set-default', subvolid, path] + + res = __salt__['cmd.run_all'](cmd) + salt.utils.fsutils._verify_run(res) + return True + + +def subvolume_show(path): + ''' + Show information of a given subvolume + + path + Mount point for the filesystem + + CLI Example: + + .. code-block:: bash + + salt '*' btrfs.subvolume_show /var/volumes/tmp + + ''' + cmd = ['btrfs', 'subvolume', 'show', path] + + res = __salt__['cmd.run_all'](cmd) + salt.utils.fsutils._verify_run(res) + + result = {} + table = {} + # The real name is the first line, later there is a table of + # values separated with colon. + stdout = res['stdout'].splitlines() + key = stdout.pop(0) + result[key.strip()] = table + + for line in stdout: + key, value = line.split(':', 1) + table[key.lower().strip()] = value.strip() + return result + + +def subvolume_snapshot(source, dest=None, name=None, read_only=False): + ''' + Create a snapshot of a source subvolume + + source + Source subvolume from where to create the snapshot + + dest + If only dest is given, the subvolume will be named as the + basename of the source + + name + Name of the snapshot + + read_only + Create a read only snapshot + + CLI Example: + + .. code-block:: bash + + salt '*' btrfs.subvolume_snapshot /var/volumes/tmp dest=/.snapshots + salt '*' btrfs.subvolume_snapshot /var/volumes/tmp name=backup + + ''' + if not dest and not name: + raise CommandExecutionError('Provide parameter dest, name, or both') + + cmd = ['btrfs', 'subvolume', 'snapshot'] + if read_only: + cmd.append('-r') + if dest and not name: + cmd.append(dest) + if dest and name: + name = os.path.join(dest, name) + if name: + cmd.append(name) + + res = __salt__['cmd.run_all'](cmd) + salt.utils.fsutils._verify_run(res) + return True + + +def subvolume_sync(path, subvolids=None, sleep=None): + ''' + Wait until given subvolume are completely removed from the + filesystem after deletion. + + path + Mount point for the filesystem + + subvolids + List of IDs of subvolumes to wait for + + sleep + Sleep N seconds betwenn checks (default: 1) + + CLI Example: + + .. code-block:: bash + + salt '*' btrfs.subvolume_sync /var/volumes/tmp + salt '*' btrfs.subvolume_sync /var/volumes/tmp subvolids='[257]' + + ''' + if subvolids and type(subvolids) is not list: + raise CommandExecutionError('Subvolids parameter must be a list') + + cmd = ['btrfs', 'subvolume', 'sync'] + if sleep: + cmd.extend(['-s', sleep]) + + cmd.append(path) + if subvolids: + cmd.extend(subvolids) + + res = __salt__['cmd.run_all'](cmd) + salt.utils.fsutils._verify_run(res) + return True diff --git a/salt/modules/chroot.py b/salt/modules/chroot.py new file mode 100644 index 00000000000..6e4705b67ee --- /dev/null +++ b/salt/modules/chroot.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# +# Author: Alberto Planas +# +# Copyright 2018 SUSE LINUX GmbH, Nuernberg, Germany. +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +''' +:maintainer: Alberto Planas +:maturity: new +:depends: None +:platform: Linux +''' +from __future__ import absolute_import, print_function, unicode_literals +import logging +import os +import sys +import tempfile + +from salt.defaults.exitcodes import EX_OK +from salt.exceptions import CommandExecutionError +from salt.utils.args import clean_kwargs + +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Chroot command is required. + ''' + if __utils__['path.which']('chroot') is not None: + return True + else: + return (False, 'Module chroot requires the command chroot') + + +def exist(name): + ''' + Return True if the chroot environment is present. + ''' + dev = os.path.join(name, 'dev') + proc = os.path.join(name, 'proc') + return all(os.path.isdir(i) for i in (name, dev, proc)) + + +def create(name): + ''' + Create a basic chroot environment. + + Note that this environment is not functional. The caller needs to + install the minimal required binaries, including Python if + chroot.call is called. + + name + Path to the chroot environment + + CLI Example: + + .. code-block:: bash + + salt myminion chroot.create /chroot + + ''' + if not exist(name): + dev = os.path.join(name, 'dev') + proc = os.path.join(name, 'proc') + try: + os.makedirs(dev, mode=0o755) + os.makedirs(proc, mode=0o555) + except OSError as e: + log.error('Error when trying to create chroot directories: %s', e) + return False + return True + + +def call(name, function, *args, **kwargs): + ''' + Executes a Salt function inside a chroot environment. + + The chroot does not need to have Salt installed, but Python is + required. + + name + Path to the chroot environment + + function + Salt execution module function + + CLI Example: + + .. code-block:: bash + + salt myminion chroot.call /chroot test.ping + + ''' + + if not function: + raise CommandExecutionError('Missing function parameter') + + if not exist(name): + raise CommandExecutionError('Chroot environment not found') + + # Create a temporary directory inside the chroot where we can + # untar salt-thin + thin_dest_path = tempfile.mkdtemp(dir=name) + thin_path = __utils__['thin.gen_thin']( + __opts__['cachedir'], + extra_mods=__salt__['config.option']('thin_extra_mods', ''), + so_mods=__salt__['config.option']('thin_so_mods', '') + ) + stdout = __salt__['archive.tar']('xzf', thin_path, dest=thin_dest_path) + if stdout: + __utils__['files.rm_rf'](thin_dest_path) + return {'result': False, 'comment': stdout} + + chroot_path = os.path.join(os.path.sep, + os.path.relpath(thin_dest_path, name)) + try: + safe_kwargs = clean_kwargs(**kwargs) + salt_argv = [ + 'python{}'.format(sys.version_info[0]), + os.path.join(chroot_path, 'salt-call'), + '--metadata', + '--local', + '--log-file', os.path.join(chroot_path, 'log'), + '--cachedir', os.path.join(chroot_path, 'cache'), + '--out', 'json', + '-l', 'quiet', + '--', + function + ] + list(args) + ['{}={}'.format(k, v) for (k, v) in safe_kwargs] + ret = __salt__['cmd.run_chroot'](name, [str(x) for x in salt_argv]) + if ret['retcode'] != EX_OK: + raise CommandExecutionError(ret['stderr']) + + # Process "real" result in stdout + try: + data = __utils__['json.find_json'](ret['stdout']) + local = data.get('local', data) + if isinstance(local, dict) and 'retcode' in local: + __context__['retcode'] = local['retcode'] + return local.get('return', data) + except ValueError: + return { + 'result': False, + 'comment': "Can't parse container command output" + } + finally: + __utils__['files.rm_rf'](thin_dest_path) diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index 81c4d3f8114..d0819f2f790 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -2889,6 +2889,7 @@ def run_chroot(root, group=None, shell=DEFAULT_SHELL, python_shell=True, + binds=None, env=None, clean_env=False, template=None, @@ -2914,19 +2915,17 @@ def run_chroot(root, :param str root: Path to the root of the jail to use. - stdin - A string of standard input can be specified for the command to be run using - the ``stdin`` parameter. This can be useful in cases where sensitive - information must be read from standard input.: + :param str stdin: A string of standard input can be specified for + the command to be run using the ``stdin`` parameter. This can + be useful in cases where sensitive information must be read + from standard input.: - runas - User to run script as. + :param str runas: User to run script as. - group - Group to run script as. + :param str group: Group to run script as. - shell - Shell to execute under. Defaults to the system default shell. + :param str shell: Shell to execute under. Defaults to the system + default shell. :param str cmd: The command to run. ex: ``ls -lart /home`` @@ -2950,6 +2949,9 @@ def run_chroot(root, arguments. Set to True to use shell features, such as pipes or redirection. + :param list binds: List of directories that will be exported inside + the chroot with the bind option. + :param dict env: Environment variables to be set prior to execution. .. note:: @@ -2968,11 +2970,11 @@ def run_chroot(root, engine will be used to render the downloaded file. Currently jinja, mako, and wempy are supported. - :param bool rstrip: - Strip all whitespace off the end of output before it is returned. + :param bool rstrip: Strip all whitespace off the end of output + before it is returned. - :param str umask: - The umask (in octal) to use when running the command. + :param str umask: The umask (in octal) to use when running the + command. :param str output_encoding: Control the encoding used to decode the command's output. @@ -3046,6 +3048,15 @@ def run_chroot(root, 'sysfs', fstype='sysfs') + binds = binds if binds else [] + for bind_exported in binds: + bind_exported_to = os.path.relpath(bind_exported, os.path.sep) + bind_exported_to = os.path.join(root, bind_exported_to) + __salt__['mount.mount']( + bind_exported_to, + bind_exported, + opts='default,bind') + # Execute chroot routine sh_ = '/bin/sh' if os.path.isfile(os.path.join(root, 'bin/bash')): @@ -3096,6 +3107,11 @@ def run_chroot(root, log.error('Processes running in chroot could not be killed, ' 'filesystem will remain mounted') + for bind_exported in binds: + bind_exported_to = os.path.relpath(bind_exported, os.path.sep) + bind_exported_to = os.path.join(root, bind_exported_to) + __salt__['mount.umount'](bind_exported_to) + __salt__['mount.umount'](os.path.join(root, 'sys')) __salt__['mount.umount'](os.path.join(root, 'proc')) __salt__['mount.umount'](os.path.join(root, 'dev')) diff --git a/salt/modules/disk.py b/salt/modules/disk.py index 0e0f6eef553..9b0c001e351 100644 --- a/salt/modules/disk.py +++ b/salt/modules/disk.py @@ -268,24 +268,34 @@ def percent(args=None): @salt.utils.decorators.path.which('blkid') -def blkid(device=None): +def blkid(device=None, token=None): ''' Return block device attributes: UUID, LABEL, etc. This function only works on systems where blkid is available. + device + Device name from the system + + token + Any valid token used for the search + CLI Example: .. code-block:: bash salt '*' disk.blkid salt '*' disk.blkid /dev/sda + salt '*' disk.blkid token='UUID=6a38ee5-7235-44e7-8b22-816a403bad5d' + salt '*' disk.blkid token='TYPE=ext4' ''' - args = "" + cmd = ['blkid'] if device: - args = " " + device + cmd.append(device) + elif token: + cmd.extend(['-t', token]) ret = {} - blkid_result = __salt__['cmd.run_all']('blkid' + args, python_shell=False) + blkid_result = __salt__['cmd.run_all'](cmd, python_shell=False) if blkid_result['retcode'] > 0: return ret @@ -422,6 +432,7 @@ def format_(device, fs_type='ext4', inode_size=None, lazy_itable_init=None, + fat=None, force=False): ''' Format a filesystem onto a device @@ -449,6 +460,10 @@ def format_(device, This option is only enabled for ext filesystems + fat + FAT size option. Can be 12, 16 or 32, and can only be used on + fat or vfat filesystems. + force Force mke2fs to create a filesystem, even if the specified device is not a partition on a block special device. This option is only enabled @@ -471,6 +486,9 @@ def format_(device, if lazy_itable_init is not None: if fs_type[:3] == 'ext': cmd.extend(['-E', 'lazy_itable_init={0}'.format(lazy_itable_init)]) + if fat is not None and fat in (12, 16, 32): + if fs_type[-3:] == 'fat': + cmd.extend(['-F', fat]) if force: if fs_type[:3] == 'ext': cmd.append('-F') diff --git a/salt/modules/dpkg_lowpkg.py b/salt/modules/dpkg_lowpkg.py index 26ca5dcf5a7..d5c89cb1955 100644 --- a/salt/modules/dpkg_lowpkg.py +++ b/salt/modules/dpkg_lowpkg.py @@ -135,7 +135,7 @@ def unpurge(*packages): return salt.utils.data.compare_dicts(old, new) -def list_pkgs(*packages): +def list_pkgs(*packages, **kwargs): ''' List the packages currently installed in a dict:: @@ -169,7 +169,7 @@ def list_pkgs(*packages): return pkgs -def file_list(*packages): +def file_list(*packages, **kwargs): ''' List the files that belong to a package. Not specifying any packages will return a list of _every_ file on the system's package database (not @@ -211,7 +211,7 @@ def file_list(*packages): return {'errors': errors, 'files': list(ret)} -def file_dict(*packages): +def file_dict(*packages, **kwargs): ''' List the files that belong to a package, grouped by package. Not specifying any packages will return a list of _every_ file on the system's diff --git a/salt/modules/ebuildpkg.py b/salt/modules/ebuildpkg.py index cb77ff7852f..205318f5797 100644 --- a/salt/modules/ebuildpkg.py +++ b/salt/modules/ebuildpkg.py @@ -358,7 +358,7 @@ def list_upgrades(refresh=True, backtrack=3, **kwargs): # pylint: disable=W0613 return _get_upgradable(backtrack) -def upgrade_available(name): +def upgrade_available(name, **kwargs): ''' Check whether or not an upgrade is available for a given package @@ -440,7 +440,7 @@ def list_pkgs(versions_as_list=False, **kwargs): return ret -def refresh_db(): +def refresh_db(**kwargs): ''' Update the portage tree using the first available method from the following list: @@ -765,7 +765,7 @@ def install(name=None, return changes -def update(pkg, slot=None, fromrepo=None, refresh=False, binhost=None): +def update(pkg, slot=None, fromrepo=None, refresh=False, binhost=None, **kwargs): ''' .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 On minions running systemd>=205, `systemd-run(1)`_ is now used to @@ -858,7 +858,7 @@ def update(pkg, slot=None, fromrepo=None, refresh=False, binhost=None): return ret -def upgrade(refresh=True, binhost=None, backtrack=3): +def upgrade(refresh=True, binhost=None, backtrack=3, **kwargs): ''' .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 On minions running systemd>=205, `systemd-run(1)`_ is now used to diff --git a/salt/modules/freebsdpkg.py b/salt/modules/freebsdpkg.py index 43f127ef359..0bae7a3baba 100644 --- a/salt/modules/freebsdpkg.py +++ b/salt/modules/freebsdpkg.py @@ -238,7 +238,7 @@ def version(*names, **kwargs): ]) -def refresh_db(): +def refresh_db(**kwargs): ''' ``pkg_add(1)`` does not use a local database of available packages, so this function simply returns ``True``. it exists merely for API compatibility. @@ -503,7 +503,7 @@ def _rehash(): __salt__['cmd.shell']('rehash', output_loglevel='trace') -def file_list(*packages): +def file_list(*packages, **kwargs): ''' List the files that belong to a package. Not specifying any packages will return a list of _every_ file on the system's package database (not @@ -525,7 +525,7 @@ def file_list(*packages): return ret -def file_dict(*packages): +def file_dict(*packages, **kwargs): ''' List the files that belong to a package, grouped by package. Not specifying any packages will return a list of _every_ file on the diff --git a/salt/modules/freezer.py b/salt/modules/freezer.py new file mode 100644 index 00000000000..786dfe45152 --- /dev/null +++ b/salt/modules/freezer.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- +# +# Author: Alberto Planas +# +# Copyright 2018 SUSE LINUX GmbH, Nuernberg, Germany. +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +''' +:maintainer: Alberto Planas +:maturity: new +:depends: None +:platform: Linux +''' +from __future__ import absolute_import, print_function, unicode_literals +import logging +import os + +from salt.exceptions import CommandExecutionError +from salt.utils.args import clean_kwargs +from salt.utils.files import fopen +import salt.utils.json as json +from salt.ext.six.moves import zip + +log = logging.getLogger(__name__) + +__func_alias__ = { + 'list_': 'list', +} + + +def __virtual__(): + ''' + Freezer is based on top of the pkg module. + + Return True as pkg is going to be there, so we can avoid of + loading all modules. + + ''' + return True + + +def _states_path(): + ''' + Return the path where we will store the states. + ''' + return os.path.join(__opts__['cachedir'], 'freezer') + + +def _paths(name=None): + ''' + Return the full path for the packages and repository freezer + files. + + ''' + name = 'freezer' if not name else name + states_path = _states_path() + return ( + os.path.join(states_path, '{}-pkgs.yml'.format(name)), + os.path.join(states_path, '{}-reps.yml'.format(name)), + ) + + +def status(name=None): + ''' + Return True if there is already a frozen state. + + A frozen state is merely a list of packages (including the + version) in a specific time. This information can be used to + compare with the current list of packages, and revert the + installation of some extra packages that are in the system. + + name + Name of the frozen state. Optional. + + CLI Example: + + .. code-block:: bash + + salt '*' freezer.status + salt '*' freezer.status pre_install + + ''' + name = 'freezer' if not name else name + return all(os.path.isfile(i) for i in _paths(name)) + + +def list_(): + ''' + Return the list of frozen states. + + CLI Example: + + .. code-block:: bash + + salt '*' freezer.list + + ''' + ret = [] + states_path = _states_path() + if not os.path.isdir(states_path): + return ret + + for state in os.listdir(states_path): + if state.endswith(('-pkgs.yml', '-reps.yml')): + # Remove the suffix, as both share the same size + ret.append(state[:-9]) + return sorted(set(ret)) + + +def freeze(name=None, force=False, **kwargs): + ''' + Save the list of package and repos in a freeze file. + + As this module is build on top of the pkg module, the user can + send extra attributes to the underlying pkg module via kwargs. + This function will call ``pkg.list_pkgs`` and ``pkg.list_repos``, + and any additional arguments will be passed through to those + functions. + + name + Name of the frozen state. Optional. + + force + If true, overwrite the state. Optional. + + CLI Example: + + .. code-block:: bash + + salt '*' freezer.freeze + salt '*' freezer.freeze pre_install + salt '*' freezer.freeze force=True root=/chroot + + ''' + states_path = _states_path() + + try: + os.makedirs(states_path) + except OSError as e: + msg = 'Error when trying to create the freezer storage %s: %s' + log.error(msg, states_path, e) + raise CommandExecutionError(msg % (states_path, e)) + + if status(name) and not force: + raise CommandExecutionError('The state is already present. Use ' + 'force parameter to overwrite.') + safe_kwargs = clean_kwargs(**kwargs) + pkgs = __salt__['pkg.list_pkgs'](**safe_kwargs) + repos = __salt__['pkg.list_repos'](**safe_kwargs) + for name, content in zip(_paths(name), (pkgs, repos)): + with fopen(name, 'w') as fp: + json.dump(content, fp) + return True + + +def restore(name=None, **kwargs): + ''' + Make sure that the system contains the packages and repos from a + frozen state. + + Read the list of packages and repositories from the freeze file, + and compare it with the current list of packages and repos. If + there is any difference, all the missing packages are repos will + be installed, and all the extra packages and repos will be + removed. + + As this module is build on top of the pkg module, the user can + send extra attributes to the underlying pkg module via kwargs. + This function will call ``pkg.list_repos``, ``pkg.mod_repo``, + ``pkg.list_pkgs``, ``pkg.install``, ``pkg.remove`` and + ``pkg.del_repo``, and any additional arguments will be passed + through to those functions. + + name + Name of the frozen state. Optional. + + CLI Example: + + .. code-block:: bash + + salt '*' freezer.restore + salt '*' freezer.restore root=/chroot + + ''' + if not status(name): + raise CommandExecutionError('Frozen state not found.') + + frozen_pkgs = {} + frozen_repos = {} + for name, content in zip(_paths(name), (frozen_pkgs, frozen_repos)): + with fopen(name) as fp: + content.update(json.load(fp)) + + # The ordering of removing or adding packages and repos can be + # relevant, as maybe some missing package comes from a repo that + # is also missing, so it cannot be installed. But can also happend + # that a missing package comes from a repo that is present, but + # will be removed. + # + # So the proposed order is; + # - Add missing repos + # - Add missing packages + # - Remove extra packages + # - Remove extra repos + + safe_kwargs = clean_kwargs(**kwargs) + + # Note that we expect that the information stored in list_XXX + # match with the mod_XXX counterpart. If this is not the case the + # recovery will be partial. + + res = { + 'pkgs': {'add': [], 'remove': []}, + 'repos': {'add': [], 'remove': []}, + 'comment': [], + } + + # Add missing repositories + repos = __salt__['pkg.list_repos'](**safe_kwargs) + missing_repos = set(frozen_repos) - set(repos) + for repo in missing_repos: + try: + # In Python 2 we cannot do advance destructuring, so we + # need to create a temporary dictionary that will merge + # all the parameters + _tmp_kwargs = frozen_repos[repo].copy() + _tmp_kwargs.update(safe_kwargs) + __salt__['pkg.mod_repo'](repo, **_tmp_kwargs) + res['repos']['add'].append(repo) + log.info('Added missing repository %s', repo) + except Exception as e: + msg = 'Error adding %s repository: %s' + log.error(msg, repo, e) + res['comment'].append(msg % (repo, e)) + + # Add missing packages + # NOTE: we can remove the `for` using `pkgs`. This will improve + # performance, but I want to have a more detalied report of what + # packages are installed or failled. + pkgs = __salt__['pkg.list_pkgs'](**safe_kwargs) + missing_pkgs = set(frozen_pkgs) - set(pkgs) + for pkg in missing_pkgs: + try: + __salt__['pkg.install'](name=pkg, **safe_kwargs) + res['pkgs']['add'].append(pkg) + log.info('Added missing package %s', pkg) + except Exception as e: + msg = 'Error adding %s package: %s' + log.error(msg, pkg, e) + res['comment'].append(msg % (pkg, e)) + + # Remove extra packages + pkgs = __salt__['pkg.list_pkgs'](**safe_kwargs) + extra_pkgs = set(pkgs) - set(frozen_pkgs) + for pkg in extra_pkgs: + try: + __salt__['pkg.remove'](name=pkg, **safe_kwargs) + res['pkgs']['remove'].append(pkg) + log.info('Removed extra package %s', pkg) + except Exception as e: + msg = 'Error removing %s package: %s' + log.error(msg, pkg, e) + res['comment'].append(msg % (pkg, e)) + + # Remove extra repositories + repos = __salt__['pkg.list_repos'](**safe_kwargs) + extra_repos = set(repos) - set(frozen_repos) + for repo in extra_repos: + try: + __salt__['pkg.del_repo'](repo, **safe_kwargs) + res['repos']['remove'].append(repo) + log.info('Removed extra repository %s', repo) + except Exception as e: + msg = 'Error removing %s repository: %s' + log.error(msg, repo, e) + res['comment'].append(msg % (repo, e)) + + return res diff --git a/salt/modules/groupadd.py b/salt/modules/groupadd.py index e2e1560ab07..15dec6e8983 100644 --- a/salt/modules/groupadd.py +++ b/salt/modules/groupadd.py @@ -12,8 +12,12 @@ # Import python libs from __future__ import absolute_import, print_function, unicode_literals import logging +import functools +import os from salt.ext import six +import salt.utils.files +import salt.utils.stringutils try: import grp except ImportError: @@ -40,6 +44,18 @@ def add(name, gid=None, system=False, root=None): ''' Add the specified group + name + Name of the new group + + gid + Use GID for the new group + + system + Create a system account + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -51,11 +67,12 @@ def add(name, gid=None, system=False, root=None): cmd.append('-g {0}'.format(gid)) if system and __grains__['kernel'] != 'OpenBSD': cmd.append('-r') - cmd.append(name) if root is not None: cmd.extend(('-R', root)) + cmd.append(name) + ret = __salt__['cmd.run_all'](cmd, python_shell=False) return not ret['retcode'] @@ -65,34 +82,53 @@ def delete(name, root=None): ''' Remove the named group + name + Name group to delete + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' group.delete foo ''' - cmd = ['groupdel', name] + cmd = ['groupdel'] if root is not None: cmd.extend(('-R', root)) + cmd.append(name) + ret = __salt__['cmd.run_all'](cmd, python_shell=False) return not ret['retcode'] -def info(name): +def info(name, root=None): ''' Return information about a group + name + Name of the group + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' group.info foo ''' + if root is not None: + getgrnam = functools.partial(_getgrnam, root=root) + else: + getgrnam = functools.partial(grp.getgrnam) + try: - grinfo = grp.getgrnam(name) + grinfo = getgrnam(name) except KeyError: return {} else: @@ -109,10 +145,16 @@ def _format_info(data): 'members': data.gr_mem} -def getent(refresh=False): +def getent(refresh=False, root=None): ''' Return info on all groups + refresh + Force a refresh of group information + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -123,41 +165,74 @@ def getent(refresh=False): return __context__['group.getent'] ret = [] - for grinfo in grp.getgrall(): + if root is not None: + getgrall = functools.partial(_getgrall, root=root) + else: + getgrall = functools.partial(grp.getgrall) + + for grinfo in getgrall(): ret.append(_format_info(grinfo)) __context__['group.getent'] = ret return ret +def _chattrib(name, key, value, param, root=None): + ''' + Change an attribute for a named user + ''' + pre_info = info(name, root=root) + if not pre_info: + return False + + if value == pre_info[key]: + return True + + cmd = ['groupmod'] + + if root is not None: + cmd.extend(('-R', root)) + + cmd.extend((param, value, name)) + + __salt__['cmd.run'](cmd, python_shell=False) + return info(name, root=root).get(key) == value + + def chgid(name, gid, root=None): ''' Change the gid for a named group + name + Name of the group to modify + + gid + Change the group ID to GID + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' group.chgid foo 4376 ''' - pre_gid = __salt__['file.group_to_gid'](name) - if gid == pre_gid: - return True - cmd = ['groupmod', '-g', gid, name] - - if root is not None: - cmd.extend(('-R', root)) - - __salt__['cmd.run'](cmd, python_shell=False) - post_gid = __salt__['file.group_to_gid'](name) - if post_gid != pre_gid: - return post_gid == gid - return False + return _chattrib(name, 'gid', gid, '-g', root=root) def adduser(name, username, root=None): ''' Add a user in the group. + name + Name of the group to modify + + username + Username to add to the group + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -178,7 +253,7 @@ def adduser(name, username, root=None): else: cmd = ['gpasswd', '--add', username, name] if root is not None: - cmd.extend(('-Q', root)) + cmd.extend(('--root', root)) else: cmd = ['usermod', '-G', name, username] if root is not None: @@ -193,6 +268,15 @@ def deluser(name, username, root=None): ''' Remove a user from the group. + name + Name of the group to modify + + username + Username to delete from the group + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -216,7 +300,7 @@ def deluser(name, username, root=None): else: cmd = ['gpasswd', '--del', username, name] if root is not None: - cmd.extend(('-R', root)) + cmd.extend(('--root', root)) retcode = __salt__['cmd.retcode'](cmd, python_shell=False) elif __grains__['kernel'] == 'OpenBSD': out = __salt__['cmd.run_stdout']('id -Gn {0}'.format(username), @@ -239,6 +323,15 @@ def members(name, members_list, root=None): ''' Replaces members of the group with a provided list. + name + Name of the group to modify + + members_list + Username list to set into the group + + root + Directory to chroot into + CLI Example: salt '*' group.members foo 'user1,user2,user3,...' @@ -259,7 +352,7 @@ def members(name, members_list, root=None): else: cmd = ['gpasswd', '--members', members_list, name] if root is not None: - cmd.extend(('-R', root)) + cmd.extend(('--root', root)) retcode = __salt__['cmd.retcode'](cmd, python_shell=False) elif __grains__['kernel'] == 'OpenBSD': retcode = 1 @@ -284,3 +377,43 @@ def members(name, members_list, root=None): return False return not retcode + + +def _getgrnam(name, root=None): + ''' + Alternative implementation for getgrnam, that use only /etc/group + ''' + root = root or '/' + passwd = os.path.join(root, 'etc/group') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + if len(comps) < 4: + log.debug('Ignoring group line: %s', line) + continue + if comps[0] == name: + # Generate a getpwnam compatible output + comps[2] = int(comps[2]) + comps[3] = comps[3].split(',') if comps[3] else [] + return grp.struct_group(comps) + raise KeyError('getgrnam(): name not found: {}'.format(name)) + + +def _getgrall(root=None): + ''' + Alternative implemetantion for getgrall, that use only /etc/group + ''' + root = root or '/' + passwd = os.path.join(root, 'etc/group') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + if len(comps) < 4: + log.debug('Ignoring group line: %s', line) + continue + # Generate a getgrall compatible output + comps[2] = int(comps[2]) + comps[3] = comps[3].split(',') if comps[3] else [] + yield grp.struct_group(comps) diff --git a/salt/modules/linux_lvm.py b/salt/modules/linux_lvm.py index 003d6c0b066..0a975324afa 100644 --- a/salt/modules/linux_lvm.py +++ b/salt/modules/linux_lvm.py @@ -64,17 +64,21 @@ def fullversion(): return ret -def pvdisplay(pvname='', real=False): +def pvdisplay(pvname='', real=False, quiet=False): ''' Return information about the physical volume(s) pvname physical device name + real dereference any symlinks and report the real device .. versionadded:: 2015.8.7 + quiet + if the physical volume is not present, do not show any error + CLI Examples: @@ -87,7 +91,8 @@ def pvdisplay(pvname='', real=False): cmd = ['pvdisplay', '-c'] if pvname: cmd.append(pvname) - cmd_ret = __salt__['cmd.run_all'](cmd, python_shell=False) + cmd_ret = __salt__['cmd.run_all'](cmd, python_shell=False, + ignore_retcode=quiet) if cmd_ret['retcode'] != 0: return {} @@ -118,10 +123,16 @@ def pvdisplay(pvname='', real=False): return ret -def vgdisplay(vgname=''): +def vgdisplay(vgname='', quiet=False): ''' Return information about the volume group(s) + vgname + volume group name + + quiet + if the volume group is not present, do not show any error + CLI Examples: .. code-block:: bash @@ -133,7 +144,8 @@ def vgdisplay(vgname=''): cmd = ['vgdisplay', '-c'] if vgname: cmd.append(vgname) - cmd_ret = __salt__['cmd.run_all'](cmd, python_shell=False) + cmd_ret = __salt__['cmd.run_all'](cmd, python_shell=False, + ignore_retcode=quiet) if cmd_ret['retcode'] != 0: return {} @@ -167,6 +179,12 @@ def lvdisplay(lvname='', quiet=False): ''' Return information about the logical volume(s) + lvname + logical device name + + quiet + if the logical volume is not present, do not show any error + CLI Examples: .. code-block:: bash @@ -178,10 +196,8 @@ def lvdisplay(lvname='', quiet=False): cmd = ['lvdisplay', '-c'] if lvname: cmd.append(lvname) - if quiet: - cmd_ret = __salt__['cmd.run_all'](cmd, python_shell=False, output_loglevel='quiet') - else: - cmd_ret = __salt__['cmd.run_all'](cmd, python_shell=False) + cmd_ret = __salt__['cmd.run_all'](cmd, python_shell=False, + ignore_retcode=quiet) if cmd_ret['retcode'] != 0: return {} @@ -230,7 +246,7 @@ def pvcreate(devices, override=True, **kwargs): for device in devices: if not os.path.exists(device): raise CommandExecutionError('{0} does not exist'.format(device)) - if not pvdisplay(device): + if not pvdisplay(device, quiet=True): cmd.append(device) elif not override: raise CommandExecutionError('Device "{0}" is already an LVM physical volume.'.format(device)) @@ -295,7 +311,7 @@ def pvremove(devices, override=True): # Verify pvcremove was successful for device in devices: - if pvdisplay(device): + if pvdisplay(device, quiet=True): raise CommandExecutionError('Device "{0}" was not affected.'.format(device)) return True diff --git a/salt/modules/mac_brew_pkg.py b/salt/modules/mac_brew_pkg.py index c4507d8267a..0152908fb6c 100644 --- a/salt/modules/mac_brew_pkg.py +++ b/salt/modules/mac_brew_pkg.py @@ -276,7 +276,7 @@ def remove(name=None, pkgs=None, **kwargs): return ret -def refresh_db(): +def refresh_db(**kwargs): ''' Update the homebrew package repository. @@ -459,7 +459,7 @@ def list_upgrades(refresh=True, **kwargs): # pylint: disable=W0613 return ret -def upgrade_available(pkg): +def upgrade_available(pkg, **kwargs): ''' Check whether or not an upgrade is available for a given package @@ -472,7 +472,7 @@ def upgrade_available(pkg): return pkg in list_upgrades() -def upgrade(refresh=True): +def upgrade(refresh=True, **kwargs): ''' Upgrade outdated, unpinned brews. @@ -517,7 +517,7 @@ def upgrade(refresh=True): return ret -def info_installed(*names): +def info_installed(*names, **kwargs): ''' Return the information of the named package(s) installed on the system. diff --git a/salt/modules/mac_portspkg.py b/salt/modules/mac_portspkg.py index 78a38d54a94..d403d0e29b2 100644 --- a/salt/modules/mac_portspkg.py +++ b/salt/modules/mac_portspkg.py @@ -376,7 +376,7 @@ def list_upgrades(refresh=True, **kwargs): # pylint: disable=W0613 return _list('outdated') -def upgrade_available(pkg, refresh=True): +def upgrade_available(pkg, refresh=True, **kwargs): ''' Check whether or not an upgrade is available for a given package @@ -389,7 +389,7 @@ def upgrade_available(pkg, refresh=True): return pkg in list_upgrades(refresh=refresh) -def refresh_db(): +def refresh_db(**kwargs): ''' Update ports with ``port selfupdate`` @@ -405,7 +405,7 @@ def refresh_db(): return salt.utils.mac_utils.execute_return_success(cmd) -def upgrade(refresh=True): # pylint: disable=W0613 +def upgrade(refresh=True, **kwargs): # pylint: disable=W0613 ''' Run a full upgrade using MacPorts 'port upgrade outdated' diff --git a/salt/modules/mount.py b/salt/modules/mount.py index e807b1729ea..97263252c75 100644 --- a/salt/modules/mount.py +++ b/salt/modules/mount.py @@ -1212,14 +1212,15 @@ def mount(name, device, mkmnt=False, fstype='', opts='defaults', user=None, util lopts = ','.join(opts) args = '-o {0}'.format(lopts) - # use of fstype on AIX differs from typical Linux use of -t functionality - # AIX uses -v vfsname, -t fstype mounts all with fstype in /etc/filesystems - if 'AIX' in __grains__['os']: - if fstype: + if fstype: + # use of fstype on AIX differs from typical Linux use of -t + # functionality AIX uses -v vfsname, -t fstype mounts all with + # fstype in /etc/filesystems + if 'AIX' in __grains__['os']: args += ' -v {0}'.format(fstype) - else: - if fstype: + else: args += ' -t {0}'.format(fstype) + cmd = 'mount {0} {1} {2} '.format(args, device, name) out = __salt__['cmd.run_all'](cmd, runas=user, python_shell=False) if out['retcode']: diff --git a/salt/modules/openbsdpkg.py b/salt/modules/openbsdpkg.py index b3b6bab9120..819a24afb1a 100644 --- a/salt/modules/openbsdpkg.py +++ b/salt/modules/openbsdpkg.py @@ -344,7 +344,7 @@ def purge(name=None, pkgs=None, **kwargs): return remove(name=name, pkgs=pkgs, purge=True) -def upgrade_available(name): +def upgrade_available(name, **kwargs): ''' Check whether or not an upgrade is available for a given package diff --git a/salt/modules/pacmanpkg.py b/salt/modules/pacmanpkg.py index e30296e8c8c..35007e27f5c 100644 --- a/salt/modules/pacmanpkg.py +++ b/salt/modules/pacmanpkg.py @@ -111,7 +111,7 @@ def latest_version(*names, **kwargs): available_version = salt.utils.functools.alias_function(latest_version, 'available_version') -def upgrade_available(name): +def upgrade_available(name, **kwargs): ''' Check whether or not an upgrade is available for a given package @@ -393,7 +393,7 @@ def group_diff(name): return ret -def refresh_db(root=None): +def refresh_db(root=None, **kwargs): ''' Just run a ``pacman -Sy``, return a dict:: @@ -843,7 +843,7 @@ def purge(name=None, pkgs=None, **kwargs): return _uninstall(action='purge', name=name, pkgs=pkgs) -def file_list(*packages): +def file_list(*packages, **kwargs): ''' List the files that belong to a package. Not specifying any packages will return a list of _every_ file on the system's package database (not @@ -877,7 +877,7 @@ def file_list(*packages): return {'errors': errors, 'files': ret} -def file_dict(*packages): +def file_dict(*packages, **kwargs): ''' List the files that belong to a package, grouped by package. Not specifying any packages will return a list of _every_ file on the system's @@ -913,7 +913,7 @@ def file_dict(*packages): return {'errors': errors, 'packages': ret} -def owner(*paths): +def owner(*paths, **kwargs): ''' .. versionadded:: 2014.7.0 diff --git a/salt/modules/parted_partition.py b/salt/modules/parted_partition.py index 1757e7118fd..c2e0ebb882b 100644 --- a/salt/modules/parted_partition.py +++ b/salt/modules/parted_partition.py @@ -44,6 +44,15 @@ VALID_UNITS = set(['s', 'B', 'kB', 'MB', 'MiB', 'GB', 'GiB', 'TB', 'TiB', '%', 'cyl', 'chs', 'compact']) +VALID_DISK_FLAGS = set(['cylinder_alignment', 'pmbr_boot', + 'implicit_partition_table']) + +VALID_PARTITION_FLAGS = set(['boot', 'root', 'swap', 'hidden', 'raid', + 'lvm', 'lba', 'hp-service', 'palo', + 'prep', 'msftres', 'bios_grub', 'atvrecv', + 'diag', 'legacy_boot', 'msftdata', 'irst', + 'esp', 'type']) + def __virtual__(): ''' @@ -641,8 +650,26 @@ def set_(device, minor, flag, state): :ref:`YAML Idiosyncrasies `). Some or all of these flags will be available, depending on what disk label you are using. - Valid flags are: bios_grub, legacy_boot, boot, lba, root, swap, hidden, raid, - LVM, PALO, PREP, DIAG + Valid flags are: + * boot + * root + * swap + * hidden + * raid + * lvm + * lba + * hp-service + * palo + * prep + * msftres + * bios_grub + * atvrecv + * diag + * legacy_boot + * msftdata + * irst + * esp + * type CLI Example: @@ -659,8 +686,7 @@ def set_(device, minor, flag, state): 'Invalid minor number passed to partition.set' ) - if flag not in set(['bios_grub', 'legacy_boot', 'boot', 'lba', 'root', - 'swap', 'hidden', 'raid', 'LVM', 'PALO', 'PREP', 'DIAG']): + if flag not in VALID_PARTITION_FLAGS: raise CommandExecutionError('Invalid flag passed to partition.set') if state not in set(['on', 'off']): @@ -691,8 +717,7 @@ def toggle(device, partition, flag): 'Invalid partition number passed to partition.toggle' ) - if flag not in set(['bios_grub', 'legacy_boot', 'boot', 'lba', 'root', - 'swap', 'hidden', 'raid', 'LVM', 'PALO', 'PREP', 'DIAG']): + if flag not in VALID_PARTITION_FLAGS: raise CommandExecutionError('Invalid flag passed to partition.toggle') cmd = 'parted -m -s {0} toggle {1} {2}'.format(device, partition, flag) @@ -700,6 +725,60 @@ def toggle(device, partition, flag): return out +def disk_set(device, flag, state): + ''' + Changes a flag on selected device. + + A flag can be either "on" or "off" (make sure to use proper + quoting, see :ref:`YAML Idiosyncrasies + `). Some or all of these flags will be + available, depending on what disk label you are using. + + Valid flags are: + * cylinder_alignment + * pmbr_boot + * implicit_partition_table + + CLI Example: + + .. code-block:: bash + + salt '*' partition.disk_set /dev/sda pmbr_boot '"on"' + ''' + _validate_device(device) + + if flag not in VALID_DISK_FLAGS: + raise CommandExecutionError('Invalid flag passed to partition.disk_set') + + if state not in set(['on', 'off']): + raise CommandExecutionError('Invalid state passed to partition.disk_set') + + cmd = ['parted', '-m', '-s', device, 'disk_set', flag, state] + out = __salt__['cmd.run'](cmd).splitlines() + return out + + +def disk_toggle(device, flag): + ''' + Toggle the state of on . Valid flags are the same + as the disk_set command. + + CLI Example: + + .. code-block:: bash + + salt '*' partition.disk_toggle /dev/sda pmbr_boot + ''' + _validate_device(device) + + if flag not in VALID_DISK_FLAGS: + raise CommandExecutionError('Invalid flag passed to partition.disk_toggle') + + cmd = ['parted', '-m', '-s', device, 'disk_toggle', flag] + out = __salt__['cmd.run'](cmd).splitlines() + return out + + def exists(device=''): ''' Check to see if the partition exists diff --git a/salt/modules/pkgin.py b/salt/modules/pkgin.py index 240f79ca266..dd5257c80d1 100644 --- a/salt/modules/pkgin.py +++ b/salt/modules/pkgin.py @@ -112,7 +112,7 @@ def _splitpkg(name): return name.split(';', 1)[0].rsplit('-', 1) -def search(pkg_name): +def search(pkg_name, **kwargs): ''' Searches for an exact match using pkgin ^package$ @@ -225,7 +225,7 @@ def version(*names, **kwargs): return __salt__['pkg_resource.version'](*names, **kwargs) -def refresh_db(force=False): +def refresh_db(force=False, **kwargs): ''' Use pkg update to get latest pkg_summary @@ -637,7 +637,7 @@ def _rehash(): __salt__['cmd.run']('rehash', output_loglevel='trace') -def file_list(package): +def file_list(package, **kwargs): ''' List the files that belong to a package. @@ -655,7 +655,7 @@ def file_list(package): return ret -def file_dict(*packages): +def file_dict(*packages, **kwargs): ''' .. versionchanged: 2016.3.0 diff --git a/salt/modules/pkgng.py b/salt/modules/pkgng.py index dabd817fbfd..ab20d05ef28 100644 --- a/salt/modules/pkgng.py +++ b/salt/modules/pkgng.py @@ -224,7 +224,7 @@ def version(*names, **kwargs): info = salt.utils.functools.alias_function(version, 'info') -def refresh_db(jail=None, chroot=None, root=None, force=False): +def refresh_db(jail=None, chroot=None, root=None, force=False, **kwargs): ''' Refresh PACKAGESITE contents @@ -2441,7 +2441,7 @@ def _parse_upgrade(stdout): return result -def version_cmp(pkg1, pkg2, ignore_epoch=False): +def version_cmp(pkg1, pkg2, ignore_epoch=False, **kwargs): ''' Do a cmp-style comparison on two packages. Return -1 if pkg1 < pkg2, 0 if pkg1 == pkg2, and 1 if pkg1 > pkg2. Return None if there was a problem diff --git a/salt/modules/rpm_lowpkg.py b/salt/modules/rpm_lowpkg.py index 893ae4f8171..e577c4391a0 100644 --- a/salt/modules/rpm_lowpkg.py +++ b/salt/modules/rpm_lowpkg.py @@ -76,7 +76,7 @@ def bin_pkg_info(path, saltenv='base'): minion so that it can be examined. saltenv : base - Salt fileserver envrionment from which to retrieve the package. Ignored + Salt fileserver environment from which to retrieve the package. Ignored if ``path`` is a local file path on the minion. CLI Example: @@ -128,12 +128,15 @@ def bin_pkg_info(path, saltenv='base'): return ret -def list_pkgs(*packages): +def list_pkgs(*packages, **kwargs): ''' List the packages currently installed in a dict:: {'': ''} + root + use root as top level directory (default: "/") + CLI Example: .. code-block:: bash @@ -141,8 +144,11 @@ def list_pkgs(*packages): salt '*' lowpkg.list_pkgs ''' pkgs = {} - cmd = ['rpm', '-q' if packages else '-qa', - '--queryformat', r'%{NAME} %{VERSION}\n'] + cmd = ['rpm'] + if kwargs.get('root'): + cmd.extend(['--root', kwargs['root']]) + cmd.extend(['-q' if packages else '-qa', + '--queryformat', r'%{NAME} %{VERSION}\n']) if packages: cmd.extend(packages) out = __salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=False) @@ -158,6 +164,9 @@ def verify(*packages, **kwargs): ''' Runs an rpm -Va on a system, and returns the results in a dict + root + use root as top level directory (default: "/") + Files with an attribute of config, doc, ghost, license or readme in the package header can be ignored using the ``ignore_types`` keyword argument @@ -199,6 +208,8 @@ def verify(*packages, **kwargs): verify_options = [x.strip() for x in six.text_type(verify_options).split(',')] cmd = ['rpm'] + if kwargs.get('root'): + cmd.extend(['--root', kwargs['root']]) cmd.extend(['--' + x for x in verify_options]) if packages: cmd.append('-V') @@ -258,6 +269,9 @@ def modified(*packages, **flags): .. versionadded:: 2015.5.0 + root + use root as top level directory (default: "/") + CLI examples: .. code-block:: bash @@ -266,10 +280,12 @@ def modified(*packages, **flags): salt '*' lowpkg.modified httpd postfix salt '*' lowpkg.modified ''' - ret = __salt__['cmd.run_all']( - ['rpm', '-Va'] + list(packages), - output_loglevel='trace', - python_shell=False) + cmd = ['rpm'] + if flags.get('root'): + cmd.extend(['--root', flags.pop('root')]) + cmd.append('-Va') + cmd.extend(packages) + ret = __salt__['cmd.run_all'](cmd, output_loglevel='trace', python_shell=False) data = {} @@ -324,12 +340,15 @@ def modified(*packages, **flags): return filtered_data -def file_list(*packages): +def file_list(*packages, **kwargs): ''' List the files that belong to a package. Not specifying any packages will return a list of _every_ file on the system's rpm database (not generally recommended). + root + use root as top level directory (default: "/") + CLI Examples: .. code-block:: bash @@ -338,12 +357,15 @@ def file_list(*packages): salt '*' lowpkg.file_list httpd postfix salt '*' lowpkg.file_list ''' - if not packages: - cmd = ['rpm', '-qla'] - else: - cmd = ['rpm', '-ql'] + cmd = ['rpm'] + if kwargs.get('root'): + cmd.extend(['--root', kwargs['root']]) + + cmd.append('-ql' if packages else '-qla') + if packages: # Can't concatenate a tuple, must do a list.extend() cmd.extend(packages) + ret = __salt__['cmd.run']( cmd, output_loglevel='trace', @@ -351,12 +373,15 @@ def file_list(*packages): return {'errors': [], 'files': ret} -def file_dict(*packages): +def file_dict(*packages, **kwargs): ''' List the files that belong to a package, sorted by group. Not specifying any packages will return a list of _every_ file on the system's rpm database (not generally recommended). + root + use root as top level directory (default: "/") + CLI Examples: .. code-block:: bash @@ -368,8 +393,11 @@ def file_dict(*packages): errors = [] ret = {} pkgs = {} - cmd = ['rpm', '-q' if packages else '-qa', - '--queryformat', r'%{NAME} %{VERSION}\n'] + cmd = ['rpm'] + if kwargs.get('root'): + cmd.extend(['--root', kwargs['root']]) + cmd.extend(['-q' if packages else '-qa', + '--queryformat', r'%{NAME} %{VERSION}\n']) if packages: cmd.extend(packages) out = __salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=False) @@ -380,8 +408,10 @@ def file_dict(*packages): comps = line.split() pkgs[comps[0]] = {'version': comps[1]} for pkg in pkgs: - files = [] - cmd = ['rpm', '-ql', pkg] + cmd = ['rpm'] + if kwargs.get('root'): + cmd.extend(['--root', kwargs['root']]) + cmd.extend(['-ql', pkg]) out = __salt__['cmd.run']( ['rpm', '-ql', pkg], output_loglevel='trace', @@ -390,7 +420,7 @@ def file_dict(*packages): return {'errors': errors, 'packages': ret} -def owner(*paths): +def owner(*paths, **kwargs): ''' Return the name of the package that owns the file. Multiple file paths can be passed. If a single path is passed, a string will be returned, @@ -400,6 +430,9 @@ def owner(*paths): If the file is not owned by a package, or is not present on the minion, then an empty string will be returned for that path. + root + use root as top level directory (default: "/") + CLI Examples: .. code-block:: bash @@ -411,7 +444,10 @@ def owner(*paths): return '' ret = {} for path in paths: - cmd = ['rpm', '-qf', '--queryformat', '%{name}', path] + cmd = ['rpm'] + if kwargs.get('root'): + cmd.extend(['--root', kwargs['root']]) + cmd.extend(['-qf', '--queryformat', '%{name}', path]) ret[path] = __salt__['cmd.run_stdout'](cmd, output_loglevel='trace', python_shell=False) @@ -471,6 +507,9 @@ def info(*packages, **kwargs): :param all_versions: Return information for all installed versions of the packages + :param root: + use root as top level directory (default: "/") + :return: CLI example: @@ -493,7 +532,14 @@ def info(*packages, **kwargs): else: size_tag = '%{SIZE}' - cmd = packages and "rpm -q {0}".format(' '.join(packages)) or "rpm -qa" + cmd = ['rpm'] + if kwargs.get('root'): + cmd.extend(['--root', kwargs['root']]) + if packages: + cmd.append('-q') + cmd.extend(packages) + else: + cmd.append('-qa') # Construct query format attr_map = { @@ -544,6 +590,7 @@ def info(*packages, **kwargs): query.append(attr_map['description']) query.append("-----\\n") + cmd = ' '.join(cmd) call = __salt__['cmd.run_all'](cmd + (" --queryformat '{0}'".format(''.join(query))), output_loglevel='trace', env={'TZ': 'UTC'}, clean_env=True) if call['retcode'] != 0: @@ -744,10 +791,13 @@ def _prepend(ver): return salt.utils.versions.version_cmp(ver1, ver2, ignore_epoch=False) -def checksum(*paths): +def checksum(*paths, **kwargs): ''' Return if the signature of a RPM file is valid. + root + use root as top level directory (default: "/") + CLI Example: .. code-block:: bash @@ -760,9 +810,14 @@ def checksum(*paths): if not paths: raise CommandExecutionError("No package files has been specified.") + cmd = ['rpm'] + if kwargs.get('root'): + cmd.extend(['--root', kwargs['root']]) + cmd.extend(['-K', '--quiet']) for package_file in paths: + cmd_ = cmd + [package_file] ret[package_file] = (bool(__salt__['file.file_exists'](package_file)) and - not __salt__['cmd.retcode'](["rpm", "-K", "--quiet", package_file], + not __salt__['cmd.retcode'](cmd_, ignore_retcode=True, output_loglevel='trace', python_shell=False)) diff --git a/salt/modules/shadow.py b/salt/modules/shadow.py index 9659867f050..70d5c2f5b4a 100644 --- a/salt/modules/shadow.py +++ b/salt/modules/shadow.py @@ -13,6 +13,7 @@ # Import python libs import os import datetime +import functools try: import spwd except ImportError: @@ -24,6 +25,7 @@ import salt.utils.stringutils from salt.exceptions import CommandExecutionError from salt.ext import six +from salt.ext.six.moves import range try: import salt.utils.pycrypto HAS_CRYPT = True @@ -48,21 +50,32 @@ def default_hash(): return '!' -def info(name): +def info(name, root=None): ''' Return information for the specified user + name + User to get the information for + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.info root ''' + if root is not None: + getspnam = functools.partial(_getspnam, root=root) + else: + getspnam = functools.partial(spwd.getspnam) + try: - data = spwd.getspnam(name) + data = getspnam(name) ret = { - 'name': data.sp_nam, - 'passwd': data.sp_pwd, + 'name': data.sp_namp if hasattr(data, 'sp_namp') else data.sp_nam, + 'passwd': data.sp_pwdp if hasattr(data, 'sp_pwdp') else data.sp_pwd, 'lstchg': data.sp_lstchg, 'min': data.sp_min, 'max': data.sp_max, @@ -82,69 +95,99 @@ def info(name): return ret -def set_inactdays(name, inactdays): +def _set_attrib(name, key, value, param, root=None, validate=True): + ''' + Set a parameter in /etc/shadow + ''' + pre_info = info(name, root=root) + + # If the user is not present or the attribute is already present, + # we return early + if not pre_info['name']: + return False + + if value == pre_info[key]: + return True + + cmd = ['chage'] + + if root is not None: + cmd.extend(('-R', root)) + + cmd.extend((param, value, name)) + + ret = not __salt__['cmd.run'](cmd, python_shell=False) + if validate: + ret = info(name, root=root).get(key) == value + return ret + + +def set_inactdays(name, inactdays, root=None): ''' Set the number of days of inactivity after a password has expired before the account is locked. See man chage. + name + User to modify + + inactdays + Set password inactive after this number of days + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.set_inactdays username 7 ''' - pre_info = info(name) - if inactdays == pre_info['inact']: - return True - cmd = 'chage -I {0} {1}'.format(inactdays, name) - __salt__['cmd.run'](cmd, python_shell=False) - post_info = info(name) - if post_info['inact'] != pre_info['inact']: - return post_info['inact'] == inactdays - return False + return _set_attrib(name, 'inact', inactdays, '-I', root=root) -def set_maxdays(name, maxdays): +def set_maxdays(name, maxdays, root=None): ''' Set the maximum number of days during which a password is valid. See man chage. + name + User to modify + + maxdays + Maximum number of days during which a password is valid + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.set_maxdays username 90 ''' - pre_info = info(name) - if maxdays == pre_info['max']: - return True - cmd = 'chage -M {0} {1}'.format(maxdays, name) - __salt__['cmd.run'](cmd, python_shell=False) - post_info = info(name) - if post_info['max'] != pre_info['max']: - return post_info['max'] == maxdays - return False + return _set_attrib(name, 'max', maxdays, '-M', root=root) -def set_mindays(name, mindays): +def set_mindays(name, mindays, root=None): ''' Set the minimum number of days between password changes. See man chage. + name + User to modify + + mindays + Minimum number of days between password changes + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.set_mindays username 7 ''' - pre_info = info(name) - if mindays == pre_info['min']: - return True - cmd = 'chage -m {0} {1}'.format(mindays, name) - __salt__['cmd.run'](cmd, python_shell=False) - post_info = info(name) - if post_info['min'] != pre_info['min']: - return post_info['min'] == mindays - return False + return _set_attrib(name, 'min', mindays, '-m', root=root) def gen_password(password, crypt_salt=None, algorithm='sha512'): @@ -189,77 +232,107 @@ def gen_password(password, crypt_salt=None, algorithm='sha512'): return salt.utils.pycrypto.gen_hash(crypt_salt, password, algorithm) -def del_password(name): +def del_password(name, root=None): ''' .. versionadded:: 2014.7.0 Delete the password from name user + name + User to delete + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.del_password username ''' - cmd = 'passwd -d {0}'.format(name) + cmd = ['passwd'] + if root is not None: + cmd.extend(('-R', root)) + cmd.extend(('-d', name)) + __salt__['cmd.run'](cmd, python_shell=False, output_loglevel='quiet') - uinfo = info(name) + uinfo = info(name, root=root) return not uinfo['passwd'] and uinfo['name'] == name -def lock_password(name): +def lock_password(name, root=None): ''' .. versionadded:: 2016.11.0 Lock the password from specified user + name + User to lock + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.lock_password username ''' - pre_info = info(name) - if pre_info['name'] == '': + pre_info = info(name, root=root) + if not pre_info['name']: return False + if pre_info['passwd'].startswith('!'): return True - cmd = 'passwd -l {0}'.format(name) - __salt__['cmd.run'](cmd, python_shell=False) + cmd = ['passwd'] - post_info = info(name) + if root is not None: + cmd.extend(('-R', root)) - return post_info['passwd'].startswith('!') + cmd.extend(('-l', name)) + __salt__['cmd.run'](cmd, python_shell=False) + return info(name, root=root)['passwd'].startswith('!') -def unlock_password(name): + +def unlock_password(name, root=None): ''' .. versionadded:: 2016.11.0 Unlock the password from name user + name + User to unlock + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.unlock_password username ''' - pre_info = info(name) - if pre_info['name'] == '': + pre_info = info(name, root=root) + if not pre_info['name']: return False - if pre_info['passwd'][0] != '!': + + if not pre_info['passwd'].startswith('!'): return True - cmd = 'passwd -u {0}'.format(name) - __salt__['cmd.run'](cmd, python_shell=False) + cmd = ['passwd'] + + if root is not None: + cmd.extend(('-R', root)) - post_info = info(name) + cmd.extend(('-u', name)) - return post_info['passwd'][0] != '!' + __salt__['cmd.run'](cmd, python_shell=False) + return not info(name, root=root)['passwd'].startswith('!') -def set_password(name, password, use_usermod=False): +def set_password(name, password, use_usermod=False, root=None): ''' Set the password for a named user. The password must be a properly defined hash. The password hash can be generated with this command: @@ -273,6 +346,18 @@ def set_password(name, password, use_usermod=False): Keep in mind that the $6 represents a sha512 hash, if your OS is using a different hashing algorithm this needs to be changed accordingly + name + User to set the password + + password + Password already hashed + + use_usermod + Use usermod command to better compatibility + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -287,6 +372,9 @@ def set_password(name, password, use_usermod=False): s_file = '/etc/tcb/{0}/shadow'.format(name) else: s_file = '/etc/shadow' + if root: + s_file = os.path.join(root, os.path.relpath(s_file, os.path.sep)) + ret = {} if not os.path.isfile(s_file): return ret @@ -306,20 +394,33 @@ def set_password(name, password, use_usermod=False): with salt.utils.files.fopen(s_file, 'w+') as fp_: lines = [salt.utils.stringutils.to_str(_l) for _l in lines] fp_.writelines(lines) - uinfo = info(name) + uinfo = info(name, root=root) return uinfo['passwd'] == password else: # Use usermod -p (less secure, but more feature-complete) - cmd = 'usermod -p {0} {1}'.format(password, name) + cmd = ['usermod'] + if root is not None: + cmd.extend(('-R', root)) + cmd.extend(('-p', password, name)) + __salt__['cmd.run'](cmd, python_shell=False, output_loglevel='quiet') - uinfo = info(name) + uinfo = info(name, root=root) return uinfo['passwd'] == password -def set_warndays(name, warndays): +def set_warndays(name, warndays, root=None): ''' Set the number of days of warning before a password change is required. See man chage. + + name + User to modify + + warndays + Number of days of warning before a password change is required + + root + Directory to chroot into CLI Example: @@ -327,33 +428,33 @@ def set_warndays(name, warndays): salt '*' shadow.set_warndays username 7 ''' - pre_info = info(name) - if warndays == pre_info['warn']: - return True - cmd = 'chage -W {0} {1}'.format(warndays, name) - __salt__['cmd.run'](cmd, python_shell=False) - post_info = info(name) - if post_info['warn'] != pre_info['warn']: - return post_info['warn'] == warndays - return False + return _set_attrib(name, 'warn', warndays, '-W', root=root) -def set_date(name, date): +def set_date(name, date, root=None): ''' Sets the value for the date the password was last changed to days since the epoch (January 1, 1970). See man chage. + name + User to modify + + date + Date the password was last changed + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.set_date username 0 ''' - cmd = ['chage', '-d', date, name] - return __salt__['cmd.retcode'](cmd, python_shell=False) == 0 + return _set_attrib(name, 'lstchg', date, '-d', root=root, validate=False) -def set_expire(name, expire): +def set_expire(name, expire, root=None): ''' .. versionchanged:: 2014.7.0 @@ -361,26 +462,77 @@ def set_expire(name, expire): (January 1, 1970). Using a value of -1 will clear expiration. See man chage. + name + User to modify + + date + Date the account expires + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.set_expire username -1 ''' - cmd = ['chage', '-E', expire, name] - return __salt__['cmd.retcode'](cmd, python_shell=False) == 0 + return _set_attrib(name, 'expire', expire, '-E', root=root, validate=False) -def list_users(): +def list_users(root=None): ''' .. versionadded:: 2018.3.0 Return a list of all shadow users + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.list_users ''' - return sorted([user.sp_nam for user in spwd.getspall()]) + if root is not None: + getspall = functools.partial(_getspall, root=root) + else: + getspall = functools.partial(spwd.getspall) + + return sorted([user.sp_namp if hasattr(user, 'sp_namp') else user.sp_nam + for user in getspall()]) + + +def _getspnam(name, root=None): + ''' + Alternative implementation for getspnam, that use only /etc/shadow + ''' + root = '/' if not root else root + passwd = os.path.join(root, 'etc/shadow') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + if comps[0] == name: + # Generate a getspnam compatible output + for i in range(2, 9): + comps[i] = int(comps[i]) if comps[i] else -1 + return spwd.struct_spwd(comps) + raise KeyError + + +def _getspall(root=None): + ''' + Alternative implementation for getspnam, that use only /etc/shadow + ''' + root = '/' if not root else root + passwd = os.path.join(root, 'etc/shadow') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + # Generate a getspall compatible output + for i in range(2, 9): + comps[i] = int(comps[i]) if comps[i] else -1 + yield spwd.struct_spwd(comps) diff --git a/salt/modules/solarisipspkg.py b/salt/modules/solarisipspkg.py index b249bd1b3d9..cb6f754a59b 100644 --- a/salt/modules/solarisipspkg.py +++ b/salt/modules/solarisipspkg.py @@ -109,7 +109,7 @@ def _ips_get_pkgversion(line): return line.split()[0].split('@')[1].strip() -def refresh_db(full=False): +def refresh_db(full=False, **kwargs): ''' Updates the remote repos database. @@ -133,7 +133,7 @@ def refresh_db(full=False): return __salt__['cmd.retcode']('/bin/pkg refresh') == 0 -def upgrade_available(name): +def upgrade_available(name, **kwargs): ''' Check if there is an upgrade available for a certain package Accepts full or partial FMRI. Returns all matches found. diff --git a/salt/modules/solarispkg.py b/salt/modules/solarispkg.py index 2a828f6e9c4..b28349a7d84 100644 --- a/salt/modules/solarispkg.py +++ b/salt/modules/solarispkg.py @@ -169,7 +169,7 @@ def latest_version(*names, **kwargs): available_version = salt.utils.functools.alias_function(latest_version, 'available_version') -def upgrade_available(name): +def upgrade_available(name, **kwargs): ''' Check whether or not an upgrade is available for a given package diff --git a/salt/modules/systemd_service.py b/salt/modules/systemd_service.py index fb349d30e6c..3efd455ba68 100644 --- a/salt/modules/systemd_service.py +++ b/salt/modules/systemd_service.py @@ -56,7 +56,7 @@ def __virtual__(): Only work on systems that have been booted with systemd ''' if __grains__['kernel'] == 'Linux' \ - and salt.utils.systemd.booted(__context__): + and salt.utils.systemd.booted(__context__): return __virtualname__ return ( False, @@ -65,6 +65,16 @@ def __virtual__(): ) +def _root(path, root): + ''' + Relocate an absolute path to a new root directory. + ''' + if root: + return os.path.join(root, os.path.relpath(path, os.path.sep)) + else: + return path + + def _canonical_unit_name(name): ''' Build a canonical unit name treating unit names without one @@ -123,15 +133,15 @@ def _check_for_unit_changes(name): __context__[contextkey] = True -def _check_unmask(name, unmask, unmask_runtime): +def _check_unmask(name, unmask, unmask_runtime, root=None): ''' Common code for conditionally removing masks before making changes to a service's state. ''' if unmask: - unmask_(name, runtime=False) + unmask_(name, runtime=False, root=root) if unmask_runtime: - unmask_(name, runtime=True) + unmask_(name, runtime=True, root=root) def _clear_context(): @@ -193,15 +203,16 @@ def _default_runlevel(): return runlevel -def _get_systemd_services(): +def _get_systemd_services(root): ''' Use os.listdir() to get all the unit files ''' ret = set() for path in SYSTEM_CONFIG_PATHS + (LOCAL_CONFIG_PATH,): - # Make sure user has access to the path, and if the path is a link - # it's likely that another entry in SYSTEM_CONFIG_PATHS or LOCAL_CONFIG_PATH - # points to it, so we can ignore it. + # Make sure user has access to the path, and if the path is a + # link it's likely that another entry in SYSTEM_CONFIG_PATHS + # or LOCAL_CONFIG_PATH points to it, so we can ignore it. + path = _root(path, root) if os.access(path, os.R_OK) and not os.path.islink(path): for fullname in os.listdir(path): try: @@ -213,19 +224,20 @@ def _get_systemd_services(): return ret -def _get_sysv_services(systemd_services=None): +def _get_sysv_services(root, systemd_services=None): ''' Use os.listdir() and os.access() to get all the initscripts ''' + initscript_path = _root(INITSCRIPT_PATH, root) try: - sysv_services = os.listdir(INITSCRIPT_PATH) + sysv_services = os.listdir(initscript_path) except OSError as exc: if exc.errno == errno.ENOENT: pass elif exc.errno == errno.EACCES: log.error( 'Unable to check sysvinit scripts, permission denied to %s', - INITSCRIPT_PATH + initscript_path ) else: log.error( @@ -236,11 +248,11 @@ def _get_sysv_services(systemd_services=None): return [] if systemd_services is None: - systemd_services = _get_systemd_services() + systemd_services = _get_systemd_services(root) ret = [] for sysv_service in sysv_services: - if os.access(os.path.join(INITSCRIPT_PATH, sysv_service), os.X_OK): + if os.access(os.path.join(initscript_path, sysv_service), os.X_OK): if sysv_service in systemd_services: log.debug( 'sysvinit script \'%s\' found, but systemd unit ' @@ -303,7 +315,8 @@ def _strip_scope(msg): return '\n'.join(ret).strip() -def _systemctl_cmd(action, name=None, systemd_scope=False, no_block=False): +def _systemctl_cmd(action, name=None, systemd_scope=False, no_block=False, + root=None): ''' Build a systemctl command line. Treat unit names without one of the valid suffixes as a service. @@ -316,6 +329,8 @@ def _systemctl_cmd(action, name=None, systemd_scope=False, no_block=False): ret.append('systemctl') if no_block: ret.append('--no-block') + if root: + ret.extend(['--root', root]) if isinstance(action, six.string_types): action = shlex.split(action) ret.extend(action) @@ -343,26 +358,27 @@ def _systemctl_status(name): return __context__[contextkey] -def _sysv_enabled(name): +def _sysv_enabled(name, root): ''' A System-V style service is assumed disabled if the "startup" symlink (starts with "S") to its script is found in /etc/init.d in the current runlevel. ''' # Find exact match (disambiguate matches like "S01anacron" for cron) - for match in glob.glob('/etc/rc%s.d/S*%s' % (_runlevel(), name)): + rc = _root('/etc/rc{}.d/S*{}'.format(_runlevel(), name), root) + for match in glob.glob(rc): if re.match(r'S\d{,2}%s' % name, os.path.basename(match)): return True return False -def _untracked_custom_unit_found(name): +def _untracked_custom_unit_found(name, root=None): ''' If the passed service name is not available, but a unit file exist in /etc/systemd/system, return True. Otherwise, return False. ''' - unit_path = os.path.join('/etc/systemd/system', - _canonical_unit_name(name)) + system = _root('/etc/systemd/system', root) + unit_path = os.path.join(system, _canonical_unit_name(name)) return os.access(unit_path, os.R_OK) and not _check_available(name) @@ -371,7 +387,8 @@ def _unit_file_changed(name): Returns True if systemctl reports that the unit file has changed, otherwise returns False. ''' - return "'systemctl daemon-reload'" in _systemctl_status(name)['stdout'].lower() + status = _systemctl_status(name)['stdout'].lower() + return "'systemctl daemon-reload'" in status def systemctl_reload(): @@ -389,8 +406,7 @@ def systemctl_reload(): out = __salt__['cmd.run_all']( _systemctl_cmd('--system daemon-reload'), python_shell=False, - redirect_stderr=True - ) + redirect_stderr=True) if out['retcode'] != 0: raise CommandExecutionError( 'Problem performing systemctl daemon-reload: %s' % out['stdout'] @@ -414,8 +430,7 @@ def get_running(): out = __salt__['cmd.run']( _systemctl_cmd('--full --no-legend --no-pager'), python_shell=False, - ignore_retcode=True, - ) + ignore_retcode=True) for line in salt.utils.itertools.split(out, '\n'): try: comps = line.strip().split() @@ -438,10 +453,13 @@ def get_running(): return sorted(ret) -def get_enabled(): +def get_enabled(root=None): ''' Return a list of all enabled services + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -452,10 +470,10 @@ def get_enabled(): # Get enabled systemd units. Can't use --state=enabled here because it's # not present until systemd 216. out = __salt__['cmd.run']( - _systemctl_cmd('--full --no-legend --no-pager list-unit-files'), + _systemctl_cmd('--full --no-legend --no-pager list-unit-files', + root=root), python_shell=False, - ignore_retcode=True, - ) + ignore_retcode=True) for line in salt.utils.itertools.split(out, '\n'): try: fullname, unit_state = line.strip().split(None, 1) @@ -473,15 +491,18 @@ def get_enabled(): # Add in any sysvinit services that are enabled ret.update(set( - [x for x in _get_sysv_services() if _sysv_enabled(x)] + [x for x in _get_sysv_services(root) if _sysv_enabled(x, root)] )) return sorted(ret) -def get_disabled(): +def get_disabled(root=None): ''' Return a list of all disabled services + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -492,10 +513,10 @@ def get_disabled(): # Get disabled systemd units. Can't use --state=disabled here because it's # not present until systemd 216. out = __salt__['cmd.run']( - _systemctl_cmd('--full --no-legend --no-pager list-unit-files'), + _systemctl_cmd('--full --no-legend --no-pager list-unit-files', + root=root), python_shell=False, - ignore_retcode=True, - ) + ignore_retcode=True) for line in salt.utils.itertools.split(out, '\n'): try: fullname, unit_state = line.strip().split(None, 1) @@ -513,17 +534,20 @@ def get_disabled(): # Add in any sysvinit services that are disabled ret.update(set( - [x for x in _get_sysv_services() if not _sysv_enabled(x)] + [x for x in _get_sysv_services(root) if not _sysv_enabled(x, root)] )) return sorted(ret) -def get_static(): +def get_static(root=None): ''' .. versionadded:: 2015.8.5 Return a list of all static services + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -534,10 +558,10 @@ def get_static(): # Get static systemd units. Can't use --state=static here because it's # not present until systemd 216. out = __salt__['cmd.run']( - _systemctl_cmd('--full --no-legend --no-pager list-unit-files'), + _systemctl_cmd('--full --no-legend --no-pager list-unit-files', + root=root), python_shell=False, - ignore_retcode=True, - ) + ignore_retcode=True) for line in salt.utils.itertools.split(out, '\n'): try: fullname, unit_state = line.strip().split(None, 1) @@ -557,18 +581,21 @@ def get_static(): return sorted(ret) -def get_all(): +def get_all(root=None): ''' Return a list of all available services + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash salt '*' service.get_all ''' - ret = _get_systemd_services() - ret.update(set(_get_sysv_services(systemd_services=ret))) + ret = _get_systemd_services(root) + ret.update(set(_get_sysv_services(root, systemd_services=ret))) return sorted(ret) @@ -606,7 +633,7 @@ def missing(name): return not available(name) -def unmask_(name, runtime=False): +def unmask_(name, runtime=False, root=None): ''' .. versionadded:: 2015.5.0 .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 @@ -633,6 +660,9 @@ def unmask_(name, runtime=False): removes a runtime mask only when this argument is set to ``True``, and otherwise removes an indefinite mask. + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -641,15 +671,16 @@ def unmask_(name, runtime=False): salt '*' service.unmask foo runtime=True ''' _check_for_unit_changes(name) - if not masked(name, runtime): + if not masked(name, runtime, root=root): log.debug('Service \'%s\' is not %smasked', name, 'runtime-' if runtime else '') return True cmd = 'unmask --runtime' if runtime else 'unmask' - out = __salt__['cmd.run_all'](_systemctl_cmd(cmd, name, systemd_scope=True), - python_shell=False, - redirect_stderr=True) + out = __salt__['cmd.run_all']( + _systemctl_cmd(cmd, name, systemd_scope=True, root=root), + python_shell=False, + redirect_stderr=True) if out['retcode'] != 0: raise CommandExecutionError('Failed to unmask service \'%s\'' % name) @@ -657,7 +688,7 @@ def unmask_(name, runtime=False): return True -def mask(name, runtime=False): +def mask(name, runtime=False, root=None): ''' .. versionadded:: 2015.5.0 .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 @@ -678,6 +709,9 @@ def mask(name, runtime=False): .. versionadded:: 2015.8.5 + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -688,9 +722,10 @@ def mask(name, runtime=False): _check_for_unit_changes(name) cmd = 'mask --runtime' if runtime else 'mask' - out = __salt__['cmd.run_all'](_systemctl_cmd(cmd, name, systemd_scope=True), - python_shell=False, - redirect_stderr=True) + out = __salt__['cmd.run_all']( + _systemctl_cmd(cmd, name, systemd_scope=True, root=root), + python_shell=False, + redirect_stderr=True) if out['retcode'] != 0: raise CommandExecutionError( @@ -701,7 +736,7 @@ def mask(name, runtime=False): return True -def masked(name, runtime=False): +def masked(name, runtime=False, root=None): ''' .. versionadded:: 2015.8.0 .. versionchanged:: 2015.8.5 @@ -731,6 +766,9 @@ def masked(name, runtime=False): only checks for runtime masks if this argument is set to ``True``. Otherwise, it will check for an indefinite mask. + root + Enable/disable/mask unit files in the specified root directory + CLI Examples: .. code-block:: bash @@ -739,7 +777,7 @@ def masked(name, runtime=False): salt '*' service.masked foo runtime=True ''' _check_for_unit_changes(name) - root_dir = '/run' if runtime else '/etc' + root_dir = _root('/run' if runtime else '/etc', root) link_path = os.path.join(root_dir, 'systemd', 'system', @@ -1055,9 +1093,10 @@ def status(name, sig=None): # pylint: disable=unused-argument results = {} for service in services: _check_for_unit_changes(service) - results[service] = __salt__['cmd.retcode'](_systemctl_cmd('is-active', service), - python_shell=False, - ignore_retcode=True) == 0 + results[service] = __salt__['cmd.retcode']( + _systemctl_cmd('is-active', service), + python_shell=False, + ignore_retcode=True) == 0 if contains_globbing: return results return results[name] @@ -1065,7 +1104,8 @@ def status(name, sig=None): # pylint: disable=unused-argument # **kwargs is required to maintain consistency with the API established by # Salt's service management states. -def enable(name, no_block=False, unmask=False, unmask_runtime=False, **kwargs): # pylint: disable=unused-argument +def enable(name, no_block=False, unmask=False, unmask_runtime=False, + root=None, **kwargs): # pylint: disable=unused-argument ''' .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 On minions running systemd>=205, `systemd-run(1)`_ is now used to @@ -1101,6 +1141,9 @@ def enable(name, no_block=False, unmask=False, unmask_runtime=False, **kwargs): In previous releases, Salt would simply unmask a service before enabling. This behavior is no longer the default. + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -1108,8 +1151,8 @@ def enable(name, no_block=False, unmask=False, unmask_runtime=False, **kwargs): salt '*' service.enable ''' _check_for_unit_changes(name) - _check_unmask(name, unmask, unmask_runtime) - if name in _get_sysv_services(): + _check_unmask(name, unmask, unmask_runtime, root) + if name in _get_sysv_services(root): cmd = [] if salt.utils.systemd.has_scope(__context__) \ and __salt__['config.get']('systemd.scope', True): @@ -1123,7 +1166,8 @@ def enable(name, no_block=False, unmask=False, unmask_runtime=False, **kwargs): python_shell=False, ignore_retcode=True) == 0 ret = __salt__['cmd.run_all']( - _systemctl_cmd('enable', name, systemd_scope=True, no_block=no_block), + _systemctl_cmd('enable', name, systemd_scope=True, no_block=no_block, + root=root), python_shell=False, ignore_retcode=True) @@ -1137,7 +1181,7 @@ def enable(name, no_block=False, unmask=False, unmask_runtime=False, **kwargs): # The unused kwargs argument is required to maintain consistency with the API # established by Salt's service management states. -def disable(name, no_block=False, **kwargs): # pylint: disable=unused-argument +def disable(name, no_block=False, root=None, **kwargs): # pylint: disable=unused-argument ''' .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 On minions running systemd>=205, `systemd-run(1)`_ is now used to @@ -1157,6 +1201,9 @@ def disable(name, no_block=False, **kwargs): # pylint: disable=unused-argument .. versionadded:: 2017.7.0 + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -1164,7 +1211,7 @@ def disable(name, no_block=False, **kwargs): # pylint: disable=unused-argument salt '*' service.disable ''' _check_for_unit_changes(name) - if name in _get_sysv_services(): + if name in _get_sysv_services(root): cmd = [] if salt.utils.systemd.has_scope(__context__) \ and __salt__['config.get']('systemd.scope', True): @@ -1179,17 +1226,21 @@ def disable(name, no_block=False, **kwargs): # pylint: disable=unused-argument ignore_retcode=True) == 0 # Using cmd.run_all instead of cmd.retcode here to make unit tests easier return __salt__['cmd.run_all']( - _systemctl_cmd('disable', name, systemd_scope=True, no_block=no_block), + _systemctl_cmd('disable', name, systemd_scope=True, no_block=no_block, + root=root), python_shell=False, ignore_retcode=True)['retcode'] == 0 # The unused kwargs argument is required to maintain consistency with the API # established by Salt's service management states. -def enabled(name, **kwargs): # pylint: disable=unused-argument +def enabled(name, root=None, **kwargs): # pylint: disable=unused-argument ''' Return if the named service is enabled to start on boot + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash @@ -1199,7 +1250,7 @@ def enabled(name, **kwargs): # pylint: disable=unused-argument # Try 'systemctl is-enabled' first, then look for a symlink created by # systemctl (older systemd releases did not support using is-enabled to # check templated services), and lastly check for a sysvinit service. - if __salt__['cmd.retcode'](_systemctl_cmd('is-enabled', name), + if __salt__['cmd.retcode'](_systemctl_cmd('is-enabled', name, root=root), python_shell=False, ignore_retcode=True) == 0: return True @@ -1207,43 +1258,50 @@ def enabled(name, **kwargs): # pylint: disable=unused-argument # On older systemd releases, templated services could not be checked # with ``systemctl is-enabled``. As a fallback, look for the symlinks # created by systemctl when enabling templated services. - cmd = ['find', LOCAL_CONFIG_PATH, '-name', name, + local_config_path = _root(LOCAL_CONFIG_PATH, '/') + cmd = ['find', local_config_path, '-name', name, '-type', 'l', '-print', '-quit'] # If the find command returns any matches, there will be output and the # string will be non-empty. if bool(__salt__['cmd.run'](cmd, python_shell=False)): return True - elif name in _get_sysv_services(): - return _sysv_enabled(name) + elif name in _get_sysv_services(root): + return _sysv_enabled(name, root) return False -def disabled(name): +def disabled(name, root=None): ''' Return if the named service is disabled from starting on boot + root + Enable/disable/mask unit files in the specified root directory + CLI Example: .. code-block:: bash salt '*' service.disabled ''' - return not enabled(name) + return not enabled(name, root=root) -def show(name): +def show(name, root=None): ''' .. versionadded:: 2014.7.0 Show properties of one or more units/jobs or the manager + root + Enable/disable/mask unit files in the specified root directory + CLI Example: salt '*' service.show ''' ret = {} - out = __salt__['cmd.run'](_systemctl_cmd('show', name), + out = __salt__['cmd.run'](_systemctl_cmd('show', name, root=root), python_shell=False) for line in salt.utils.itertools.split(out, '\n'): comps = line.split('=') @@ -1263,19 +1321,22 @@ def show(name): return ret -def execs(): +def execs(root=None): ''' .. versionadded:: 2014.7.0 Return a list of all files specified as ``ExecStart`` for all services. + root + Enable/disable/mask unit files in the specified root directory + CLI Example: salt '*' service.execs ''' ret = {} - for service in get_all(): - data = show(service) + for service in get_all(root=root): + data = show(service, root=root) if 'ExecStart' not in data: continue ret[service] = data['ExecStart']['path'] diff --git a/salt/modules/useradd.py b/salt/modules/useradd.py index e370dd4bb31..e38a094ed28 100644 --- a/salt/modules/useradd.py +++ b/salt/modules/useradd.py @@ -17,6 +17,8 @@ HAS_PWD = False import logging import copy +import functools +import os # Import salt libs import salt.utils.data @@ -55,12 +57,17 @@ def _quote_username(name): return salt.utils.stringutils.to_str(name) -def _get_gecos(name): +def _get_gecos(name, root=None): ''' Retrieve GECOS field info and return it in dictionary form ''' + if root is not None and __grains__['kernel'] != 'AIX': + getpwnam = functools.partial(_getpwnam, root=root) + else: + getpwnam = functools.partial(pwd.getpwnam) gecos_field = salt.utils.stringutils.to_unicode( - pwd.getpwnam(_quote_username(name)).pw_gecos).split(',', 4) + getpwnam(_quote_username(name)).pw_gecos).split(',', 4) + if not gecos_field: return {} else: @@ -96,7 +103,7 @@ def _update_gecos(name, key, value, root=None): value = six.text_type(value) else: value = salt.utils.stringutils.to_unicode(value) - pre_info = _get_gecos(name) + pre_info = _get_gecos(name, root=root) if not pre_info: return False if value == pre_info[key]: @@ -104,14 +111,13 @@ def _update_gecos(name, key, value, root=None): gecos_data = copy.deepcopy(pre_info) gecos_data[key] = value - cmd = ['usermod', '-c', _build_gecos(gecos_data), name] - + cmd = ['usermod'] if root is not None and __grains__['kernel'] != 'AIX': cmd.extend(('-R', root)) + cmd.extend(('-c', _build_gecos(gecos_data), name)) __salt__['cmd.run'](cmd, python_shell=False) - post_info = info(name) - return _get_gecos(name).get(key) == value + return _get_gecos(name, root=root).get(key) == value def add(name, @@ -129,11 +135,62 @@ def add(name, other='', createhome=True, loginclass=None, - root=None, - nologinit=False): + nologinit=False, + root=None): ''' Add a user to the minion + name + Username LOGIN to add + + uid + User ID of the new account + + gid + Name or ID of the primary group of the new accoun + + groups + List of supplementary groups of the new account + + home + Home directory of the new account + + shell + Login shell of the new account + + unique + Allow to create users with duplicate + + system + Create a system account + + fullname + GECOS field for the full name + + roomnumber + GECOS field for the room number + + workphone + GECOS field for the work phone + + homephone + GECOS field for the home phone + + other + GECOS field for other information + + createhome + Create the user's home directory + + loginclass + Login class for the new account (OpenBSD) + + nologinit + Do not add the user to the lastlog and faillog databases + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -231,17 +288,17 @@ def add(name, # user does exist, and B) running useradd again would result in a # nonzero exit status and be interpreted as a False result. if groups: - chgroups(name, groups) + chgroups(name, groups, root=root) if fullname: - chfullname(name, fullname) + chfullname(name, fullname, root=root) if roomnumber: - chroomnumber(name, roomnumber) + chroomnumber(name, roomnumber, root=root) if workphone: - chworkphone(name, workphone) + chworkphone(name, workphone, root=root) if homephone: - chhomephone(name, homephone) + chhomephone(name, homephone, root=root) if other: - chother(name, other) + chother(name, other, root=root) return True @@ -249,6 +306,18 @@ def delete(name, remove=False, force=False, root=None): ''' Remove a user from the minion + name + Username to delete + + remove + Remove home directory and mail spool + + force + Force some actions that would fail otherwise + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -292,10 +361,16 @@ def delete(name, remove=False, force=False, root=None): return False -def getent(refresh=False): +def getent(refresh=False, root=None): ''' Return the list of all info for all users + refresh + Force a refresh of user information + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -306,72 +381,106 @@ def getent(refresh=False): return __context__['user.getent'] ret = [] - for data in pwd.getpwall(): + if root is not None and __grains__['kernel'] != 'AIX': + getpwall = functools.partial(_getpwall, root=root) + else: + getpwall = functools.partial(pwd.getpwall) + + for data in getpwall(): ret.append(_format_info(data)) __context__['user.getent'] = ret return ret -def chuid(name, uid): +def _chattrib(name, key, value, param, persist=False, root=None): + ''' + Change an attribute for a named user + ''' + pre_info = info(name, root=root) + if not pre_info: + raise CommandExecutionError('User \'{0}\' does not exist'.format(name)) + + if value == pre_info[key]: + return True + + cmd = ['usermod'] + + if root is not None and __grains__['kernel'] != 'AIX': + cmd.extend(('-R', root)) + + if persist and __grains__['kernel'] != 'OpenBSD': + cmd.append('-m') + + cmd.extend((param, value, name)) + + __salt__['cmd.run'](cmd, python_shell=False) + return info(name, root=root).get(key) == value + + +def chuid(name, uid, root=None): ''' Change the uid for a named user + name + User to modify + + uid + New UID for the user account + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chuid foo 4376 ''' - pre_info = info(name) - if uid == pre_info['uid']: - return True - cmd = ['usermod', '-u', uid, name] - __salt__['cmd.run'](cmd, python_shell=False) - return info(name).get('uid') == uid + return _chattrib(name, 'uid', uid, '-u', root=root) def chgid(name, gid, root=None): ''' Change the default group of the user + name + User to modify + + gid + Force use GID as new primary group + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chgid foo 4376 ''' - pre_info = info(name) - if gid == pre_info['gid']: - return True - cmd = ['usermod', '-g', gid, name] - - if root is not None and __grains__['kernel'] != 'AIX': - cmd.extend(('-R', root)) - - __salt__['cmd.run'](cmd, python_shell=False) - return info(name).get('gid') == gid + return _chattrib(name, 'gid', gid, '-g', root=root) def chshell(name, shell, root=None): ''' Change the default shell of the user + name + User to modify + + shell + New login shell for the user account + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chshell foo /bin/zsh ''' - pre_info = info(name) - if shell == pre_info['shell']: - return True - cmd = ['usermod', '-s', shell, name] - - if root is not None and __grains__['kernel'] != 'AIX': - cmd.extend(('-R', root)) - - __salt__['cmd.run'](cmd, python_shell=False) - return info(name).get('shell') == shell + return _chattrib(name, 'shell', shell, '-s', root=root) def chhome(name, home, persist=False, root=None): @@ -379,25 +488,25 @@ def chhome(name, home, persist=False, root=None): Change the home directory of the user, pass True for persist to move files to the new home directory if the old home directory exist. + name + User to modify + + home + New home directory for the user account + + presist + Move contents of the home directory to the new location + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chhome foo /home/users/foo True ''' - pre_info = info(name) - if home == pre_info['home']: - return True - cmd = ['usermod', '-d', home] - - if root is not None and __grains__['kernel'] != 'AIX': - cmd.extend(('-R', root)) - - if persist and __grains__['kernel'] != 'OpenBSD': - cmd.append('-m') - cmd.append(name) - __salt__['cmd.run'](cmd, python_shell=False) - return info(name).get('home') == home + return _chattrib(name, 'home', home, '-d', persist=persist, root=root) def chgroups(name, groups, append=False, root=None): @@ -414,6 +523,9 @@ def chgroups(name, groups, append=False, root=None): If ``True``, append the specified group(s). Otherwise, this function will replace the user's groups with the specified group(s). + root + Directory to chroot into + CLI Examples: .. code-block:: bash @@ -460,20 +572,29 @@ def chgroups(name, groups, append=False, root=None): return result['retcode'] == 0 -def chfullname(name, fullname): +def chfullname(name, fullname, root=None): ''' Change the user's Full Name + name + User to modify + + fullname + GECOS field for the full name + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chfullname foo "Foo Bar" ''' - return _update_gecos(name, 'fullname', fullname) + return _update_gecos(name, 'fullname', fullname, root=root) -def chroomnumber(name, roomnumber): +def chroomnumber(name, roomnumber, root=None): ''' Change the user's Room Number @@ -483,52 +604,88 @@ def chroomnumber(name, roomnumber): salt '*' user.chroomnumber foo 123 ''' - return _update_gecos(name, 'roomnumber', roomnumber) + return _update_gecos(name, 'roomnumber', roomnumber, root=root) -def chworkphone(name, workphone): +def chworkphone(name, workphone, root=None): ''' Change the user's Work Phone + name + User to modify + + workphone + GECOS field for the work phone + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chworkphone foo 7735550123 ''' - return _update_gecos(name, 'workphone', workphone) + return _update_gecos(name, 'workphone', workphone, root=root) -def chhomephone(name, homephone): +def chhomephone(name, homephone, root=None): ''' Change the user's Home Phone + name + User to modify + + homephone + GECOS field for the home phone + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chhomephone foo 7735551234 ''' - return _update_gecos(name, 'homephone', homephone) + return _update_gecos(name, 'homephone', homephone, root=root) -def chother(name, other): +def chother(name, other, root=None): ''' Change the user's other GECOS attribute + name + User to modify + + other + GECOS field for other information + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chother foobar ''' - return _update_gecos(name, 'other', other) + return _update_gecos(name, 'other', other, root=root) def chloginclass(name, loginclass, root=None): ''' Change the default login class of the user + name + User to modify + + loginclass + Login class for the new account + + root + Directory to chroot into + .. note:: This function only applies to OpenBSD systems. @@ -546,25 +703,43 @@ def chloginclass(name, loginclass, root=None): cmd = ['usermod', '-L', loginclass, name] - if root is not None: + if root is not None and __grains__['kernel'] != 'AIX': cmd.extend(('-R', root)) __salt__['cmd.run'](cmd, python_shell=False) return get_loginclass(name) == loginclass -def info(name): +def info(name, root=None): ''' Return user information + name + User to get the information + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.info root ''' + # If root is provided, we use a less portable solution that + # depends on analyzing /etc/passwd manually. Of course we cannot + # find users from NIS nor LDAP, but in those cases do not makes + # sense to provide a root parameter. + # + # Please, note that if the non-root /etc/passwd file is long the + # iteration can be slow. + if root is not None and __grains__['kernel'] != 'AIX': + getpwnam = functools.partial(_getpwnam, root=root) + else: + getpwnam = functools.partial(pwd.getpwnam) + try: - data = pwd.getpwnam(_quote_username(name)) + data = getpwnam(_quote_username(name)) except KeyError: return {} else: @@ -575,6 +750,9 @@ def get_loginclass(name): ''' Get the login class of the user + name + User to get the information + .. note:: This function only applies to OpenBSD systems. @@ -632,6 +810,9 @@ def primary_group(name): .. versionadded:: 2016.3.0 + name + User to get the information + CLI Example: .. code-block:: bash @@ -645,6 +826,9 @@ def list_groups(name): ''' Return a list of groups the named user belongs to + name + User to get the information + CLI Example: .. code-block:: bash @@ -654,43 +838,79 @@ def list_groups(name): return salt.utils.user.get_group_list(name) -def list_users(): +def list_users(root=None): ''' Return a list of all users + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.list_users ''' - return sorted([user.pw_name for user in pwd.getpwall()]) + if root is not None and __grains__['kernel'] != 'AIX': + getpwall = functools.partial(_getpwall, root=root) + else: + getpwall = functools.partial(pwd.getpwall) + + return sorted([user.pw_name for user in getpwall()]) def rename(name, new_name, root=None): ''' Change the username for a named user + name + User to modify + + new_name + New value of the login name + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.rename name new_name ''' - current_info = info(name) - if not current_info: - raise CommandExecutionError('User \'{0}\' does not exist'.format(name)) + if info(new_name, root=root): + raise CommandExecutionError('User \'{0}\' already exists'.format(new_name)) - new_info = info(new_name) - if new_info: - raise CommandExecutionError( - 'User \'{0}\' already exists'.format(new_name) - ) + return _chattrib(name, 'name', new_name, '-l', root=root) - cmd = ['usermod', '-l', new_name, name] - if root is not None and __grains__['kernel'] != 'AIX': - cmd.extend(('-R', root)) +def _getpwnam(name, root=None): + ''' + Alternative implementation for getpwnam, that use only /etc/passwd + ''' + root = '/' if not root else root + passwd = os.path.join(root, 'etc/passwd') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + if comps[0] == name: + # Generate a getpwnam compatible output + comps[2], comps[3] = int(comps[2]), int(comps[3]) + return pwd.struct_passwd(comps) + raise KeyError - __salt__['cmd.run'](cmd, python_shell=False) - return info(name).get('name') == new_name + +def _getpwall(root=None): + ''' + Alternative implemetantion for getpwall, that use only /etc/passwd + ''' + root = '/' if not root else root + passwd = os.path.join(root, 'etc/passwd') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + # Generate a getpwall compatible output + comps[2], comps[3] = int(comps[2]), int(comps[3]) + yield pwd.struct_passwd(comps) diff --git a/salt/modules/xbpspkg.py b/salt/modules/xbpspkg.py index e493f8c80fd..b5d7d8a4778 100644 --- a/salt/modules/xbpspkg.py +++ b/salt/modules/xbpspkg.py @@ -121,7 +121,7 @@ def list_pkgs(versions_as_list=False, **kwargs): return ret -def list_upgrades(refresh=True): +def list_upgrades(refresh=True, **kwargs): ''' Check whether or not an upgrade is available for all packages @@ -247,7 +247,7 @@ def latest_version(*names, **kwargs): available_version = latest_version -def upgrade_available(name): +def upgrade_available(name, **kwargs): ''' Check whether or not an upgrade is available for a given package @@ -260,7 +260,7 @@ def upgrade_available(name): return latest_version(name) != '' -def refresh_db(): +def refresh_db(**kwargs): ''' Update list of available packages from installed repos @@ -300,7 +300,7 @@ def version(*names, **kwargs): return __salt__['pkg_resource.version'](*names, **kwargs) -def upgrade(refresh=True): +def upgrade(refresh=True, **kwargs): ''' Run a full system upgrade @@ -484,7 +484,7 @@ def remove(name=None, pkgs=None, recursive=True, **kwargs): return salt.utils.data.compare_dicts(old, new) -def list_repos(): +def list_repos(**kwargs): ''' List all repos known by XBPS @@ -607,7 +607,7 @@ def add_repo(repo, conffile='/usr/share/xbps.d/15-saltstack.conf'): return True -def del_repo(repo): +def del_repo(repo, **kwargs): ''' Remove an XBPS repository from the system. diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py index 68fe9edd626..405c2f9fa65 100644 --- a/salt/modules/yumpkg.py +++ b/salt/modules/yumpkg.py @@ -619,7 +619,7 @@ def version(*names, **kwargs): return __salt__['pkg_resource.version'](*names, **kwargs) -def version_cmp(pkg1, pkg2, ignore_epoch=False): +def version_cmp(pkg1, pkg2, ignore_epoch=False, **kwargs): ''' .. versionadded:: 2015.5.4 @@ -1012,7 +1012,7 @@ def list_upgrades(refresh=True, **kwargs): list_updates = salt.utils.functools.alias_function(list_upgrades, 'list_updates') -def list_downloaded(): +def list_downloaded(**kwargs): ''' .. versionadded:: 2017.7.0 @@ -1948,13 +1948,13 @@ def upgrade(name=None, def update(name=None, - pkgs=None, - refresh=True, - skip_verify=False, - normalize=True, - minimal=False, - obsoletes=False, - **kwargs): + pkgs=None, + refresh=True, + skip_verify=False, + normalize=True, + minimal=False, + obsoletes=False, + **kwargs): ''' .. versionadded:: 2019.2.0 @@ -2647,7 +2647,7 @@ def group_install(name, groupinstall = salt.utils.functools.alias_function(group_install, 'groupinstall') -def list_repos(basedir=None): +def list_repos(basedir=None, **kwargs): ''' Lists all repos in (default: all dirs in `reposdir` yum option). @@ -2969,7 +2969,7 @@ def _parse_repo_file(filename): return (headers, salt.utils.data.decode(config)) -def file_list(*packages): +def file_list(*packages, **kwargs): ''' .. versionadded:: 2014.1.0 @@ -2988,7 +2988,7 @@ def file_list(*packages): return __salt__['lowpkg.file_list'](*packages) -def file_dict(*packages): +def file_dict(*packages, **kwargs): ''' .. versionadded:: 2014.1.0 @@ -3007,7 +3007,7 @@ def file_dict(*packages): return __salt__['lowpkg.file_dict'](*packages) -def owner(*paths): +def owner(*paths, **kwargs): ''' .. versionadded:: 2014.7.0 @@ -3095,7 +3095,7 @@ def modified(*packages, **flags): @salt.utils.decorators.path.which('yumdownloader') -def download(*packages): +def download(*packages, **kwargs): ''' .. versionadded:: 2015.5.0 @@ -3168,7 +3168,7 @@ def download(*packages): return ret -def diff(*paths): +def diff(*paths, **kwargs): ''' Return a formatted diff between current files and original in a package. NOTE: this function includes all files (configuration and not), but does @@ -3235,7 +3235,7 @@ def _get_patches(installed_only=False): return patches -def list_patches(refresh=False): +def list_patches(refresh=False, **kwargs): ''' .. versionadded:: 2017.7.0 @@ -3258,7 +3258,7 @@ def list_patches(refresh=False): return _get_patches() -def list_installed_patches(): +def list_installed_patches(**kwargs): ''' .. versionadded:: 2017.7.0 diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py index 7ac0df26c62..de45df90f67 100644 --- a/salt/modules/zypperpkg.py +++ b/salt/modules/zypperpkg.py @@ -99,6 +99,7 @@ class _Zypper(object): LOCK_EXIT_CODE = 7 XML_DIRECTIVES = ['-x', '--xmlout'] + # ZYPPER_LOCK is not affected by --root ZYPPER_LOCK = '/var/run/zypp.pid' TAG_RELEASED = 'zypper/released' TAG_BLOCKED = 'zypper/blocked' @@ -107,7 +108,6 @@ def __init__(self): ''' Constructor ''' - self.__called = False self._reset() def _reset(self): @@ -129,6 +129,10 @@ def _reset(self): self.__refresh = False self.__ignore_repo_failure = False self.__systemd_scope = False + self.__root = None + + # Call status + self.__called = False def __call__(self, *args, **kwargs): ''' @@ -136,11 +140,17 @@ def __call__(self, *args, **kwargs): :param kwargs: :return: ''' + # Reset after the call + if self.__called: + self._reset() + # Ignore exit code for 106 (repo is not available) if 'no_repo_failure' in kwargs: self.__ignore_repo_failure = kwargs['no_repo_failure'] if 'systemd_scope' in kwargs: self.__systemd_scope = kwargs['systemd_scope'] + if 'root' in kwargs: + self.__root = kwargs['root'] return self def __getattr__(self, item): @@ -153,7 +163,6 @@ def __getattr__(self, item): # Reset after the call if self.__called: self._reset() - self.__called = False if item == 'xml': self.__xml = True @@ -284,6 +293,8 @@ def __call(self, *args, **kwargs): self.__cmd.append('--xmlout') if not self.__refresh and '--no-refresh' not in args: self.__cmd.append('--no-refresh') + if self.__root: + self.__cmd.extend(['--root', self.__root]) self.__cmd.extend(args) kwargs['output_loglevel'] = 'trace' @@ -442,7 +453,7 @@ def _clean_cache(): __context__.pop(cache_name, None) -def list_upgrades(refresh=True, **kwargs): +def list_upgrades(refresh=True, root=None, **kwargs): ''' List all available package upgrades on this system @@ -451,6 +462,9 @@ def list_upgrades(refresh=True, **kwargs): If set to False it depends on zypper if a refresh is executed. + root + operate on a different root directory. + CLI Example: .. code-block:: bash @@ -458,7 +472,7 @@ def list_upgrades(refresh=True, **kwargs): salt '*' pkg.list_upgrades ''' if refresh: - refresh_db() + refresh_db(root) ret = dict() cmd = ['list-updates'] @@ -467,7 +481,7 @@ def list_upgrades(refresh=True, **kwargs): if not isinstance(repo_name, six.string_types): repo_name = six.text_type(repo_name) cmd.extend(['--repo', repo_name]) - for update_node in __zypper__.nolock.xml.call(*cmd).getElementsByTagName('update'): + for update_node in __zypper__(root=root).nolock.xml.call(*cmd).getElementsByTagName('update'): if update_node.getAttribute('kind') == 'package': ret[update_node.getAttribute('name')] = update_node.getAttribute('edition') @@ -504,6 +518,9 @@ def info_installed(*names, **kwargs): :param all_versions: Include information for all versions of the packages installed on the minion. + :param root: + Operate on a different root directory. + CLI example: .. code-block:: bash @@ -544,6 +561,9 @@ def info_available(*names, **kwargs): If set to False it depends on zypper if a refresh is executed or not. + root + operate on a different root directory. + CLI example: .. code-block:: bash @@ -558,9 +578,11 @@ def info_available(*names, **kwargs): else: names = sorted(list(set(names))) + root = kwargs.get('root', None) + # Refresh db before extracting the latest package if kwargs.get('refresh', True): - refresh_db() + refresh_db(root) pkg_info = [] batch = names[:] @@ -569,7 +591,8 @@ def info_available(*names, **kwargs): # Run in batches while batch: pkg_info.extend(re.split(r"Information for package*", - __zypper__.nolock.call('info', '-t', 'package', *batch[:batch_size]))) + __zypper__(root=root).nolock.call('info', '-t', 'package', + *batch[:batch_size]))) batch = batch[batch_size:] for pkg_data in pkg_info: @@ -629,6 +652,9 @@ def latest_version(*names, **kwargs): If set to False it depends on zypper if a refresh is executed or not. + root + operate on a different root directory. + CLI example: .. code-block:: bash @@ -671,6 +697,9 @@ def upgrade_available(name, **kwargs): If set to False it depends on zypper if a refresh is executed or not. + root + operate on a different root directory. + CLI Example: .. code-block:: bash @@ -687,6 +716,9 @@ def version(*names, **kwargs): installed. If more than one package name is specified, a dict of name/version pairs is returned. + root + operate on a different root directory. + CLI Example: .. code-block:: bash @@ -697,7 +729,7 @@ def version(*names, **kwargs): return __salt__['pkg_resource.version'](*names, **kwargs) or {} -def version_cmp(ver1, ver2, ignore_epoch=False): +def version_cmp(ver1, ver2, ignore_epoch=False, **kwargs): ''' .. versionadded:: 2015.5.4 @@ -719,7 +751,7 @@ def version_cmp(ver1, ver2, ignore_epoch=False): return __salt__['lowpkg.version_cmp'](ver1, ver2, ignore_epoch=ignore_epoch) -def list_pkgs(versions_as_list=False, **kwargs): +def list_pkgs(versions_as_list=False, root=None, **kwargs): ''' List the packages currently installed as a dict. By default, the dict contains versions as a comma separated string:: @@ -731,6 +763,9 @@ def list_pkgs(versions_as_list=False, **kwargs): {'': ['', '']} + root: + operate on a different root directory. + attr: If a list of package attributes is specified, returned value will contain them in addition to version, eg.:: @@ -770,10 +805,14 @@ def list_pkgs(versions_as_list=False, **kwargs): contextkey = 'pkg.list_pkgs' + # TODO(aplanas): this cached value depends on the parameters if contextkey not in __context__: ret = {} - cmd = ['rpm', '-qa', '--queryformat', - salt.utils.pkg.rpm.QUERYFORMAT.replace('%{REPOID}', '(none)') + '\n'] + cmd = ['rpm'] + if root: + cmd.extend(['--root', root]) + cmd.extend(['-qa', '--queryformat', + salt.utils.pkg.rpm.QUERYFORMAT.replace('%{REPOID}', '(none)') + '\n']) output = __salt__['cmd.run'](cmd, python_shell=False, output_loglevel='trace') @@ -859,6 +898,9 @@ def list_repo_pkgs(*args, **kwargs): When ``True``, the return data for each package will be organized by repository. + root + operate on a different root directory. + CLI Examples: .. code-block:: bash @@ -891,7 +933,8 @@ def _is_match(pkgname): return True return False - for node in __zypper__.xml.call('se', '-s', *targets).getElementsByTagName('solvable'): + root = kwargs.get('root') or None + for node in __zypper__(root=root).xml.call('se', '-s', *targets).getElementsByTagName('solvable'): pkginfo = dict(node.attributes.items()) try: if pkginfo['kind'] != 'package': @@ -933,23 +976,27 @@ def _is_match(pkgname): return byrepo_ret -def _get_configured_repos(): +def _get_configured_repos(root=None): ''' Get all the info about repositories from the configurations. ''' + repos = os.path.join(root, os.path.relpath(REPOS, os.path.sep)) if root else REPOS repos_cfg = configparser.ConfigParser() - repos_cfg.read([REPOS + '/' + fname for fname in os.listdir(REPOS) if fname.endswith(".repo")]) + if os.path.exists(repos): + repos_cfg.read([repos + '/' + fname for fname in os.listdir(repos) if fname.endswith(".repo")]) + else: + log.warning('Repositories not found in {}'.format(repos)) return repos_cfg -def _get_repo_info(alias, repos_cfg=None): +def _get_repo_info(alias, repos_cfg=None, root=None): ''' Get one repo meta-data. ''' try: - meta = dict((repos_cfg or _get_configured_repos()).items(alias)) + meta = dict((repos_cfg or _get_configured_repos(root=root)).items(alias)) meta['alias'] = alias for key, val in six.iteritems(meta): if val in ['0', '1']: @@ -961,51 +1008,60 @@ def _get_repo_info(alias, repos_cfg=None): return {} -def get_repo(repo, **kwargs): # pylint: disable=unused-argument +def get_repo(repo, root=None, **kwargs): # pylint: disable=unused-argument ''' Display a repo. + root + operate on a different root directory. + CLI Example: .. code-block:: bash salt '*' pkg.get_repo alias ''' - return _get_repo_info(repo) + return _get_repo_info(repo, root=root) -def list_repos(): +def list_repos(root=None, **kwargs): ''' Lists all repos. + root + operate on a different root directory. + CLI Example: .. code-block:: bash salt '*' pkg.list_repos ''' - repos_cfg = _get_configured_repos() + repos_cfg = _get_configured_repos(root=root) all_repos = {} for alias in repos_cfg.sections(): - all_repos[alias] = _get_repo_info(alias, repos_cfg=repos_cfg) + all_repos[alias] = _get_repo_info(alias, repos_cfg=repos_cfg, root=root) return all_repos -def del_repo(repo): +def del_repo(repo, root=None): ''' Delete a repo. + root + operate on a different root directory. + CLI Examples: .. code-block:: bash salt '*' pkg.del_repo alias ''' - repos_cfg = _get_configured_repos() + repos_cfg = _get_configured_repos(root=root) for alias in repos_cfg.sections(): if alias == repo: - doc = __zypper__.xml.call('rr', '--loose-auth', '--loose-query', alias) + doc = __zypper__(root=root).xml.call('rr', '--loose-auth', '--loose-query', alias) msg = doc.getElementsByTagName('message') if doc.getElementsByTagName('progress') and msg: return { @@ -1044,6 +1100,9 @@ def mod_repo(repo, **kwargs): If set to True, automatically trust and import public GPG key for the repository. + root + operate on a different root directory. + Key/Value pairs may also be removed from a repo's configuration by setting a key to a blank value. Bear in mind that a name cannot be deleted, and a URL can only be deleted if a ``mirrorlist`` is specified (or vice versa). @@ -1056,7 +1115,8 @@ def mod_repo(repo, **kwargs): salt '*' pkg.mod_repo alias url= mirrorlist=http://host.com/ ''' - repos_cfg = _get_configured_repos() + root = kwargs.get('root') or None + repos_cfg = _get_configured_repos(root=root) added = False # An attempt to add new one? @@ -1076,7 +1136,7 @@ def mod_repo(repo, **kwargs): # Is there already such repo under different alias? for alias in repos_cfg.sections(): - repo_meta = _get_repo_info(alias, repos_cfg=repos_cfg) + repo_meta = _get_repo_info(alias, repos_cfg=repos_cfg, root=root) # Complete user URL, in case it is not new_url = _urlparse(url) @@ -1098,17 +1158,17 @@ def mod_repo(repo, **kwargs): ) # Add new repo - __zypper__.xml.call('ar', url, repo) + __zypper__(root=root).xml.call('ar', url, repo) # Verify the repository has been added - repos_cfg = _get_configured_repos() + repos_cfg = _get_configured_repos(root=root) if repo not in repos_cfg.sections(): raise CommandExecutionError( 'Failed add new repository \'{0}\' for unspecified reason. ' 'Please check zypper logs.'.format(repo)) added = True - repo_info = _get_repo_info(repo) + repo_info = _get_repo_info(repo, root=root) if ( not added and 'baseurl' in kwargs and not (kwargs['baseurl'] == repo_info['baseurl']) @@ -1117,8 +1177,8 @@ def mod_repo(repo, **kwargs): # we need to remove the repository and add it again with the new baseurl repo_info.update(kwargs) repo_info.setdefault('cache', False) - del_repo(repo) - return mod_repo(repo, **repo_info) + del_repo(repo, root=root) + return mod_repo(repo, root=root, **repo_info) # Modify added or existing repo according to the options cmd_opt = [] @@ -1151,7 +1211,7 @@ def mod_repo(repo, **kwargs): if cmd_opt: cmd_opt = global_cmd_opt + ['mr'] + cmd_opt + [repo] - __zypper__.refreshable.xml.call(*cmd_opt) + __zypper__(root=root).refreshable.xml.call(*cmd_opt) comment = None if call_refresh: @@ -1159,23 +1219,26 @@ def mod_repo(repo, **kwargs): # --gpg-auto-import-keys is not doing anything # so we need to specifically refresh here with --gpg-auto-import-keys refresh_opts = global_cmd_opt + ['refresh'] + [repo] - __zypper__.xml.call(*refresh_opts) + __zypper__(root=root).xml.call(*refresh_opts) elif not added and not cmd_opt: comment = 'Specified arguments did not result in modification of repo' - repo = get_repo(repo) + repo = get_repo(repo, root=root) if comment: repo['comment'] = comment return repo -def refresh_db(): +def refresh_db(root=None): ''' Force a repository refresh by calling ``zypper refresh --force``, return a dict:: {'': Bool} + root + operate on a different root directory. + CLI Example: .. code-block:: bash @@ -1185,7 +1248,7 @@ def refresh_db(): # Remove rtag file to keep multiple refreshes from happening in pkg states salt.utils.pkg.clear_rtag(__opts__) ret = {} - out = __zypper__.refreshable.call('refresh', '--force') + out = __zypper__(root=root).refreshable.call('refresh', '--force') for line in out.splitlines(): if not line: @@ -1213,6 +1276,8 @@ def install(name=None, skip_verify=False, version=None, ignore_repo_failure=False, + no_recommends=False, + root=None, **kwargs): ''' .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 @@ -1301,6 +1366,12 @@ def install(name=None, Zypper returns error code 106 if one of the repositories are not available for various reasons. In case to set strict check, this parameter needs to be set to True. Default: False. + no_recommends + Do not install recommended packages, only required ones. + + root + operate on a different root directory. + diff_attr: If a list of package attributes is specified, returned value will contain them, eg.:: @@ -1340,7 +1411,7 @@ def install(name=None, 'arch': ''}}} ''' if refresh: - refresh_db() + refresh_db(root) try: pkg_params, pkg_type = __salt__['pkg_resource.parse_targets'](name, pkgs, sources, **kwargs) @@ -1350,7 +1421,7 @@ def install(name=None, if pkg_params is None or len(pkg_params) == 0: return {} - version_num = Wildcard(__zypper__)(name, version) + version_num = Wildcard(__zypper__(root=root))(name, version) if version_num: if pkgs is None and sources is None: @@ -1375,7 +1446,7 @@ def install(name=None, targets.append(target) elif pkg_type == 'advisory': targets = [] - cur_patches = list_patches() + cur_patches = list_patches(root=root) for advisory_id in pkg_params: if advisory_id not in cur_patches: raise CommandExecutionError('Advisory id "{0}" not found'.format(advisory_id)) @@ -1385,7 +1456,7 @@ def install(name=None, targets = pkg_params diff_attr = kwargs.get("diff_attr") - old = list_pkgs(attr=diff_attr) if not downloadonly else list_downloaded() + old = list_pkgs(attr=diff_attr, root=root) if not downloadonly else list_downloaded(root) downgrades = [] if fromrepo: fromrepoopt = ['--force', '--force-resolution', '--from', fromrepo] @@ -1404,6 +1475,8 @@ def install(name=None, cmd_install.append('--download-only') if fromrepo: cmd_install.extend(fromrepoopt) + if no_recommends: + cmd_install.append('--no-recommends') errors = [] if pkg_type == 'advisory': @@ -1415,7 +1488,7 @@ def install(name=None, while targets: cmd = cmd_install + targets[:500] targets = targets[500:] - for line in __zypper__(no_repo_failure=ignore_repo_failure, systemd_scope=systemd_scope).call(*cmd).splitlines(): + for line in __zypper__(no_repo_failure=ignore_repo_failure, systemd_scope=systemd_scope, root=root).call(*cmd).splitlines(): match = re.match(r"^The selected package '([^']+)'.+has lower version", line) if match: downgrades.append(match.group(1)) @@ -1423,10 +1496,10 @@ def install(name=None, while downgrades: cmd = cmd_install + ['--force'] + downgrades[:500] downgrades = downgrades[500:] - __zypper__(no_repo_failure=ignore_repo_failure).call(*cmd) + __zypper__(no_repo_failure=ignore_repo_failure, root=root).call(*cmd) _clean_cache() - new = list_pkgs(attr=diff_attr) if not downloadonly else list_downloaded() + new = list_pkgs(attr=diff_attr, root=root) if not downloadonly else list_downloaded(root) ret = salt.utils.data.compare_dicts(old, new) if errors: @@ -1446,6 +1519,8 @@ def upgrade(refresh=True, fromrepo=None, novendorchange=False, skip_verify=False, + no_recommends=False, + root=None, **kwargs): # pylint: disable=unused-argument ''' .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 @@ -1485,6 +1560,12 @@ def upgrade(refresh=True, skip_verify Skip the GPG verification check (e.g., ``--no-gpg-checks``) + no_recommends + Do not install recommended packages, only required ones. + + root + Operate on a different root directory. + Returns a dictionary containing the changes: .. code-block:: python @@ -1507,7 +1588,7 @@ def upgrade(refresh=True, cmd_update.insert(0, '--no-gpg-checks') if refresh: - refresh_db() + refresh_db(root) if dryrun: cmd_update.append('--dry-run') @@ -1526,16 +1607,20 @@ def upgrade(refresh=True, else: log.warning('Disabling vendor changes is not supported on this Zypper version') + if no_recommends: + cmd_update.append('--no-recommends') + log.info('Disabling recommendations') + if dryrun: # Creates a solver test case for debugging. log.info('Executing debugsolver and performing a dry-run dist-upgrade') - __zypper__(systemd_scope=_systemd_scope()).noraise.call(*cmd_update + ['--debug-solver']) + __zypper__(systemd_scope=_systemd_scope(), root=root).noraise.call(*cmd_update + ['--debug-solver']) - old = list_pkgs() + old = list_pkgs(root=root) - __zypper__(systemd_scope=_systemd_scope()).noraise.call(*cmd_update) + __zypper__(systemd_scope=_systemd_scope(), root=root).noraise.call(*cmd_update) _clean_cache() - new = list_pkgs() + new = list_pkgs(root=root) ret = salt.utils.data.compare_dicts(old, new) if __zypper__.exit_code not in __zypper__.SUCCESS_EXIT_CODES: @@ -1556,7 +1641,7 @@ def upgrade(refresh=True, return ret -def _uninstall(name=None, pkgs=None): +def _uninstall(name=None, pkgs=None, root=None): ''' Remove and purge do identical things but with different Zypper commands, this function performs the common logic. @@ -1566,7 +1651,7 @@ def _uninstall(name=None, pkgs=None): except MinionError as exc: raise CommandExecutionError(exc) - old = list_pkgs() + old = list_pkgs(root=root) targets = [] for target in pkg_params: # Check if package version set to be removed is actually installed: @@ -1582,11 +1667,11 @@ def _uninstall(name=None, pkgs=None): errors = [] while targets: - __zypper__(systemd_scope=systemd_scope).call('remove', *targets[:500]) + __zypper__(systemd_scope=systemd_scope, root=root).call('remove', *targets[:500]) targets = targets[500:] _clean_cache() - ret = salt.utils.data.compare_dicts(old, list_pkgs()) + ret = salt.utils.data.compare_dicts(old, list_pkgs(root=root)) if errors: raise CommandExecutionError( @@ -1623,7 +1708,7 @@ def normalize_name(name): return name -def remove(name=None, pkgs=None, **kwargs): # pylint: disable=unused-argument +def remove(name=None, pkgs=None, root=None, **kwargs): # pylint: disable=unused-argument ''' .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 On minions running systemd>=205, `systemd-run(1)`_ is now used to @@ -1651,6 +1736,9 @@ def remove(name=None, pkgs=None, **kwargs): # pylint: disable=unused-argument A list of packages to delete. Must be passed as a python list. The ``name`` parameter will be ignored if this option is passed. + root + Operate on a different root directory. + .. versionadded:: 0.16.0 @@ -1664,10 +1752,10 @@ def remove(name=None, pkgs=None, **kwargs): # pylint: disable=unused-argument salt '*' pkg.remove ,, salt '*' pkg.remove pkgs='["foo", "bar"]' ''' - return _uninstall(name=name, pkgs=pkgs) + return _uninstall(name=name, pkgs=pkgs, root=root) -def purge(name=None, pkgs=None, **kwargs): # pylint: disable=unused-argument +def purge(name=None, pkgs=None, root=None, **kwargs): # pylint: disable=unused-argument ''' .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0 On minions running systemd>=205, `systemd-run(1)`_ is now used to @@ -1696,6 +1784,9 @@ def purge(name=None, pkgs=None, **kwargs): # pylint: disable=unused-argument A list of packages to delete. Must be passed as a python list. The ``name`` parameter will be ignored if this option is passed. + root + Operate on a different root directory. + .. versionadded:: 0.16.0 @@ -1709,13 +1800,16 @@ def purge(name=None, pkgs=None, **kwargs): # pylint: disable=unused-argument salt '*' pkg.purge ,, salt '*' pkg.purge pkgs='["foo", "bar"]' ''' - return _uninstall(name=name, pkgs=pkgs) + return _uninstall(name=name, pkgs=pkgs, root=root) -def list_locks(): +def list_locks(root=None): ''' List current package locks. + root + operate on a different root directory. + Return a dict containing the locked package with attributes:: {'': {'case_sensitive': '', @@ -1729,8 +1823,9 @@ def list_locks(): salt '*' pkg.list_locks ''' locks = {} - if os.path.exists(LOCKS): - with salt.utils.files.fopen(LOCKS) as fhr: + _locks = os.path.join(root, os.path.relpath(LOCKS, os.path.sep)) if root else LOCKS + try: + with salt.utils.files.fopen(_locks) as fhr: items = salt.utils.stringutils.to_unicode(fhr.read()).split('\n\n') for meta in [item.split('\n') for item in items]: lock = {} @@ -1739,15 +1834,22 @@ def list_locks(): lock.update(dict([tuple([i.strip() for i in element.split(':', 1)]), ])) if lock.get('solvable_name'): locks[lock.pop('solvable_name')] = lock + except IOError: + pass + except Exception: + log.warning('Detected a problem when accessing {}'.format(_locks)) return locks -def clean_locks(): +def clean_locks(root=None): ''' Remove unused locks that do not currently (with regard to repositories used) lock any package. + root + Operate on a different root directory. + CLI Example: .. code-block:: bash @@ -1756,10 +1858,11 @@ def clean_locks(): ''' LCK = "removed" out = {LCK: 0} - if not os.path.exists("/etc/zypp/locks"): + locks = os.path.join(root, os.path.relpath(LOCKS, os.path.sep)) if root else LOCKS + if not os.path.exists(locks): return out - for node in __zypper__.xml.call('cl').getElementsByTagName("message"): + for node in __zypper__(root=root).xml.call('cl').getElementsByTagName("message"): text = node.childNodes[0].nodeValue.lower() if text.startswith(LCK): out[LCK] = text.split(" ")[1] @@ -1808,10 +1911,13 @@ def unhold(name=None, pkgs=None, **kwargs): return ret -def remove_lock(packages, **kwargs): # pylint: disable=unused-argument +def remove_lock(packages, root=None, **kwargs): # pylint: disable=unused-argument ''' Remove specified package lock. + root + operate on a different root directory. + CLI Example: .. code-block:: bash @@ -1821,7 +1927,7 @@ def remove_lock(packages, **kwargs): # pylint: disable=unused-argument salt '*' pkg.remove_lock pkgs='["foo", "bar"]' ''' salt.utils.versions.warn_until('Sodium', 'This function is deprecated. Please use unhold() instead.') - locks = list_locks() + locks = list_locks(root) try: packages = list(__salt__['pkg_resource.parse_targets'](packages)[0].keys()) except MinionError as exc: @@ -1836,7 +1942,7 @@ def remove_lock(packages, **kwargs): # pylint: disable=unused-argument missing.append(pkg) if removed: - __zypper__.call('rl', *removed) + __zypper__(root=root).call('rl', *removed) return {'removed': len(removed), 'not_found': missing} @@ -1885,10 +1991,13 @@ def hold(name=None, pkgs=None, **kwargs): return ret -def add_lock(packages, **kwargs): # pylint: disable=unused-argument +def add_lock(packages, root=None, **kwargs): # pylint: disable=unused-argument ''' Add a package lock. Specify packages to lock by exact name. + root + operate on a different root directory. + CLI Example: .. code-block:: bash @@ -1898,7 +2007,7 @@ def add_lock(packages, **kwargs): # pylint: disable=unused-argument salt '*' pkg.add_lock pkgs='["foo", "bar"]' ''' salt.utils.versions.warn_until('Sodium', 'This function is deprecated. Please use hold() instead.') - locks = list_locks() + locks = list_locks(root) added = [] try: packages = list(__salt__['pkg_resource.parse_targets'](packages)[0].keys()) @@ -1910,7 +2019,7 @@ def add_lock(packages, **kwargs): # pylint: disable=unused-argument added.append(pkg) if added: - __zypper__.call('al', *added) + __zypper__(root=root).call('al', *added) return {'added': len(added), 'packages': added} @@ -1920,7 +2029,9 @@ def verify(*names, **kwargs): Runs an rpm -Va on a system, and returns the results in a dict Files with an attribute of config, doc, ghost, license or readme in the - package header can be ignored using the ``ignore_types`` keyword argument + package header can be ignored using the ``ignore_types`` keyword argument. + + The root parameter can also be passed via the keyword argument. CLI Example: @@ -1934,12 +2045,14 @@ def verify(*names, **kwargs): return __salt__['lowpkg.verify'](*names, **kwargs) -def file_list(*packages): +def file_list(*packages, **kwargs): ''' List the files that belong to a package. Not specifying any packages will return a list of *every* file on the system's rpm database (not generally recommended). + The root parameter can also be passed via the keyword argument. + CLI Examples: .. code-block:: bash @@ -1948,15 +2061,17 @@ def file_list(*packages): salt '*' pkg.file_list httpd postfix salt '*' pkg.file_list ''' - return __salt__['lowpkg.file_list'](*packages) + return __salt__['lowpkg.file_list'](*packages, **kwargs) -def file_dict(*packages): +def file_dict(*packages, **kwargs): ''' List the files that belong to a package, grouped by package. Not specifying any packages will return a list of *every* file on the system's rpm database (not generally recommended). + The root parameter can also be passed via the keyword argument. + CLI Examples: .. code-block:: bash @@ -1965,7 +2080,7 @@ def file_dict(*packages): salt '*' pkg.file_list httpd postfix salt '*' pkg.file_list ''' - return __salt__['lowpkg.file_dict'](*packages) + return __salt__['lowpkg.file_dict'](*packages, **kwargs) def modified(*packages, **flags): @@ -2004,6 +2119,9 @@ def modified(*packages, **flags): capabilities Include only files where capabilities differ or not. Note: supported only on newer RPM versions. + root + operate on a different root directory. + CLI Examples: .. code-block:: bash @@ -2017,7 +2135,7 @@ def modified(*packages, **flags): return __salt__['lowpkg.modified'](*packages, **flags) -def owner(*paths): +def owner(*paths, **kwargs): ''' Return the name of the package that owns the file. Multiple file paths can be passed. If a single path is passed, a string will be returned, @@ -2027,6 +2145,8 @@ def owner(*paths): If the file is not owned by a package, or is not present on the minion, then an empty string will be returned for that path. + The root parameter can also be passed via the keyword argument. + CLI Examples: .. code-block:: bash @@ -2034,15 +2154,15 @@ def owner(*paths): salt '*' pkg.owner /usr/bin/apachectl salt '*' pkg.owner /usr/bin/apachectl /etc/httpd/conf/httpd.conf ''' - return __salt__['lowpkg.owner'](*paths) + return __salt__['lowpkg.owner'](*paths, **kwargs) -def _get_patterns(installed_only=None): +def _get_patterns(installed_only=None, root=None): ''' List all known patterns in repos. ''' patterns = {} - for element in __zypper__.nolock.xml.call('se', '-t', 'pattern').getElementsByTagName('solvable'): + for element in __zypper__(root=root).nolock.xml.call('se', '-t', 'pattern').getElementsByTagName('solvable'): installed = element.getAttribute('status') == 'installed' if (installed_only and installed) or not installed_only: patterns[element.getAttribute('name')] = { @@ -2053,7 +2173,7 @@ def _get_patterns(installed_only=None): return patterns -def list_patterns(refresh=False): +def list_patterns(refresh=False, root=None): ''' List all known patterns from available repos. @@ -2062,6 +2182,9 @@ def list_patterns(refresh=False): If set to False (default) it depends on zypper if a refresh is executed. + root + operate on a different root directory. + CLI Examples: .. code-block:: bash @@ -2069,27 +2192,30 @@ def list_patterns(refresh=False): salt '*' pkg.list_patterns ''' if refresh: - refresh_db() + refresh_db(root) - return _get_patterns() + return _get_patterns(root=root) -def list_installed_patterns(): +def list_installed_patterns(root=None): ''' List installed patterns on the system. + root + operate on a different root directory. + CLI Examples: .. code-block:: bash salt '*' pkg.list_installed_patterns ''' - return _get_patterns(installed_only=True) + return _get_patterns(installed_only=True, root=root) def search(criteria, refresh=False, **kwargs): ''' - List known packags, available to the system. + List known packages, available to the system. refresh force a refresh if set to True. @@ -2137,6 +2263,9 @@ def search(criteria, refresh=False, **kwargs): details (bool) Show version and repository + root + operate on a different root directory. + CLI Examples: .. code-block:: bash @@ -2157,8 +2286,11 @@ def search(criteria, refresh=False, **kwargs): 'not_installed_only': '-u', 'details': '--details' } + + root = kwargs.get('root', None) + if refresh: - refresh_db() + refresh_db(root) cmd = ['search'] if kwargs.get('match') == 'exact': @@ -2173,7 +2305,7 @@ def search(criteria, refresh=False, **kwargs): cmd.append(ALLOWED_SEARCH_OPTIONS.get(opt)) cmd.append(criteria) - solvables = __zypper__.nolock.noraise.xml.call(*cmd).getElementsByTagName('solvable') + solvables = __zypper__(root=root).nolock.noraise.xml.call(*cmd).getElementsByTagName('solvable') if not solvables: raise CommandExecutionError( 'No packages found matching \'{0}\''.format(criteria) @@ -2202,7 +2334,7 @@ def _get_first_aggregate_text(node_list): return '\n'.join(out) -def list_products(all=False, refresh=False): +def list_products(all=False, refresh=False, root=None): ''' List all available or installed SUSE products. @@ -2214,6 +2346,9 @@ def list_products(all=False, refresh=False): If set to False (default) it depends on zypper if a refresh is executed. + root + operate on a different root directory. + Includes handling for OEM products, which read the OEM productline file and overwrite the release value. @@ -2225,10 +2360,12 @@ def list_products(all=False, refresh=False): salt '*' pkg.list_products all=True ''' if refresh: - refresh_db() + refresh_db(root) ret = list() - OEM_PATH = "/var/lib/suseRegister/OEM" + OEM_PATH = '/var/lib/suseRegister/OEM' + if root: + OEM_PATH = os.path.join(root, os.path.relpath(OEM_PATH, os.path.sep)) cmd = list() if not all: cmd.append('--disable-repos') @@ -2236,7 +2373,7 @@ def list_products(all=False, refresh=False): if not all: cmd.append('-i') - product_list = __zypper__.nolock.xml.call(*cmd).getElementsByTagName('product-list') + product_list = __zypper__(root=root).nolock.xml.call(*cmd).getElementsByTagName('product-list') if not product_list: return ret # No products found @@ -2278,6 +2415,9 @@ def download(*packages, **kwargs): If set to False (default) it depends on zypper if a refresh is executed. + root + operate on a different root directory. + CLI example: .. code-block:: bash @@ -2288,12 +2428,14 @@ def download(*packages, **kwargs): if not packages: raise SaltInvocationError('No packages specified') + root = kwargs.get('root', None) + refresh = kwargs.get('refresh', False) if refresh: - refresh_db() + refresh_db(root) pkg_ret = {} - for dld_result in __zypper__.xml.call('download', *packages).getElementsByTagName("download-result"): + for dld_result in __zypper__(root=root).xml.call('download', *packages).getElementsByTagName("download-result"): repo = dld_result.getElementsByTagName("repository")[0] path = dld_result.getElementsByTagName("localfile")[0].getAttribute("path") pkg_info = { @@ -2304,7 +2446,7 @@ def download(*packages, **kwargs): key = _get_first_aggregate_text( dld_result.getElementsByTagName('name') ) - if __salt__['lowpkg.checksum'](pkg_info['path']): + if __salt__['lowpkg.checksum'](pkg_info['path'], root=root): pkg_ret[key] = pkg_info if pkg_ret: @@ -2318,12 +2460,15 @@ def download(*packages, **kwargs): ) -def list_downloaded(): +def list_downloaded(root=None): ''' .. versionadded:: 2017.7.0 List prefetched packages downloaded by Zypper in the local disk. + root + operate on a different root directory. + CLI example: .. code-block:: bash @@ -2331,6 +2476,8 @@ def list_downloaded(): salt '*' pkg.list_downloaded ''' CACHE_DIR = '/var/cache/zypp/packages/' + if root: + CACHE_DIR = os.path.join(root, os.path.relpath(CACHE_DIR, os.path.sep)) ret = {} for root, dirnames, filenames in salt.utils.path.os_walk(CACHE_DIR): @@ -2347,12 +2494,14 @@ def list_downloaded(): return ret -def diff(*paths): +def diff(*paths, **kwargs): ''' Return a formatted diff between current files and original in a package. NOTE: this function includes all files (configuration and not), but does not work on binary content. + The root parameter can also be passed via the keyword argument. + :param path: Full path to the installed file :return: Difference string or raises and exception if examined file is binary. @@ -2366,7 +2515,7 @@ def diff(*paths): pkg_to_paths = {} for pth in paths: - pth_pkg = __salt__['lowpkg.owner'](pth) + pth_pkg = __salt__['lowpkg.owner'](pth, **kwargs) if not pth_pkg: ret[pth] = os.path.exists(pth) and 'Not managed' or 'N/A' else: @@ -2375,7 +2524,7 @@ def diff(*paths): pkg_to_paths[pth_pkg].append(pth) if pkg_to_paths: - local_pkgs = __salt__['pkg.download'](*pkg_to_paths.keys()) + local_pkgs = __salt__['pkg.download'](*pkg_to_paths.keys(), **kwargs) for pkg, files in six.iteritems(pkg_to_paths): for path in files: ret[path] = __salt__['lowpkg.diff']( @@ -2386,12 +2535,12 @@ def diff(*paths): return ret -def _get_patches(installed_only=False): +def _get_patches(installed_only=False, root=None): ''' List all known patches in repos. ''' patches = {} - for element in __zypper__.nolock.xml.call('se', '-t', 'patch').getElementsByTagName('solvable'): + for element in __zypper__(root=root).nolock.xml.call('se', '-t', 'patch').getElementsByTagName('solvable'): installed = element.getAttribute('status') == 'installed' if (installed_only and installed) or not installed_only: patches[element.getAttribute('name')] = { @@ -2402,7 +2551,7 @@ def _get_patches(installed_only=False): return patches -def list_patches(refresh=False): +def list_patches(refresh=False, root=None, **kwargs): ''' .. versionadded:: 2017.7.0 @@ -2413,6 +2562,9 @@ def list_patches(refresh=False): If set to False (default) it depends on zypper if a refresh is executed. + root + operate on a different root directory. + CLI Examples: .. code-block:: bash @@ -2420,33 +2572,39 @@ def list_patches(refresh=False): salt '*' pkg.list_patches ''' if refresh: - refresh_db() + refresh_db(root) - return _get_patches() + return _get_patches(root=root) -def list_installed_patches(): +def list_installed_patches(root=None, **kwargs): ''' .. versionadded:: 2017.7.0 List installed advisory patches on the system. + root + operate on a different root directory. + CLI Examples: .. code-block:: bash salt '*' pkg.list_installed_patches ''' - return _get_patches(installed_only=True) + return _get_patches(installed_only=True, root=root) -def list_provides(**kwargs): +def list_provides(root=None, **kwargs): ''' .. versionadded:: 2018.3.0 List package provides of installed packages as a dict. {'': ['', '', ...]} + root + operate on a different root directory. + CLI Examples: .. code-block:: bash @@ -2455,7 +2613,10 @@ def list_provides(**kwargs): ''' ret = __context__.get('pkg.list_provides') if not ret: - cmd = ['rpm', '-qa', '--queryformat', '%{PROVIDES}_|-%{NAME}\n'] + cmd = ['rpm'] + if root: + cmd.extend(['--root', root]) + cmd.extend(['-qa', '--queryformat', '%{PROVIDES}_|-%{NAME}\n']) ret = dict() for line in __salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=False).splitlines(): provide, realname = line.split('_|-') @@ -2471,7 +2632,7 @@ def list_provides(**kwargs): return ret -def resolve_capabilities(pkgs, refresh, **kwargs): +def resolve_capabilities(pkgs, refresh=False, root=None, **kwargs): ''' .. versionadded:: 2018.3.0 @@ -2485,6 +2646,9 @@ def resolve_capabilities(pkgs, refresh, **kwargs): If set to False (default) it depends on zypper if a refresh is executed. + root + operate on a different root directory. + resolve_capabilities If this option is set to True the input will be checked if a package with this name exists. If not, this function will @@ -2500,7 +2664,7 @@ def resolve_capabilities(pkgs, refresh, **kwargs): salt '*' pkg.resolve_capabilities resolve_capabilities=True w3m_ssl ''' if refresh: - refresh_db() + refresh_db(root) ret = list() for pkg in pkgs: @@ -2513,12 +2677,12 @@ def resolve_capabilities(pkgs, refresh, **kwargs): if kwargs.get('resolve_capabilities', False): try: - search(name, match='exact') + search(name, root=root, match='exact') except CommandExecutionError: # no package this such a name found # search for a package which provides this name try: - result = search(name, provides=True, match='exact') + result = search(name, root=root, provides=True, match='exact') if len(result) == 1: name = next(iter(result.keys())) elif len(result) > 1: diff --git a/salt/states/blockdev.py b/salt/states/blockdev.py index 38543ac8a07..2db5d805c3a 100644 --- a/salt/states/blockdev.py +++ b/salt/states/blockdev.py @@ -193,5 +193,6 @@ def _checkblk(name): Check if the blk exists and return its fstype if ok ''' - blk = __salt__['cmd.run']('blkid -o value -s TYPE {0}'.format(name)) + blk = __salt__['cmd.run']('blkid -o value -s TYPE {0}'.format(name), + ignore_retcode=True) return '' if not blk else blk diff --git a/salt/states/btrfs.py b/salt/states/btrfs.py new file mode 100644 index 00000000000..b2b939d5354 --- /dev/null +++ b/salt/states/btrfs.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- +# +# Author: Alberto Planas +# +# Copyright 2018 SUSE LINUX GmbH, Nuernberg, Germany. +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +''' +:maintainer: Alberto Planas +:maturity: new +:depends: None +:platform: Linux +''' +from __future__ import absolute_import, print_function, unicode_literals +import functools +import logging +import os.path +import tempfile +import traceback + +from salt.exceptions import CommandExecutionError + +log = logging.getLogger(__name__) + +__virtualname__ = 'btrfs' + + +def _mount(device): + ''' + Mount the device in a temporary place. + ''' + dest = tempfile.mkdtemp() + res = __states__['mount.mounted'](dest, device=device, fstype='btrfs', + opts='subvol=/', persist=False) + if not res['result']: + log.error('Cannot mount device %s in %s', device, dest) + _umount(dest) + return None + return dest + + +def _umount(path): + ''' + Umount and clean the temporary place. + ''' + __states__['mount.unmounted'](path) + __utils__['files.rm_rf'](path) + + +def _is_default(path, dest, name): + ''' + Check if the subvolume is the current default. + ''' + subvol_id = __salt__['btrfs.subvolume_show'](path)[name]['subvolume id'] + def_id = __salt__['btrfs.subvolume_get_default'](dest)['id'] + return subvol_id == def_id + + +def _set_default(path, dest, name): + ''' + Set the subvolume as the current default. + ''' + subvol_id = __salt__['btrfs.subvolume_show'](path)[name]['subvolume id'] + return __salt__['btrfs.subvolume_set_default'](subvol_id, dest) + + +def _is_cow(path): + ''' + Check if the subvolume is copy on write + ''' + dirname = os.path.dirname(path) + return 'C' not in __salt__['file.lsattr'](dirname)[path] + + +def _unset_cow(path): + ''' + Disable the copy on write in a subvolume + ''' + return __salt__['file.chattr'](path, operator='add', attributes='C') + + +def __mount_device(action): + ''' + Small decorator to makes sure that the mount and umount happends in + a transactional way. + ''' + @functools.wraps(action) + def wrapper(*args, **kwargs): + name = kwargs['name'] + device = kwargs['device'] + + ret = { + 'name': name, + 'result': False, + 'changes': {}, + 'comment': ['Some error happends during the operation.'], + } + try: + dest = _mount(device) + if not dest: + msg = 'Device {} cannot be mounted'.format(device) + ret['comment'].append(msg) + kwargs['__dest'] = dest + ret = action(*args, **kwargs) + except Exception as e: + log.error('''Traceback: {}'''.format(traceback.format_exc())) + ret['comment'].append(e) + finally: + _umount(dest) + return ret + return wrapper + + +@__mount_device +def subvolume_created(name, device, qgroupids=None, set_default=False, + copy_on_write=True, force_set_default=True, + __dest=None): + ''' + Makes sure that a btrfs subvolume is present. + + name + Name of the subvolume to add + + device + Device where to create the subvolume + + qgroupids + Add the newly created subcolume to a qgroup. This parameter + is a list + + set_default + If True, this new subvolume will be set as default when + mounted, unless subvol option in mount is used + + copy_on_write + If false, set the subvolume with chattr +C + + force_set_default + If false and the subvolume is already present, it will not + force it as default if ``set_default`` is True + + ''' + ret = { + 'name': name, + 'result': False, + 'changes': {}, + 'comment': [], + } + path = os.path.join(__dest, name) + + exists = __salt__['btrfs.subvolume_exists'](path) + if exists: + ret['comment'].append('Subvolume {} already present'.format(name)) + + # Resolve first the test case. The check is not complete, but at + # least we will report if a subvolume needs to be created. Can + # happend that the subvolume is there, but we also need to set it + # as default, or persist in fstab. + if __opts__['test']: + ret['result'] = None + if not exists: + ret['comment'].append('Subvolume {} will be created'.format(name)) + return ret + + if not exists: + # Create the directories where the subvolume lives + _path = os.path.dirname(path) + res = __states__['file.directory'](_path, makedirs=True) + if not res['result']: + ret['comment'].append('Error creating {} directory'.format(_path)) + return ret + + try: + __salt__['btrfs.subvolume_create'](name, dest=__dest, + qgroupids=qgroupids) + except CommandExecutionError: + ret['comment'].append('Error creating subvolume {}'.format(name)) + return ret + + ret['changes'][name] = 'Created subvolume {}'.format(name) + + # If the volume was already present, we can opt-out the check for + # default subvolume. + if (not exists or (exists and force_set_default)) and \ + set_default and not _is_default(path, __dest, name): + ret['changes'][name + '_default'] = _set_default(path, __dest, name) + + if not copy_on_write and _is_cow(path): + ret['changes'][name + '_no_cow'] = _unset_cow(path) + + ret['result'] = True + return ret + + +@__mount_device +def subvolume_deleted(name, device, commit=False, __dest=None): + ''' + Makes sure that a btrfs subvolume is removed. + + name + Name of the subvolume to remove + + device + Device where to remove the subvolume + + commit + Wait until the transaction is over + + ''' + ret = { + 'name': name, + 'result': False, + 'changes': {}, + 'comment': [], + } + + path = os.path.join(__dest, name) + + exists = __salt__['btrfs.subvolume_exists'](path) + if not exists: + ret['comment'].append('Subvolume {} already missing'.format(name)) + + if __opts__['test']: + ret['result'] = None + if exists: + ret['comment'].append('Subvolume {} will be removed'.format(name)) + return ret + + # If commit is set, we wait until all is over + commit = 'after' if commit else None + + if not exists: + try: + __salt__['btrfs.subvolume_delete'](path, commit=commit) + except CommandExecutionError: + ret['comment'].append('Error removing subvolume {}'.format(name)) + return ret + + ret['changes'][name] = 'Removed subvolume {}'.format(name) + + ret['result'] = True + return ret diff --git a/salt/states/cmd.py b/salt/states/cmd.py index 4d20b513817..86934f9ffc4 100644 --- a/salt/states/cmd.py +++ b/salt/states/cmd.py @@ -402,6 +402,7 @@ def wait(name, unless=None, creates=None, cwd=None, + root=None, runas=None, shell=None, env=(), @@ -436,6 +437,10 @@ def wait(name, The current working directory to execute the command in, defaults to /root + root + Path to the root of the jail to use. If this parameter is set, the command + will run inside a chroot + runas The user name to run the command as @@ -674,6 +679,7 @@ def run(name, unless=None, creates=None, cwd=None, + root=None, runas=None, shell=None, env=None, @@ -707,6 +713,10 @@ def run(name, The current working directory to execute the command in, defaults to /root + root + Path to the root of the jail to use. If this parameter is set, the command + will run inside a chroot + runas The user name to run the command as @@ -882,6 +892,7 @@ def run(name, cmd_kwargs = copy.deepcopy(kwargs) cmd_kwargs.update({'cwd': cwd, + 'root': root, 'runas': runas, 'use_vt': use_vt, 'shell': shell or __grains__['shell'], @@ -912,10 +923,11 @@ def run(name, # Wow, we passed the test, run this sucker! try: - cmd_all = __salt__['cmd.run_all']( - name, timeout=timeout, python_shell=True, **cmd_kwargs + run_cmd = 'cmd.run_all' if not root else 'cmd.run_chroot' + cmd_all = __salt__[run_cmd]( + cmd=name, timeout=timeout, python_shell=True, **cmd_kwargs ) - except CommandExecutionError as err: + except Exception as err: ret['comment'] = six.text_type(err) return ret diff --git a/salt/states/lvm.py b/salt/states/lvm.py index bc937a33ab2..5cb15d0ed6e 100644 --- a/salt/states/lvm.py +++ b/salt/states/lvm.py @@ -56,7 +56,7 @@ def pv_present(name, **kwargs): 'name': name, 'result': True} - if __salt__['lvm.pvdisplay'](name): + if __salt__['lvm.pvdisplay'](name, quiet=True): ret['comment'] = 'Physical Volume {0} already present'.format(name) elif __opts__['test']: ret['comment'] = 'Physical Volume {0} is set to be created'.format(name) @@ -86,7 +86,7 @@ def pv_absent(name): 'name': name, 'result': True} - if not __salt__['lvm.pvdisplay'](name): + if not __salt__['lvm.pvdisplay'](name, quiet=True): ret['comment'] = 'Physical Volume {0} does not exist'.format(name) elif __opts__['test']: ret['comment'] = 'Physical Volume {0} is set to be removed'.format(name) @@ -95,7 +95,7 @@ def pv_absent(name): else: changes = __salt__['lvm.pvremove'](name) - if __salt__['lvm.pvdisplay'](name): + if __salt__['lvm.pvdisplay'](name, quiet=True): ret['comment'] = 'Failed to remove Physical Volume {0}'.format(name) ret['result'] = False else: @@ -125,7 +125,7 @@ def vg_present(name, devices=None, **kwargs): if isinstance(devices, six.string_types): devices = devices.split(',') - if __salt__['lvm.vgdisplay'](name): + if __salt__['lvm.vgdisplay'](name, quiet=True): ret['comment'] = 'Volume Group {0} already present'.format(name) for device in devices: realdev = os.path.realpath(device) @@ -185,7 +185,7 @@ def vg_absent(name): 'name': name, 'result': True} - if not __salt__['lvm.vgdisplay'](name): + if not __salt__['lvm.vgdisplay'](name, quiet=True): ret['comment'] = 'Volume Group {0} already absent'.format(name) elif __opts__['test']: ret['comment'] = 'Volume Group {0} is set to be removed'.format(name) @@ -194,7 +194,7 @@ def vg_absent(name): else: changes = __salt__['lvm.vgremove'](name) - if not __salt__['lvm.vgdisplay'](name): + if not __salt__['lvm.vgdisplay'](name, quiet=True): ret['comment'] = 'Removed Volume Group {0}'.format(name) ret['changes']['removed'] = changes else: @@ -311,7 +311,7 @@ def lv_absent(name, vgname=None): 'result': True} lvpath = '/dev/{0}/{1}'.format(vgname, name) - if not __salt__['lvm.lvdisplay'](lvpath): + if not __salt__['lvm.lvdisplay'](lvpath, quiet=True): ret['comment'] = 'Logical Volume {0} already absent'.format(name) elif __opts__['test']: ret['comment'] = 'Logical Volume {0} is set to be removed'.format(name) @@ -320,7 +320,7 @@ def lv_absent(name, vgname=None): else: changes = __salt__['lvm.lvremove'](name, vgname) - if not __salt__['lvm.lvdisplay'](lvpath): + if not __salt__['lvm.lvdisplay'](lvpath, quiet=True): ret['comment'] = 'Removed Logical Volume {0}'.format(name) ret['changes']['removed'] = changes else: diff --git a/salt/states/mount.py b/salt/states/mount.py index 162da1ca621..2f5946b647a 100644 --- a/salt/states/mount.py +++ b/salt/states/mount.py @@ -956,3 +956,295 @@ def mod_watch(name, user=None, **kwargs): else: ret['comment'] = 'Watch not supported in {0} at this time'.format(kwargs['sfun']) return ret + + +def _convert_to(maybe_device, convert_to): + ''' + Convert a device name, UUID or LABEL to a device name, UUID or + LABEL. + + Return the fs_spec required for fstab. + + ''' + + # Fast path. If we already have the information required, we can + # save one blkid call + if not convert_to or \ + (convert_to == 'device' and maybe_device.startswith('/')) or \ + maybe_device.startswith('{}='.format(convert_to.upper())): + return maybe_device + + # Get the device information + if maybe_device.startswith('/'): + blkid = __salt__['disk.blkid'](maybe_device) + else: + blkid = __salt__['disk.blkid'](token=maybe_device) + + result = None + if len(blkid) == 1: + if convert_to == 'device': + result = list(blkid.keys())[0] + else: + key = convert_to.upper() + result = '{}={}'.format(key, list(blkid.values())[0][key]) + + return result + + +def fstab_present(name, fs_file, fs_vfstype, fs_mntops='defaults', + fs_freq=0, fs_passno=0, mount_by=None, + config='/etc/fstab', mount=True, match_on='auto'): + ''' + Makes sure that a fstab mount point is pressent. + + name + The name of block device. Can be any valid fs_spec value. + + fs_file + Mount point (target) for the filesystem. + + fs_vfstype + The type of the filesystem (e.g. ext4, xfs, btrfs, ...) + + fs_mntops + The mount options associated with the filesystem. Default is + ``defaults``. + + fs_freq + Field is used by dump to determine which fs need to be + dumped. Default is ``0`` + + fs_passno + Field is used by fsck to determine the order in which + filesystem checks are done at boot time. Default is ``0`` + + mount_by + Select the final value for fs_spec. Can be [``None``, + ``device``, ``label``, ``uuid``, ``partlabel``, + ``partuuid``]. If ``None``, the value for fs_spect will be the + parameter ``name``, in other case will search the correct + value based on the device name. For example, for ``uuid``, the + value for fs_spec will be of type 'UUID=xxx' instead of the + device name set in ``name``. + + config + Place where the fstab file lives. Default is ``/etc/fstab`` + + mount + Set if the mount should be mounted immediately. Default is + ``True`` + + match_on + A name or list of fstab properties on which this state should + be applied. Default is ``auto``, a special value indicating + to guess based on fstype. In general, ``auto`` matches on + name for recognized special devices and device otherwise. + + ''' + ret = { + 'name': name, + 'result': False, + 'changes': {}, + 'comment': [], + } + + # Adjust fs_mntops based on the OS + if fs_mntops == 'defaults': + if __grains__['os'] in ['MacOS', 'Darwin']: + fs_mntops = 'noowners' + elif __grains__['os'] == 'AIX': + fs_mntops = '' + + # Adjust the config file based on the OS + if config == '/etc/fstab': + if __grains__['os'] in ['MacOS', 'Darwin']: + config = '/etc/auto_salt' + elif __grains__['os'] == 'AIX': + config = '/etc/filesystems' + + if not fs_file == '/': + fs_file = fs_file.rstrip('/') + + fs_spec = _convert_to(name, mount_by) + + # Validate that the device is valid after the conversion + if not fs_spec: + msg = 'Device {} cannot be converted to {}' + ret['comment'].append(msg.format(name, mount_by)) + return ret + + if __opts__['test']: + if __grains__['os'] in ['MacOS', 'Darwin']: + out = __salt__['mount.set_automaster'](name=fs_file, + device=fs_spec, + fstype=fs_vfstype, + opts=fs_mntops, + config=config, + test=True) + elif __grains__['os'] == 'AIX': + out = __salt__['mount.set_filesystems'](name=fs_file, + device=fs_spec, + fstype=fs_vfstype, + opts=fs_mntops, + mount=mount, + config=config, + test=True, + match_on=match_on) + else: + out = __salt__['mount.set_fstab'](name=fs_file, + device=fs_spec, + fstype=fs_vfstype, + opts=fs_mntops, + dump=fs_freq, + pass_num=fs_passno, + config=config, + test=True, + match_on=match_on) + ret['result'] = None + if out == 'present': + msg = '{} entry is already in {}.' + ret['comment'].append(msg.format(fs_file, config)) + elif out == 'new': + msg = '{} entry will be written in {}.' + ret['comment'].append(msg.format(fs_file, config)) + elif out == 'change': + msg = '{} entry will be updated in {}.' + ret['comment'].append(msg.format(fs_file, config)) + else: + ret['result'] = False + msg = '{} entry cannot be created in {}: {}.' + ret['comment'].append(msg.format(fs_file, config, out)) + return ret + + if __grains__['os'] in ['MacOS', 'Darwin']: + out = __salt__['mount.set_automaster'](name=fs_file, + device=fs_spec, + fstype=fs_vfstype, + opts=fs_mntops, + config=config) + elif __grains__['os'] == 'AIX': + out = __salt__['mount.set_filesystems'](name=fs_file, + device=fs_spec, + fstype=fs_vfstype, + opts=fs_mntops, + mount=mount, + config=config, + match_on=match_on) + else: + out = __salt__['mount.set_fstab'](name=fs_file, + device=fs_spec, + fstype=fs_vfstype, + opts=fs_mntops, + dump=fs_freq, + pass_num=fs_passno, + config=config, + match_on=match_on) + + ret['result'] = True + if out == 'present': + msg = '{} entry was already in {}.' + ret['comment'].append(msg.format(fs_file, config)) + elif out == 'new': + ret['changes']['persist'] = out + msg = '{} entry added in {}.' + ret['comment'].append(msg.format(fs_file, config)) + elif out == 'change': + ret['changes']['persist'] = out + msg = '{} entry updated in {}.' + ret['comment'].append(msg.format(fs_file, config)) + else: + ret['result'] = False + msg = '{} entry cannot be changed in {}: {}.' + ret['comment'].append(msg.format(fs_file, config, out)) + + return ret + + +def fstab_absent(name, fs_file, mount_by=None, config='/etc/fstab'): + ''' + Makes sure that a fstab mount point is absent. + + name + The name of block device. Can be any valid fs_spec value. + + fs_file + Mount point (target) for the filesystem. + + mount_by + Select the final value for fs_spec. Can be [``None``, + ``device``, ``label``, ``uuid``, ``partlabel``, + ``partuuid``]. If ``None``, the value for fs_spect will be the + parameter ``name``, in other case will search the correct + value based on the device name. For example, for ``uuid``, the + value for fs_spec will be of type 'UUID=xxx' instead of the + device name set in ``name``. + + config + Place where the fstab file lives + + ''' + ret = { + 'name': name, + 'result': False, + 'changes': {}, + 'comment': [], + } + + # Adjust the config file based on the OS + if config == '/etc/fstab': + if __grains__['os'] in ['MacOS', 'Darwin']: + config = '/etc/auto_salt' + elif __grains__['os'] == 'AIX': + config = '/etc/filesystems' + + if not fs_file == '/': + fs_file = fs_file.rstrip('/') + + fs_spec = _convert_to(name, mount_by) + + if __grains__['os'] in ['MacOS', 'Darwin']: + fstab_data = __salt__['mount.automaster'](config) + elif __grains__['os'] == 'AIX': + fstab_data = __salt__['mount.filesystems'](config) + else: + fstab_data = __salt__['mount.fstab'](config) + + if __opts__['test']: + ret['result'] = None + if fs_file not in fstab_data: + msg = '{} entry is already missing in {}.' + ret['comment'].append(msg.format(fs_file, config)) + else: + msg = '{} entry will be removed from {}.' + ret['comment'].append(msg.format(fs_file, config)) + return ret + + if fs_file in fstab_data: + if __grains__['os'] in ['MacOS', 'Darwin']: + out = __salt__['mount.rm_automaster'](name=fs_file, + device=fs_spec, + config=config) + elif __grains__['os'] == 'AIX': + out = __salt__['mount.rm_filesystems'](name=fs_file, + device=fs_spec, + config=config) + else: + out = __salt__['mount.rm_fstab'](name=fs_file, + device=fs_spec, + config=config) + + if out is not True: + ret['result'] = False + msg = '{} entry failed when removing from {}.' + ret['comment'].append(msg.format(fs_file, config)) + else: + ret['result'] = True + ret['changes']['persist'] = 'removed' + msg = '{} entry removed from {}.' + ret['comment'].append(msg.format(fs_file, config)) + else: + ret['result'] = True + msg = '{} entry is already missing in {}.' + ret['comment'].append(msg.format(fs_file, config)) + + return ret diff --git a/salt/states/pkg.py b/salt/states/pkg.py index 0aca1e0af88..22a97fe98c8 100644 --- a/salt/states/pkg.py +++ b/salt/states/pkg.py @@ -241,7 +241,7 @@ def _fulfills_version_spec(versions, oper, desired_version, return False -def _find_unpurge_targets(desired): +def _find_unpurge_targets(desired, **kwargs): ''' Find packages which are marked to be purged but can't yet be removed because they are dependencies for other installed packages. These are the @@ -250,7 +250,7 @@ def _find_unpurge_targets(desired): ''' return [ x for x in desired - if x in __salt__['pkg.list_pkgs'](purge_desired=True) + if x in __salt__['pkg.list_pkgs'](purge_desired=True, **kwargs) ] @@ -265,7 +265,7 @@ def _find_download_targets(name=None, Inspect the arguments to pkg.downloaded and discover what packages need to be downloaded. Return a dict of packages to download. ''' - cur_pkgs = __salt__['pkg.list_downloaded']() + cur_pkgs = __salt__['pkg.list_downloaded'](**kwargs) if pkgs: to_download = _repack_pkgs(pkgs, normalize=normalize) @@ -383,7 +383,7 @@ def _find_advisory_targets(name=None, Inspect the arguments to pkg.patch_installed and discover what advisory patches need to be installed. Return a dict of advisory patches to install. ''' - cur_patches = __salt__['pkg.list_installed_patches']() + cur_patches = __salt__['pkg.list_installed_patches'](**kwargs) if advisory_ids: to_download = advisory_ids else: @@ -587,7 +587,7 @@ def _find_install_targets(name=None, 'minion log.'.format('pkgs' if pkgs else 'sources')} - to_unpurge = _find_unpurge_targets(desired) + to_unpurge = _find_unpurge_targets(desired, **kwargs) else: if salt.utils.platform.is_windows(): pkginfo = _get_package_info(name, saltenv=kwargs['saltenv']) @@ -607,7 +607,7 @@ def _find_install_targets(name=None, else: desired = {name: version} - to_unpurge = _find_unpurge_targets(desired) + to_unpurge = _find_unpurge_targets(desired, **kwargs) # FreeBSD pkg supports `openjdk` and `java/openjdk7` package names origin = bool(re.search('/', name)) @@ -766,7 +766,8 @@ def _find_install_targets(name=None, verify_result = __salt__['pkg.verify']( package_name, ignore_types=ignore_types, - verify_options=verify_options + verify_options=verify_options, + **kwargs ) except (CommandExecutionError, SaltInvocationError) as exc: failed_verify = exc.strerror @@ -795,7 +796,9 @@ def _find_install_targets(name=None, verify_result = __salt__['pkg.verify']( package_name, ignore_types=ignore_types, - verify_options=verify_options) + verify_options=verify_options, + **kwargs + ) except (CommandExecutionError, SaltInvocationError) as exc: failed_verify = exc.strerror continue @@ -1910,7 +1913,8 @@ def installed( # have caught invalid arguments earlier. verify_result = __salt__['pkg.verify'](reinstall_pkg, ignore_types=ignore_types, - verify_options=verify_options) + verify_options=verify_options, + **kwargs) if verify_result: failed.append(reinstall_pkg) altered_files[reinstall_pkg] = verify_result @@ -2098,7 +2102,7 @@ def downloaded(name, 'package(s): {0}'.format(exc) return ret - new_pkgs = __salt__['pkg.list_downloaded']() + new_pkgs = __salt__['pkg.list_downloaded'](**kwargs) ok, failed = _verify_install(targets, new_pkgs, ignore_epoch=ignore_epoch) if failed: @@ -2974,7 +2978,7 @@ def uptodate(name, refresh=False, pkgs=None, **kwargs): pkgs, refresh = _resolve_capabilities(pkgs, refresh=refresh, **kwargs) try: packages = __salt__['pkg.list_upgrades'](refresh=refresh, **kwargs) - expected = {pkgname: {'new': pkgver, 'old': __salt__['pkg.version'](pkgname)} + expected = {pkgname: {'new': pkgver, 'old': __salt__['pkg.version'](pkgname, **kwargs)} for pkgname, pkgver in six.iteritems(packages)} if isinstance(pkgs, list): packages = [pkg for pkg in packages if pkg in pkgs] @@ -3156,7 +3160,7 @@ def group_installed(name, skip=None, include=None, **kwargs): .format(name, exc)) return ret - failed = [x for x in targets if x not in __salt__['pkg.list_pkgs']()] + failed = [x for x in targets if x not in __salt__['pkg.list_pkgs'](**kwargs)] if failed: ret['comment'] = ( 'Failed to install the following packages: {0}' diff --git a/salt/states/pkgrepo.py b/salt/states/pkgrepo.py index 4d5e9eea92f..6d8e94aa185 100644 --- a/salt/states/pkgrepo.py +++ b/salt/states/pkgrepo.py @@ -393,10 +393,7 @@ def managed(name, ppa=None, **kwargs): kwargs.pop(kwarg, None) try: - pre = __salt__['pkg.get_repo']( - repo, - ppa_auth=kwargs.get('ppa_auth', None) - ) + pre = __salt__['pkg.get_repo'](repo=repo, **kwargs) except CommandExecutionError as exc: ret['result'] = False ret['comment'] = \ @@ -512,10 +509,7 @@ def managed(name, ppa=None, **kwargs): return ret try: - post = __salt__['pkg.get_repo']( - repo, - ppa_auth=kwargs.get('ppa_auth', None) - ) + post = __salt__['pkg.get_repo'](repo=repo, **kwargs) if pre: for kwarg in sanitizedkwargs: if post.get(kwarg) != pre.get(kwarg): @@ -608,9 +602,7 @@ def absent(name, **kwargs): return ret try: - repo = __salt__['pkg.get_repo']( - name, ppa_auth=kwargs.get('ppa_auth', None) - ) + repo = __salt__['pkg.get_repo'](name, **kwargs) except CommandExecutionError as exc: ret['result'] = False ret['comment'] = \ diff --git a/salt/utils/extmods.py b/salt/utils/extmods.py index 18a794fab18..ac8feafd180 100644 --- a/salt/utils/extmods.py +++ b/salt/utils/extmods.py @@ -8,6 +8,7 @@ import logging import os import shutil +import sys # Import salt libs import salt.fileclient @@ -133,6 +134,12 @@ def sync(opts, shutil.copyfile(fn_, dest) ret.append('{0}.{1}'.format(form, relname)) + # If the synchronized module is an utils + # directory, we add it to sys.path + for util_dir in opts['utils_dirs']: + if mod_dir.endswith(util_dir) and mod_dir not in sys.path: + sys.path.append(mod_dir) + touched = bool(ret) if opts['clean_dynamic_modules'] is True: current = set(_listdir_recursively(mod_dir)) diff --git a/tests/unit/modules/test_btrfs.py b/tests/unit/modules/test_btrfs.py index ebd28a64518..b5f934034d4 100644 --- a/tests/unit/modules/test_btrfs.py +++ b/tests/unit/modules/test_btrfs.py @@ -5,6 +5,8 @@ # Import python libs from __future__ import absolute_import, print_function, unicode_literals +import pytest + # Import Salt Testing Libs from tests.support.mixins import LoaderModuleMockMixin from tests.support.unit import TestCase, skipIf @@ -29,7 +31,7 @@ class BtrfsTestCase(TestCase, LoaderModuleMockMixin): Test cases for salt.modules.btrfs ''' def setup_loader_modules(self): - return {btrfs: {}} + return {btrfs: {'__salt__': {}}} # 'version' function tests: 1 def test_version(self): @@ -362,3 +364,369 @@ def test_properties_error(self): ''' self.assertRaises(CommandExecutionError, btrfs.properties, '/dev/sda1', 'subvol', True) + + def test_subvolume_exists(self): + ''' + Test subvolume_exists + ''' + salt_mock = { + 'cmd.retcode': MagicMock(return_value=0), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_exists('/mnt/one') + + def test_subvolume_not_exists(self): + ''' + Test subvolume_exists + ''' + salt_mock = { + 'cmd.retcode': MagicMock(return_value=1), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert not btrfs.subvolume_exists('/mnt/nowhere') + + def test_subvolume_create_fails_parameters(self): + ''' + Test btrfs subvolume create + ''' + # Fails when qgroupids is not a list + with pytest.raises(CommandExecutionError): + btrfs.subvolume_create('var', qgroupids='1') + + @patch('salt.modules.btrfs.subvolume_exists') + def test_subvolume_create_already_exists(self, subvolume_exists): + ''' + Test btrfs subvolume create + ''' + subvolume_exists.return_value = True + assert not btrfs.subvolume_create('var', dest='/mnt') + + @patch('salt.modules.btrfs.subvolume_exists') + def test_subvolume_create(self, subvolume_exists): + ''' + Test btrfs subvolume create + ''' + subvolume_exists.return_value = False + salt_mock = { + 'cmd.run_all': MagicMock(return_value={'recode': 0}), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_create('var', dest='/mnt') + subvolume_exists.assert_called_once() + salt_mock['cmd.run_all'].assert_called_once() + salt_mock['cmd.run_all'].assert_called_with( + ['btrfs', 'subvolume', 'create', '/mnt/var']) + + def test_subvolume_delete_fails_parameters(self): + ''' + Test btrfs subvolume delete + ''' + # We need to provide name or names + with pytest.raises(CommandExecutionError): + btrfs.subvolume_delete() + + with pytest.raises(CommandExecutionError): + btrfs.subvolume_delete(names='var') + + def test_subvolume_delete_fails_parameter_commit(self): + ''' + Test btrfs subvolume delete + ''' + # Parameter commit can be 'after' or 'each' + with pytest.raises(CommandExecutionError): + btrfs.subvolume_delete(name='var', commit='maybe') + + @patch('salt.modules.btrfs.subvolume_exists') + def test_subvolume_delete_already_missing(self, subvolume_exists): + ''' + Test btrfs subvolume delete + ''' + subvolume_exists.return_value = False + assert not btrfs.subvolume_delete(name='var', names=['tmp']) + + @patch('salt.modules.btrfs.subvolume_exists') + def test_subvolume_delete_already_missing_name(self, subvolume_exists): + ''' + Test btrfs subvolume delete + ''' + subvolume_exists.return_value = False + assert not btrfs.subvolume_delete(name='var') + + @patch('salt.modules.btrfs.subvolume_exists') + def test_subvolume_delete_already_missing_names(self, subvolume_exists): + ''' + Test btrfs subvolume delete + ''' + subvolume_exists.return_value = False + assert not btrfs.subvolume_delete(names=['tmp']) + + @patch('salt.modules.btrfs.subvolume_exists') + def test_subvolume_delete(self, subvolume_exists): + ''' + Test btrfs subvolume delete + ''' + subvolume_exists.return_value = True + salt_mock = { + 'cmd.run_all': MagicMock(return_value={'recode': 0}), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_delete('var', names=['tmp']) + salt_mock['cmd.run_all'].assert_called_once() + salt_mock['cmd.run_all'].assert_called_with( + ['btrfs', 'subvolume', 'delete', 'var', 'tmp']) + + def test_subvolume_find_new_empty(self): + ''' + Test btrfs subvolume find-new + ''' + salt_mock = { + 'cmd.run_all': MagicMock(return_value={ + 'recode': 0, + 'stdout': 'transid marker was 1024' + }), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_find_new('var', '2000') == { + 'files': [], + 'transid': '1024' + } + salt_mock['cmd.run_all'].assert_called_once() + salt_mock['cmd.run_all'].assert_called_with( + ['btrfs', 'subvolume', 'find-new', 'var', '2000']) + + def test_subvolume_find_new(self): + ''' + Test btrfs subvolume find-new + ''' + salt_mock = { + 'cmd.run_all': MagicMock(return_value={ + 'recode': 0, + 'stdout': '''inode 185148 ... gen 2108 flags NONE var/log/audit/audit.log +inode 187390 ... INLINE etc/openvpn/openvpn-status.log +transid marker was 1024''' + }), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_find_new('var', '1023') == { + 'files': ['var/log/audit/audit.log', + 'etc/openvpn/openvpn-status.log'], + 'transid': '1024' + } + salt_mock['cmd.run_all'].assert_called_once() + salt_mock['cmd.run_all'].assert_called_with( + ['btrfs', 'subvolume', 'find-new', 'var', '1023']) + + def test_subvolume_get_default_free(self): + ''' + Test btrfs subvolume get-default + ''' + salt_mock = { + 'cmd.run_all': MagicMock(return_value={ + 'recode': 0, + 'stdout': 'ID 5 (FS_TREE)', + }), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_get_default('/mnt') == { + 'id': '5', + 'name': '(FS_TREE)', + } + salt_mock['cmd.run_all'].assert_called_once() + salt_mock['cmd.run_all'].assert_called_with( + ['btrfs', 'subvolume', 'get-default', '/mnt']) + + def test_subvolume_get_default(self): + ''' + Test btrfs subvolume get-default + ''' + salt_mock = { + 'cmd.run_all': MagicMock(return_value={ + 'recode': 0, + 'stdout': 'ID 257 gen 8 top level 5 path var', + }), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_get_default('/mnt') == { + 'id': '257', + 'name': 'var', + } + salt_mock['cmd.run_all'].assert_called_once() + salt_mock['cmd.run_all'].assert_called_with( + ['btrfs', 'subvolume', 'get-default', '/mnt']) + + def test_subvolume_list_fails_parameters(self): + ''' + Test btrfs subvolume list + ''' + # Fails when sort is not a list + with pytest.raises(CommandExecutionError): + btrfs.subvolume_list('/mnt', sort='-rootid') + + # Fails when sort is not recognized + with pytest.raises(CommandExecutionError): + btrfs.subvolume_list('/mnt', sort=['-root']) + + def test_subvolume_list_simple(self): + ''' + Test btrfs subvolume list + ''' + salt_mock = { + 'cmd.run_all': MagicMock(return_value={ + 'recode': 0, + 'stdout': '''ID 257 gen 8 top level 5 path one +ID 258 gen 10 top level 5 path another one +''', + }), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_list('/mnt') == [ + { + 'id': '257', + 'gen': '8', + 'top level': '5', + 'path': 'one', + }, + { + 'id': '258', + 'gen': '10', + 'top level': '5', + 'path': 'another one', + }, + ] + salt_mock['cmd.run_all'].assert_called_once() + salt_mock['cmd.run_all'].assert_called_with( + ['btrfs', 'subvolume', 'list', '/mnt']) + + def test_subvolume_list(self): + ''' + Test btrfs subvolume list + ''' + salt_mock = { + 'cmd.run_all': MagicMock(return_value={ + 'recode': 0, + 'stdout': '''\ +ID 257 gen 8 cgen 8 parent 5 top level 5 parent_uuid - received_uuid - \ + uuid 777...-..05 path one +ID 258 gen 10 cgen 10 parent 5 top level 5 parent_uuid - received_uuid - \ + uuid a90...-..01 path another one +''', + }), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_list('/mnt', parent_id=True, + absolute=True, + ogeneration=True, + generation=True, + subvolumes=True, uuid=True, + parent_uuid=True, + sent_subvolume_uuid=True, + generation_cmp='-100', + ogeneration_cmp='+5', + sort=['-rootid', 'gen']) == [ + { + 'id': '257', + 'gen': '8', + 'cgen': '8', + 'parent': '5', + 'top level': '5', + 'parent_uuid': '-', + 'received_uuid': '-', + 'uuid': '777...-..05', + 'path': 'one', + }, + { + 'id': '258', + 'gen': '10', + 'cgen': '10', + 'parent': '5', + 'top level': '5', + 'parent_uuid': '-', + 'received_uuid': '-', + 'uuid': 'a90...-..01', + 'path': 'another one', + }, + ] + salt_mock['cmd.run_all'].assert_called_once() + salt_mock['cmd.run_all'].assert_called_with( + ['btrfs', 'subvolume', 'list', '-p', '-a', '-c', '-g', + '-o', '-u', '-q', '-R', '-G', '-100', '-C', '+5', + '--sort=-rootid,gen', '/mnt']) + + def test_subvolume_set_default(self): + ''' + Test btrfs subvolume set-default + ''' + salt_mock = { + 'cmd.run_all': MagicMock(return_value={'recode': 0}), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_set_default('257', '/mnt') + salt_mock['cmd.run_all'].assert_called_once() + salt_mock['cmd.run_all'].assert_called_with( + ['btrfs', 'subvolume', 'set-default', '257', '/mnt']) + + def test_subvolume_show(self): + ''' + Test btrfs subvolume show + ''' + salt_mock = { + 'cmd.run_all': MagicMock(return_value={ + 'recode': 0, + 'stdout': '''@/var + Name: var + UUID: 7a14...-...04 + Parent UUID: - + Received UUID: - + Creation time: 2018-10-01 14:33:12 +0200 + Subvolume ID: 258 + Generation: 82479 + Gen at creation: 10 + Parent ID: 256 + Top level ID: 256 + Flags: - + Snapshot(s): +''', + }), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_show('/var') == { + '@/var': { + 'name': 'var', + 'uuid': '7a14...-...04', + 'parent uuid': '-', + 'received uuid': '-', + 'creation time': '2018-10-01 14:33:12 +0200', + 'subvolume id': '258', + 'generation': '82479', + 'gen at creation': '10', + 'parent id': '256', + 'top level id': '256', + 'flags': '-', + 'snapshot(s)': '', + }, + } + salt_mock['cmd.run_all'].assert_called_once() + salt_mock['cmd.run_all'].assert_called_with( + ['btrfs', 'subvolume', 'show', '/var']) + + def test_subvolume_sync_fail_parameters(self): + ''' + Test btrfs subvolume sync + ''' + # Fails when subvolids is not a list + with pytest.raises(CommandExecutionError): + btrfs.subvolume_sync('/mnt', subvolids='257') + + def test_subvolume_sync(self): + ''' + Test btrfs subvolume sync + ''' + salt_mock = { + 'cmd.run_all': MagicMock(return_value={'recode': 0}), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs.subvolume_sync('/mnt', subvolids=['257'], + sleep='1') + salt_mock['cmd.run_all'].assert_called_once() + salt_mock['cmd.run_all'].assert_called_with( + ['btrfs', 'subvolume', 'sync', '-s', '1', '/mnt', '257']) diff --git a/tests/unit/modules/test_chroot.py b/tests/unit/modules/test_chroot.py new file mode 100644 index 00000000000..7181dd7e509 --- /dev/null +++ b/tests/unit/modules/test_chroot.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +# +# Author: Alberto Planas +# +# Copyright 2018 SUSE LINUX GmbH, Nuernberg, Germany. +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +''' +:maintainer: Alberto Planas +:platform: Linux +''' + +# Import Python Libs +from __future__ import absolute_import, print_function, unicode_literals + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import skipIf, TestCase +from tests.support.mock import ( + MagicMock, + NO_MOCK, + NO_MOCK_REASON, + patch, +) + +from salt.exceptions import CommandExecutionError +import salt.modules.chroot as chroot + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class ChrootTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.modules.chroot + ''' + + def setup_loader_modules(self): + return { + chroot: { + '__salt__': {}, + '__utils__': {}, + '__opts__': {'cachedir': ''}, + } + } + + @patch('os.path.isdir') + def test_exist(self, isdir): + ''' + Test if the chroot environment exist. + ''' + isdir.side_effect = (True, True, True) + self.assertTrue(chroot.exist('/chroot')) + + isdir.side_effect = (True, True, False) + self.assertFalse(chroot.exist('/chroot')) + + @patch('os.makedirs') + @patch('salt.modules.chroot.exist') + def test_create(self, exist, makedirs): + ''' + Test the creation of an empty chroot environment. + ''' + exist.return_value = True + self.assertTrue(chroot.create('/chroot')) + makedirs.assert_not_called() + + exist.return_value = False + self.assertTrue(chroot.create('/chroot')) + makedirs.assert_called() + + @patch('salt.modules.chroot.exist') + def test_call_fails_input_validation(self, exist): + ''' + Test execution of Salt functions in chroot. + ''' + # Basic input validation + exist.return_value = False + self.assertRaises(CommandExecutionError, chroot.call, '/chroot', '') + self.assertRaises(CommandExecutionError, chroot.call, '/chroot', 'test.ping') + + @patch('salt.modules.chroot.exist') + @patch('tempfile.mkdtemp') + def test_call_fails_untar(self, mkdtemp, exist): + ''' + Test execution of Salt functions in chroot. + ''' + # Fail the tar command + exist.return_value = True + mkdtemp.return_value = '/chroot/tmp01' + utils_mock = { + 'thin.gen_thin': MagicMock(return_value='/salt-thin.tgz'), + 'files.rm_rf': MagicMock(), + } + salt_mock = { + 'archive.tar': MagicMock(return_value='Error'), + 'config.option': MagicMock(), + } + with patch.dict(chroot.__utils__, utils_mock), \ + patch.dict(chroot.__salt__, salt_mock): + self.assertEqual(chroot.call('/chroot', 'test.ping'), { + 'result': False, + 'comment': 'Error' + }) + utils_mock['thin.gen_thin'].assert_called_once() + salt_mock['config.option'].assert_called() + salt_mock['archive.tar'].assert_called_once() + utils_mock['files.rm_rf'].assert_called_once() + + @patch('salt.modules.chroot.exist') + @patch('tempfile.mkdtemp') + def test_call_fails_salt_thin(self, mkdtemp, exist): + ''' + Test execution of Salt functions in chroot. + ''' + # Fail the inner command + exist.return_value = True + mkdtemp.return_value = '/chroot/tmp01' + utils_mock = { + 'thin.gen_thin': MagicMock(return_value='/salt-thin.tgz'), + 'files.rm_rf': MagicMock(), + } + salt_mock = { + 'archive.tar': MagicMock(return_value=''), + 'config.option': MagicMock(), + 'cmd.run_chroot': MagicMock(return_value={ + 'retcode': 1, + 'stderr': 'Error', + }), + } + with patch.dict(chroot.__utils__, utils_mock), \ + patch.dict(chroot.__salt__, salt_mock): + self.assertRaises(CommandExecutionError, chroot.call, '/chroot', + 'test.ping') + utils_mock['thin.gen_thin'].assert_called_once() + salt_mock['config.option'].assert_called() + salt_mock['archive.tar'].assert_called_once() + salt_mock['cmd.run_chroot'].assert_called_once() + utils_mock['files.rm_rf'].assert_called_once() + + @patch('salt.modules.chroot.exist') + @patch('tempfile.mkdtemp') + def test_call_success(self, mkdtemp, exist): + ''' + Test execution of Salt functions in chroot. + ''' + # Success test + exist.return_value = True + mkdtemp.return_value = '/chroot/tmp01' + utils_mock = { + 'thin.gen_thin': MagicMock(return_value='/salt-thin.tgz'), + 'files.rm_rf': MagicMock(), + 'json.find_json': MagicMock(return_value={'return': 'result'}) + } + salt_mock = { + 'archive.tar': MagicMock(return_value=''), + 'config.option': MagicMock(), + 'cmd.run_chroot': MagicMock(return_value={ + 'retcode': 0, + 'stdout': '', + }), + } + with patch.dict(chroot.__utils__, utils_mock), \ + patch.dict(chroot.__salt__, salt_mock): + self.assertEqual(chroot.call('/chroot', 'test.ping'), 'result') + utils_mock['thin.gen_thin'].assert_called_once() + salt_mock['config.option'].assert_called() + salt_mock['archive.tar'].assert_called_once() + salt_mock['cmd.run_chroot'].assert_called_once() + utils_mock['files.rm_rf'].assert_called_once() diff --git a/tests/unit/modules/test_freezer.py b/tests/unit/modules/test_freezer.py new file mode 100644 index 00000000000..f6cf2f374f0 --- /dev/null +++ b/tests/unit/modules/test_freezer.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- +# +# Author: Alberto Planas +# +# Copyright 2018 SUSE LINUX GmbH, Nuernberg, Germany. +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +''' +:maintainer: Alberto Planas +:platform: Linux +''' + +# Import Python Libs +from __future__ import absolute_import, print_function, unicode_literals + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import skipIf, TestCase +from tests.support.mock import ( + MagicMock, + NO_MOCK, + NO_MOCK_REASON, + patch, +) + +from salt.exceptions import CommandExecutionError +import salt.modules.freezer as freezer + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class FreezerTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.modules.freezer + ''' + + def setup_loader_modules(self): + return { + freezer: { + '__salt__': {}, + '__opts__': {'cachedir': ''}, + } + } + + @patch('os.path.isfile') + def test_status(self, isfile): + ''' + Test if a frozen state exist. + ''' + isfile.side_effect = (True, True) + self.assertTrue(freezer.status()) + + isfile.side_effect = (True, False) + self.assertFalse(freezer.status()) + + @patch('os.listdir') + @patch('os.path.isdir') + def test_list(self, isdir, listdir): + ''' + Test the listing of all frozen states. + ''' + # There is no freezer directory + isdir.return_value = False + self.assertEqual(freezer.list_(), []) + + # There is freezer directory, but is empty + isdir.return_value = True + listdir.return_value = [] + self.assertEqual(freezer.list_(), []) + + # There is freezer directory with states + isdir.return_value = True + listdir.return_value = [ + 'freezer-pkgs.yml', 'freezer-reps.yml', + 'state-pkgs.yml', 'state-reps.yml', + 'random-file' + ] + self.assertEqual(freezer.list_(), ['freezer', 'state']) + + @patch('os.makedirs') + def test_freeze_fails_cache(self, makedirs): + ''' + Test to freeze a current installation + ''' + # Fails when creating the freeze cache directory + makedirs.side_effect = OSError() + self.assertRaises(CommandExecutionError, freezer.freeze) + + @patch('salt.modules.freezer.status') + @patch('os.makedirs') + def test_freeze_fails_already_frozen(self, makedirs, status): + ''' + Test to freeze a current installation + ''' + # Fails when there is already a frozen state + status.return_value = True + self.assertRaises(CommandExecutionError, freezer.freeze) + makedirs.assert_called_once() + + @patch('salt.utils.json.dump') + @patch('salt.modules.freezer.fopen') + @patch('salt.modules.freezer.status') + @patch('os.makedirs') + def test_freeze_success_new_state(self, makedirs, status, fopen, dump): + ''' + Test to freeze a current installation + ''' + # Freeze the current new state + status.return_value = False + salt_mock = { + 'pkg.list_pkgs': MagicMock(return_value={}), + 'pkg.list_repos': MagicMock(return_value={}), + } + with patch.dict(freezer.__salt__, salt_mock): + self.assertTrue(freezer.freeze()) + makedirs.assert_called_once() + salt_mock['pkg.list_pkgs'].assert_called_once() + salt_mock['pkg.list_repos'].assert_called_once() + fopen.assert_called() + dump.asster_called() + + @patch('salt.utils.json.dump') + @patch('salt.modules.freezer.fopen') + @patch('salt.modules.freezer.status') + @patch('os.makedirs') + def test_freeze_success_force(self, makedirs, status, fopen, dump): + ''' + Test to freeze a current installation + ''' + # Freeze the current old state + status.return_value = True + salt_mock = { + 'pkg.list_pkgs': MagicMock(return_value={}), + 'pkg.list_repos': MagicMock(return_value={}), + } + with patch.dict(freezer.__salt__, salt_mock): + self.assertTrue(freezer.freeze(force=True)) + makedirs.assert_called_once() + salt_mock['pkg.list_pkgs'].assert_called_once() + salt_mock['pkg.list_repos'].assert_called_once() + fopen.assert_called() + dump.asster_called() + + @patch('salt.modules.freezer.status') + def test_restore_fails_missing_state(self, status): + ''' + Test to restore an old state + ''' + # Fails if the state is not found + status.return_value = False + self.assertRaises(CommandExecutionError, freezer.restore) + + @patch('salt.utils.json.load') + @patch('salt.modules.freezer.fopen') + @patch('salt.modules.freezer.status') + def test_restore_add_missing_repo(self, status, fopen, load): + ''' + Test to restore an old state + ''' + # Only a missing repo is installed + status.return_value = True + load.side_effect = ({}, {'missing-repo': {}}) + salt_mock = { + 'pkg.list_pkgs': MagicMock(return_value={}), + 'pkg.list_repos': MagicMock(return_value={}), + 'pkg.mod_repo': MagicMock(), + } + with patch.dict(freezer.__salt__, salt_mock): + self.assertEqual(freezer.restore(), { + 'pkgs': {'add': [], 'remove': []}, + 'repos': {'add': ['missing-repo'], 'remove': []}, + 'comment': [], + }) + salt_mock['pkg.list_pkgs'].assert_called() + salt_mock['pkg.list_repos'].assert_called() + salt_mock['pkg.mod_repo'].assert_called_once() + fopen.assert_called() + load.asster_called() + + @patch('salt.utils.json.load') + @patch('salt.modules.freezer.fopen') + @patch('salt.modules.freezer.status') + def test_restore_add_missing_package(self, status, fopen, load): + ''' + Test to restore an old state + ''' + # Only a missing package is installed + status.return_value = True + load.side_effect = ({'missing-package': {}}, {}) + salt_mock = { + 'pkg.list_pkgs': MagicMock(return_value={}), + 'pkg.list_repos': MagicMock(return_value={}), + 'pkg.install': MagicMock(), + } + with patch.dict(freezer.__salt__, salt_mock): + self.assertEqual(freezer.restore(), { + 'pkgs': {'add': ['missing-package'], 'remove': []}, + 'repos': {'add': [], 'remove': []}, + 'comment': [], + }) + salt_mock['pkg.list_pkgs'].assert_called() + salt_mock['pkg.list_repos'].assert_called() + salt_mock['pkg.install'].assert_called_once() + fopen.assert_called() + load.asster_called() + + @patch('salt.utils.json.load') + @patch('salt.modules.freezer.fopen') + @patch('salt.modules.freezer.status') + def test_restore_remove_extra_package(self, status, fopen, load): + ''' + Test to restore an old state + ''' + # Only an extra package is removed + status.return_value = True + load.side_effect = ({}, {}) + salt_mock = { + 'pkg.list_pkgs': MagicMock(return_value={'extra-package': {}}), + 'pkg.list_repos': MagicMock(return_value={}), + 'pkg.remove': MagicMock(), + } + with patch.dict(freezer.__salt__, salt_mock): + self.assertEqual(freezer.restore(), { + 'pkgs': {'add': [], 'remove': ['extra-package']}, + 'repos': {'add': [], 'remove': []}, + 'comment': [], + }) + salt_mock['pkg.list_pkgs'].assert_called() + salt_mock['pkg.list_repos'].assert_called() + salt_mock['pkg.remove'].assert_called_once() + fopen.assert_called() + load.asster_called() + + @patch('salt.utils.json.load') + @patch('salt.modules.freezer.fopen') + @patch('salt.modules.freezer.status') + def test_restore_remove_extra_repo(self, status, fopen, load): + ''' + Test to restore an old state + ''' + # Only an extra repository is removed + status.return_value = True + load.side_effect = ({}, {}) + salt_mock = { + 'pkg.list_pkgs': MagicMock(return_value={}), + 'pkg.list_repos': MagicMock(return_value={'extra-repo': {}}), + 'pkg.del_repo': MagicMock(), + } + with patch.dict(freezer.__salt__, salt_mock): + self.assertEqual(freezer.restore(), { + 'pkgs': {'add': [], 'remove': []}, + 'repos': {'add': [], 'remove': ['extra-repo']}, + 'comment': [], + }) + salt_mock['pkg.list_pkgs'].assert_called() + salt_mock['pkg.list_repos'].assert_called() + salt_mock['pkg.del_repo'].assert_called_once() + fopen.assert_called() + load.asster_called() diff --git a/tests/unit/modules/test_groupadd.py b/tests/unit/modules/test_groupadd.py index 8e0e64749ad..2ce7897a065 100644 --- a/tests/unit/modules/test_groupadd.py +++ b/tests/unit/modules/test_groupadd.py @@ -84,21 +84,19 @@ def test_chgid_gid_same(self): ''' Tests if the group id is the same as argument ''' - mock_pre_gid = MagicMock(return_value=10) - with patch.dict(groupadd.__salt__, - {'file.group_to_gid': mock_pre_gid}): + mock = MagicMock(return_value={'gid': 10}) + with patch.object(groupadd, 'info', mock): self.assertTrue(groupadd.chgid('test', 10)) def test_chgid(self): ''' Tests the gid for a named group was changed ''' - mock_pre_gid = MagicMock(return_value=0) - mock_cmdrun = MagicMock(return_value=0) - with patch.dict(groupadd.__salt__, - {'file.group_to_gid': mock_pre_gid}): - with patch.dict(groupadd.__salt__, {'cmd.run': mock_cmdrun}): - self.assertFalse(groupadd.chgid('test', 500)) + mock = MagicMock(return_value=None) + with patch.dict(groupadd.__salt__, {'cmd.run': mock}): + mock = MagicMock(side_effect=[{'gid': 10}, {'gid': 500}]) + with patch.object(groupadd, 'info', mock): + self.assertTrue(groupadd.chgid('test', 500)) # 'delete' function tests: 1 diff --git a/tests/unit/modules/test_mount.py b/tests/unit/modules/test_mount.py index a9df52f1d87..1a0d64f2f58 100644 --- a/tests/unit/modules/test_mount.py +++ b/tests/unit/modules/test_mount.py @@ -305,6 +305,13 @@ def test_mount(self): 'stderr': True}) with patch.dict(mount.__salt__, {'cmd.run_all': mock}): self.assertTrue(mount.mount('name', 'device')) + mock.assert_called_with('mount device name ', + python_shell=False, runas=None) + + with patch.dict(mount.__salt__, {'cmd.run_all': mock}): + self.assertTrue(mount.mount('name', 'device', fstype='fstype')) + mock.assert_called_with('mount -t fstype device name ', + python_shell=False, runas=None) mock = MagicMock(return_value={'retcode': False, 'stderr': False}) @@ -320,6 +327,35 @@ def test_mount(self): 'stderr': True}) with patch.dict(mount.__salt__, {'cmd.run_all': mock}): self.assertTrue(mount.mount('name', 'device')) + mock.assert_called_with('mount device name ', + python_shell=False, runas=None) + + with patch.dict(mount.__salt__, {'cmd.run_all': mock}): + self.assertTrue(mount.mount('name', 'device', fstype='fstype')) + mock.assert_called_with('mount -v fstype device name ', + python_shell=False, runas=None) + + mock = MagicMock(return_value={'retcode': False, + 'stderr': False}) + with patch.dict(mount.__salt__, {'cmd.run_all': mock}): + self.assertTrue(mount.mount('name', 'device')) + + with patch.dict(mount.__grains__, {'os': 'Linux'}): + mock = MagicMock(return_value=True) + with patch.object(os.path, 'exists', mock): + mock = MagicMock(return_value=None) + with patch.dict(mount.__salt__, {'file.mkdir': None}): + mock = MagicMock(return_value={'retcode': True, + 'stderr': True}) + with patch.dict(mount.__salt__, {'cmd.run_all': mock}): + self.assertTrue(mount.mount('name', 'device')) + mock.assert_called_with('mount -o defaults device name ', + python_shell=False, runas=None) + + with patch.dict(mount.__salt__, {'cmd.run_all': mock}): + self.assertTrue(mount.mount('name', 'device', fstype='fstype')) + mock.assert_called_with('mount -o defaults -t fstype device name ', + python_shell=False, runas=None) mock = MagicMock(return_value={'retcode': False, 'stderr': False}) diff --git a/tests/unit/modules/test_parted_partition.py b/tests/unit/modules/test_parted_partition.py index 8f381d55a6a..6f51fff0b35 100644 --- a/tests/unit/modules/test_parted_partition.py +++ b/tests/unit/modules/test_parted_partition.py @@ -376,3 +376,20 @@ def test_list__valid_unit_valid_legacy_cmd_output(self): } } self.assertEqual(output, expected) + + def test_disk_set(self): + with patch('salt.modules.parted._validate_device', MagicMock()): + self.cmdrun.return_value = '' + output = parted.disk_set('/dev/sda', 'pmbr_boot', 'on') + self.cmdrun.assert_called_once_with( + ['parted', '-m', '-s', '/dev/sda', 'disk_set', + 'pmbr_boot', 'on']) + assert output == [] + + def test_disk_toggle(self): + with patch('salt.modules.parted._validate_device', MagicMock()): + self.cmdrun.return_value = '' + output = parted.disk_toggle('/dev/sda', 'pmbr_boot') + self.cmdrun.assert_called_once_with( + ['parted', '-m', '-s', '/dev/sda', 'disk_toggle', 'pmbr_boot']) + assert output == [] diff --git a/tests/unit/modules/test_rpm_lowpkg.py b/tests/unit/modules/test_rpm_lowpkg.py index 0a2359ccb2f..dc9f52c572e 100644 --- a/tests/unit/modules/test_rpm_lowpkg.py +++ b/tests/unit/modules/test_rpm_lowpkg.py @@ -20,6 +20,11 @@ import salt.modules.rpm_lowpkg as rpm +def _called_with_root(mock): + cmd = ' '.join(mock.call_args[0][0]) + return cmd.startswith('rpm --root /') + + @skipIf(NO_MOCK, NO_MOCK_REASON) class RpmTestCase(TestCase, LoaderModuleMockMixin): ''' @@ -28,7 +33,7 @@ class RpmTestCase(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): return {rpm: {'rpm': MagicMock(return_value=MagicMock)}} - # 'list_pkgs' function tests: 1 + # 'list_pkgs' function tests: 2 def test_list_pkgs(self): ''' @@ -37,13 +42,24 @@ def test_list_pkgs(self): mock = MagicMock(return_value='') with patch.dict(rpm.__salt__, {'cmd.run': mock}): self.assertDictEqual(rpm.list_pkgs(), {}) + self.assertFalse(_called_with_root(mock)) + + def test_list_pkgs_root(self): + ''' + Test if it list the packages currently installed in a dict, + called with root parameter + ''' + mock = MagicMock(return_value='') + with patch.dict(rpm.__salt__, {'cmd.run': mock}): + rpm.list_pkgs(root='/') + self.assertTrue(_called_with_root(mock)) - # 'verify' function tests: 1 + # 'verify' function tests: 2 def test_verify(self): ''' - Test if it runs an rpm -Va on a system, - and returns the results in a dict + Test if it runs an rpm -Va on a system, and returns the + results in a dict ''' mock = MagicMock(return_value={'stdout': '', 'stderr': '', @@ -51,8 +67,22 @@ def test_verify(self): 'pid': 12345}) with patch.dict(rpm.__salt__, {'cmd.run_all': mock}): self.assertDictEqual(rpm.verify('httpd'), {}) + self.assertFalse(_called_with_root(mock)) + + def test_verify_root(self): + ''' + Test if it runs an rpm -Va on a system, and returns the + results in a dict, called with root parameter + ''' + mock = MagicMock(return_value={'stdout': '', + 'stderr': '', + 'retcode': 0, + 'pid': 12345}) + with patch.dict(rpm.__salt__, {'cmd.run_all': mock}): + rpm.verify('httpd', root='/') + self.assertTrue(_called_with_root(mock)) - # 'file_list' function tests: 1 + # 'file_list' function tests: 2 def test_file_list(self): ''' @@ -62,8 +92,20 @@ def test_file_list(self): with patch.dict(rpm.__salt__, {'cmd.run': mock}): self.assertDictEqual(rpm.file_list('httpd'), {'errors': [], 'files': []}) + self.assertFalse(_called_with_root(mock)) - # 'file_dict' function tests: 1 + def test_file_list_root(self): + ''' + Test if it list the files that belong to a package, using the + root parameter. + ''' + + mock = MagicMock(return_value='') + with patch.dict(rpm.__salt__, {'cmd.run': mock}): + rpm.file_list('httpd', root='/') + self.assertTrue(_called_with_root(mock)) + + # 'file_dict' function tests: 2 def test_file_dict(self): ''' @@ -73,6 +115,16 @@ def test_file_dict(self): with patch.dict(rpm.__salt__, {'cmd.run': mock}): self.assertDictEqual(rpm.file_dict('httpd'), {'errors': [], 'packages': {}}) + self.assertFalse(_called_with_root(mock)) + + def test_file_dict_root(self): + ''' + Test if it list the files that belong to a package + ''' + mock = MagicMock(return_value='') + with patch.dict(rpm.__salt__, {'cmd.run': mock}): + rpm.file_dict('httpd', root='/') + self.assertTrue(_called_with_root(mock)) # 'owner' function tests: 1 @@ -86,6 +138,7 @@ def test_owner(self): mock = MagicMock(return_value=ret) with patch.dict(rpm.__salt__, {'cmd.run_stdout': mock}): self.assertEqual(rpm.owner('/usr/bin/salt-jenkins-build'), '') + self.assertFalse(_called_with_root(mock)) ret = {'/usr/bin/vim': 'vim-enhanced-7.4.160-1.e17.x86_64', '/usr/bin/python': 'python-2.7.5-16.e17.x86_64'} @@ -94,8 +147,22 @@ def test_owner(self): with patch.dict(rpm.__salt__, {'cmd.run_stdout': mock}): self.assertDictEqual(rpm.owner('/usr/bin/python', '/usr/bin/vim'), ret) + self.assertFalse(_called_with_root(mock)) - # 'checksum' function tests: 1 + def test_owner_root(self): + ''' + Test if it return the name of the package that owns the file, + using the parameter root. + ''' + self.assertEqual(rpm.owner(), '') + + ret = 'file /usr/bin/salt-jenkins-build is not owned by any package' + mock = MagicMock(return_value=ret) + with patch.dict(rpm.__salt__, {'cmd.run_stdout': mock}): + rpm.owner('/usr/bin/salt-jenkins-build', root='/') + self.assertTrue(_called_with_root(mock)) + + # 'checksum' function tests: 2 def test_checksum(self): ''' @@ -110,6 +177,17 @@ def test_checksum(self): mock = MagicMock(side_effect=[True, 0, True, 1, False, 0]) with patch.dict(rpm.__salt__, {'file.file_exists': mock, 'cmd.retcode': mock}): self.assertDictEqual(rpm.checksum("file1.rpm", "file2.rpm", "file3.rpm"), ret) + self.assertFalse(_called_with_root(mock)) + + def test_checksum_root(self): + ''' + Test if checksum validate as expected, using the parameter + root + ''' + mock = MagicMock(side_effect=[True, 0]) + with patch.dict(rpm.__salt__, {'file.file_exists': mock, 'cmd.retcode': mock}): + rpm.checksum("file1.rpm", root='/') + self.assertTrue(_called_with_root(mock)) def test_version_cmp_rpm(self): ''' diff --git a/tests/unit/modules/test_systemd_service.py b/tests/unit/modules/test_systemd_service.py index 1d3a760c131..deeb23591b7 100644 --- a/tests/unit/modules/test_systemd_service.py +++ b/tests/unit/modules/test_systemd_service.py @@ -110,7 +110,7 @@ def test_get_enabled(self): 'README' ) ) - sysv_enabled_mock = MagicMock(side_effect=lambda x: x == 'baz') + sysv_enabled_mock = MagicMock(side_effect=lambda x, _: x == 'baz') with patch.dict(systemd.__salt__, {'cmd.run': cmd_mock}): with patch.object(os, 'listdir', listdir_mock): @@ -146,7 +146,7 @@ def test_get_disabled(self): 'README' ) ) - sysv_enabled_mock = MagicMock(side_effect=lambda x: x == 'baz') + sysv_enabled_mock = MagicMock(side_effect=lambda x, _: x == 'baz') with patch.dict(systemd.__salt__, {'cmd.run': cmd_mock}): with patch.object(os, 'listdir', listdir_mock): diff --git a/tests/unit/modules/test_useradd.py b/tests/unit/modules/test_useradd.py index 18da8d8ce86..74cafc64409 100644 --- a/tests/unit/modules/test_useradd.py +++ b/tests/unit/modules/test_useradd.py @@ -415,14 +415,15 @@ def test_rename(self): mock = MagicMock(return_value=None) with patch.dict(useradd.__salt__, {'cmd.run': mock}): - mock = MagicMock(side_effect=[{'name': ''}, False, + mock = MagicMock(side_effect=[False, {'name': ''}, {'name': 'salt'}]) with patch.object(useradd, 'info', mock): self.assertTrue(useradd.rename('name', 'salt')) mock = MagicMock(return_value=None) with patch.dict(useradd.__salt__, {'cmd.run': mock}): - mock = MagicMock(side_effect=[{'name': ''}, False, {'name': ''}]) + mock = MagicMock(side_effect=[False, {'name': ''}, + {'name': ''}]) with patch.object(useradd, 'info', mock): self.assertFalse(useradd.rename('salt', 'salt')) diff --git a/tests/unit/modules/test_zypperpkg.py b/tests/unit/modules/test_zypperpkg.py index 5c5091a5705..9d109a431dc 100644 --- a/tests/unit/modules/test_zypperpkg.py +++ b/tests/unit/modules/test_zypperpkg.py @@ -40,6 +40,9 @@ def __getattr__(self, item): return self def __call__(self, *args, **kwargs): + # If the call is for a configuration modifier, we return self + if any(i in kwargs for i in ('no_repo_failure', 'systemd_scope', 'root')): + return self return MagicMock(return_value=self.__return_value)() @@ -925,7 +928,7 @@ def __init__(self): 'pico': '0.1.1', } - def __call__(self): + def __call__(self, root=None): pkgs = self._pkgs.copy() for target in self._packages: if self._pkgs.get(target): @@ -991,10 +994,10 @@ def test_repo_add_nomod_noref(self): with zypper_patcher: zypper.mod_repo(name, **{'url': url}) self.assertEqual( - zypper.__zypper__.xml.call.call_args_list, + zypper.__zypper__(root=None).xml.call.call_args_list, [call('ar', url, name)] ) - self.assertTrue(zypper.__zypper__.refreshable.xml.call.call_count == 0) + self.assertTrue(zypper.__zypper__(root=None).refreshable.xml.call.call_count == 0) def test_repo_noadd_nomod_noref(self): ''' @@ -1016,8 +1019,8 @@ def test_repo_noadd_nomod_noref(self): self.assertEqual( out['comment'], 'Specified arguments did not result in modification of repo') - self.assertTrue(zypper.__zypper__.xml.call.call_count == 0) - self.assertTrue(zypper.__zypper__.refreshable.xml.call.call_count == 0) + self.assertTrue(zypper.__zypper__(root=None).xml.call.call_count == 0) + self.assertTrue(zypper.__zypper__(root=None).refreshable.xml.call.call_count == 0) def test_repo_noadd_modbaseurl_ref(self): ''' @@ -1045,9 +1048,11 @@ def test_repo_noadd_modbaseurl_ref(self): 'priority': 1, 'cache': False, 'keeppackages': False, - 'type': 'rpm-md'} - self.assertTrue(zypper.mod_repo.call_count == 2) - self.assertTrue(zypper.mod_repo.mock_calls[1] == call(name, **expected_params)) + 'type': 'rpm-md', + 'root': None, + } + self.assertEqual(zypper.mod_repo.call_count, 2) + self.assertEqual(zypper.mod_repo.mock_calls[1], call(name, **expected_params)) def test_repo_add_mod_noref(self): ''' @@ -1063,10 +1068,10 @@ def test_repo_add_mod_noref(self): with zypper_patcher: zypper.mod_repo(name, **{'url': url, 'refresh': True}) self.assertEqual( - zypper.__zypper__.xml.call.call_args_list, + zypper.__zypper__(root=None).xml.call.call_args_list, [call('ar', url, name)] ) - zypper.__zypper__.refreshable.xml.call.assert_called_once_with( + zypper.__zypper__(root=None).refreshable.xml.call.assert_called_once_with( 'mr', '--refresh', name ) @@ -1085,8 +1090,8 @@ def test_repo_noadd_mod_noref(self): 'salt.modules.zypperpkg', **self.zypper_patcher_config) with zypper_patcher: zypper.mod_repo(name, **{'url': url, 'refresh': True}) - self.assertTrue(zypper.__zypper__.xml.call.call_count == 0) - zypper.__zypper__.refreshable.xml.call.assert_called_once_with( + self.assertTrue(zypper.__zypper__(root=None).xml.call.call_count == 0) + zypper.__zypper__(root=None).refreshable.xml.call.assert_called_once_with( 'mr', '--refresh', name ) @@ -1105,13 +1110,13 @@ def test_repo_add_nomod_ref(self): with zypper_patcher: zypper.mod_repo(name, **{'url': url, 'gpgautoimport': True}) self.assertEqual( - zypper.__zypper__.xml.call.call_args_list, + zypper.__zypper__(root=None).xml.call.call_args_list, [ call('ar', url, name), call('--gpg-auto-import-keys', 'refresh', name) ] ) - self.assertTrue(zypper.__zypper__.refreshable.xml.call.call_count == 0) + self.assertTrue(zypper.__zypper__(root=None).refreshable.xml.call.call_count == 0) def test_repo_noadd_nomod_ref(self): ''' @@ -1132,10 +1137,10 @@ def test_repo_noadd_nomod_ref(self): with zypper_patcher: zypper.mod_repo(name, **{'url': url, 'gpgautoimport': True}) self.assertEqual( - zypper.__zypper__.xml.call.call_args_list, + zypper.__zypper__(root=None).xml.call.call_args_list, [call('--gpg-auto-import-keys', 'refresh', name)] ) - self.assertTrue(zypper.__zypper__.refreshable.xml.call.call_count == 0) + self.assertTrue(zypper.__zypper__(root=None).refreshable.xml.call.call_count == 0) def test_repo_add_mod_ref(self): ''' @@ -1156,13 +1161,13 @@ def test_repo_add_mod_ref(self): **{'url': url, 'refresh': True, 'gpgautoimport': True} ) self.assertEqual( - zypper.__zypper__.xml.call.call_args_list, + zypper.__zypper__(root=None).xml.call.call_args_list, [ call('ar', url, name), call('--gpg-auto-import-keys', 'refresh', name) ] ) - zypper.__zypper__.refreshable.xml.call.assert_called_once_with( + zypper.__zypper__(root=None).refreshable.xml.call.assert_called_once_with( '--gpg-auto-import-keys', 'mr', '--refresh', name ) @@ -1188,10 +1193,10 @@ def test_repo_noadd_mod_ref(self): **{'url': url, 'refresh': True, 'gpgautoimport': True} ) self.assertEqual( - zypper.__zypper__.xml.call.call_args_list, + zypper.__zypper__(root=None).xml.call.call_args_list, [call('--gpg-auto-import-keys', 'refresh', name)] ) - zypper.__zypper__.refreshable.xml.call.assert_called_once_with( + zypper.__zypper__(root=None).refreshable.xml.call.assert_called_once_with( '--gpg-auto-import-keys', 'mr', '--refresh', name ) diff --git a/tests/unit/states/test_btrfs.py b/tests/unit/states/test_btrfs.py new file mode 100644 index 00000000000..f7edd1f92ba --- /dev/null +++ b/tests/unit/states/test_btrfs.py @@ -0,0 +1,512 @@ +# -*- coding: utf-8 -*- +# +# Author: Alberto Planas +# +# Copyright 2018 SUSE LINUX GmbH, Nuernberg, Germany. +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +''' +:maintainer: Alberto Planas +:platform: Linux +''' +# Import Python Libs +from __future__ import absolute_import, print_function, unicode_literals +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import skipIf, TestCase +from tests.support.mock import ( + MagicMock, + NO_MOCK, + NO_MOCK_REASON, + patch, +) + +from salt.exceptions import CommandExecutionError +import salt.states.btrfs as btrfs + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class BtrfsTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.states.btrfs + ''' + + def setup_loader_modules(self): + return { + btrfs: { + '__salt__': {}, + '__states__': {}, + '__utils__': {}, + } + } + + @patch('salt.states.btrfs._umount') + @patch('tempfile.mkdtemp') + def test__mount_fails(self, mkdtemp, umount): + ''' + Test mounting a device in a temporary place. + ''' + mkdtemp.return_value = '/tmp/xxx' + states_mock = { + 'mount.mounted': MagicMock(return_value={'result': False}), + } + with patch.dict(btrfs.__states__, states_mock): + assert btrfs._mount('/dev/sda1') is None + mkdtemp.assert_called_once() + states_mock['mount.mounted'].assert_called_with('/tmp/xxx', + device='/dev/sda1', + fstype='btrfs', + opts='subvol=/', + persist=False) + umount.assert_called_with('/tmp/xxx') + + @patch('salt.states.btrfs._umount') + @patch('tempfile.mkdtemp') + def test__mount(self, mkdtemp, umount): + ''' + Test mounting a device in a temporary place. + ''' + mkdtemp.return_value = '/tmp/xxx' + states_mock = { + 'mount.mounted': MagicMock(return_value={'result': True}), + } + with patch.dict(btrfs.__states__, states_mock): + assert btrfs._mount('/dev/sda1') == '/tmp/xxx' + mkdtemp.assert_called_once() + states_mock['mount.mounted'].assert_called_with('/tmp/xxx', + device='/dev/sda1', + fstype='btrfs', + opts='subvol=/', + persist=False) + umount.assert_not_called() + + def test__umount(self): + ''' + Test umounting and cleanning temporary place. + ''' + states_mock = { + 'mount.unmounted': MagicMock(), + } + utils_mock = { + 'files.rm_rf': MagicMock(), + } + with patch.dict(btrfs.__states__, states_mock), \ + patch.dict(btrfs.__utils__, utils_mock): + btrfs._umount('/tmp/xxx') + states_mock['mount.unmounted'].assert_called_with('/tmp/xxx') + utils_mock['files.rm_rf'].assert_called_with('/tmp/xxx') + + def test__is_default_not_default(self): + ''' + Test if the subvolume is the current default. + ''' + salt_mock = { + 'btrfs.subvolume_show': MagicMock(return_value={ + '@/var': {'subvolume id': '256'}, + }), + 'btrfs.subvolume_get_default': MagicMock(return_value={ + 'id': '5', + }), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert not btrfs._is_default('/tmp/xxx/@/var', '/tmp/xxx', '@/var') + salt_mock['btrfs.subvolume_show'].assert_called_with('/tmp/xxx/@/var') + salt_mock['btrfs.subvolume_get_default'].assert_called_with('/tmp/xxx') + + def test__is_default(self): + ''' + Test if the subvolume is the current default. + ''' + salt_mock = { + 'btrfs.subvolume_show': MagicMock(return_value={ + '@/var': {'subvolume id': '256'}, + }), + 'btrfs.subvolume_get_default': MagicMock(return_value={ + 'id': '256', + }), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs._is_default('/tmp/xxx/@/var', '/tmp/xxx', '@/var') + salt_mock['btrfs.subvolume_show'].assert_called_with('/tmp/xxx/@/var') + salt_mock['btrfs.subvolume_get_default'].assert_called_with('/tmp/xxx') + + def test__set_default(self): + ''' + Test setting a subvolume as the current default. + ''' + salt_mock = { + 'btrfs.subvolume_show': MagicMock(return_value={ + '@/var': {'subvolume id': '256'}, + }), + 'btrfs.subvolume_set_default': MagicMock(return_value=True), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs._set_default('/tmp/xxx/@/var', '/tmp/xxx', '@/var') + salt_mock['btrfs.subvolume_show'].assert_called_with('/tmp/xxx/@/var') + salt_mock['btrfs.subvolume_set_default'].assert_called_with('256', '/tmp/xxx') + + def test__is_cow_not_cow(self): + ''' + Test if the subvolume is copy on write. + ''' + salt_mock = { + 'file.lsattr': MagicMock(return_value={ + '/tmp/xxx/@/var': ['C'], + }), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert not btrfs._is_cow('/tmp/xxx/@/var') + salt_mock['file.lsattr'].assert_called_with('/tmp/xxx/@') + + def test__is_cow(self): + ''' + Test if the subvolume is copy on write. + ''' + salt_mock = { + 'file.lsattr': MagicMock(return_value={ + '/tmp/xxx/@/var': [], + }), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs._is_cow('/tmp/xxx/@/var') + salt_mock['file.lsattr'].assert_called_with('/tmp/xxx/@') + + def test__unset_cow(self): + ''' + Test disabling the subvolume as copy on write. + ''' + salt_mock = { + 'file.chattr': MagicMock(return_value=True), + } + with patch.dict(btrfs.__salt__, salt_mock): + assert btrfs._unset_cow('/tmp/xxx/@/var') + salt_mock['file.chattr'].assert_called_with('/tmp/xxx/@/var', + operator='add', + attributes='C') + + @patch('salt.states.btrfs._umount') + @patch('salt.states.btrfs._mount') + def test_subvolume_created_exists(self, mount, umount): + ''' + Test creating a subvolume. + ''' + mount.return_value = '/tmp/xxx' + salt_mock = { + 'btrfs.subvolume_exists': MagicMock(return_value=True), + } + opts_mock = { + 'test': False, + } + with patch.dict(btrfs.__salt__, salt_mock), \ + patch.dict(btrfs.__opts__, opts_mock): + assert btrfs.subvolume_created(name='@/var', + device='/dev/sda1') == { + 'name': '@/var', + 'result': True, + 'changes': {}, + 'comment': ['Subvolume @/var already present'], + } + salt_mock['btrfs.subvolume_exists'].assert_called_with('/tmp/xxx/@/var') + mount.assert_called_once() + umount.assert_called_once() + + @patch('salt.states.btrfs._umount') + @patch('salt.states.btrfs._mount') + def test_subvolume_created_exists_test(self, mount, umount): + ''' + Test creating a subvolume. + ''' + mount.return_value = '/tmp/xxx' + salt_mock = { + 'btrfs.subvolume_exists': MagicMock(return_value=True), + } + opts_mock = { + 'test': True, + } + with patch.dict(btrfs.__salt__, salt_mock), \ + patch.dict(btrfs.__opts__, opts_mock): + assert btrfs.subvolume_created(name='@/var', + device='/dev/sda1') == { + 'name': '@/var', + 'result': None, + 'changes': {}, + 'comment': ['Subvolume @/var already present'], + } + salt_mock['btrfs.subvolume_exists'].assert_called_with('/tmp/xxx/@/var') + mount.assert_called_once() + umount.assert_called_once() + + @patch('salt.states.btrfs._is_default') + @patch('salt.states.btrfs._umount') + @patch('salt.states.btrfs._mount') + def test_subvolume_created_exists_was_default(self, mount, umount, + is_default): + ''' + Test creating a subvolume. + ''' + mount.return_value = '/tmp/xxx' + is_default.return_value = True + salt_mock = { + 'btrfs.subvolume_exists': MagicMock(return_value=True), + } + opts_mock = { + 'test': False, + } + with patch.dict(btrfs.__salt__, salt_mock), \ + patch.dict(btrfs.__opts__, opts_mock): + assert btrfs.subvolume_created(name='@/var', + device='/dev/sda1', + set_default=True) == { + 'name': '@/var', + 'result': True, + 'changes': {}, + 'comment': ['Subvolume @/var already present'], + } + salt_mock['btrfs.subvolume_exists'].assert_called_with('/tmp/xxx/@/var') + mount.assert_called_once() + umount.assert_called_once() + + @patch('salt.states.btrfs._set_default') + @patch('salt.states.btrfs._is_default') + @patch('salt.states.btrfs._umount') + @patch('salt.states.btrfs._mount') + def test_subvolume_created_exists_set_default(self, mount, umount, + is_default, set_default): + ''' + Test creating a subvolume. + ''' + mount.return_value = '/tmp/xxx' + is_default.return_value = False + set_default.return_value = True + salt_mock = { + 'btrfs.subvolume_exists': MagicMock(return_value=True), + } + opts_mock = { + 'test': False, + } + with patch.dict(btrfs.__salt__, salt_mock), \ + patch.dict(btrfs.__opts__, opts_mock): + assert btrfs.subvolume_created(name='@/var', + device='/dev/sda1', + set_default=True) == { + 'name': '@/var', + 'result': True, + 'changes': { + '@/var_default': True + }, + 'comment': ['Subvolume @/var already present'], + } + salt_mock['btrfs.subvolume_exists'].assert_called_with('/tmp/xxx/@/var') + mount.assert_called_once() + umount.assert_called_once() + + @patch('salt.states.btrfs._set_default') + @patch('salt.states.btrfs._is_default') + @patch('salt.states.btrfs._umount') + @patch('salt.states.btrfs._mount') + def test_subvolume_created_exists_set_default_no_force(self, + mount, + umount, + is_default, + set_default): + ''' + Test creating a subvolume. + ''' + mount.return_value = '/tmp/xxx' + is_default.return_value = False + set_default.return_value = True + salt_mock = { + 'btrfs.subvolume_exists': MagicMock(return_value=True), + } + opts_mock = { + 'test': False, + } + with patch.dict(btrfs.__salt__, salt_mock), \ + patch.dict(btrfs.__opts__, opts_mock): + assert btrfs.subvolume_created(name='@/var', + device='/dev/sda1', + set_default=True, + force_set_default=False) == { + 'name': '@/var', + 'result': True, + 'changes': {}, + 'comment': ['Subvolume @/var already present'], + } + salt_mock['btrfs.subvolume_exists'].assert_called_with('/tmp/xxx/@/var') + mount.assert_called_once() + umount.assert_called_once() + + @patch('salt.states.btrfs._is_cow') + @patch('salt.states.btrfs._umount') + @patch('salt.states.btrfs._mount') + def test_subvolume_created_exists_no_cow(self, mount, umount, is_cow): + ''' + Test creating a subvolume. + ''' + mount.return_value = '/tmp/xxx' + is_cow.return_value = False + salt_mock = { + 'btrfs.subvolume_exists': MagicMock(return_value=True), + } + opts_mock = { + 'test': False, + } + with patch.dict(btrfs.__salt__, salt_mock), \ + patch.dict(btrfs.__opts__, opts_mock): + assert btrfs.subvolume_created(name='@/var', + device='/dev/sda1', + copy_on_write=False) == { + 'name': '@/var', + 'result': True, + 'changes': {}, + 'comment': ['Subvolume @/var already present'], + } + salt_mock['btrfs.subvolume_exists'].assert_called_with('/tmp/xxx/@/var') + mount.assert_called_once() + umount.assert_called_once() + + @patch('salt.states.btrfs._unset_cow') + @patch('salt.states.btrfs._is_cow') + @patch('salt.states.btrfs._umount') + @patch('salt.states.btrfs._mount') + def test_subvolume_created_exists_unset_cow(self, mount, umount, + is_cow, unset_cow): + ''' + Test creating a subvolume. + ''' + mount.return_value = '/tmp/xxx' + is_cow.return_value = True + unset_cow.return_value = True + salt_mock = { + 'btrfs.subvolume_exists': MagicMock(return_value=True), + } + opts_mock = { + 'test': False, + } + with patch.dict(btrfs.__salt__, salt_mock), \ + patch.dict(btrfs.__opts__, opts_mock): + assert btrfs.subvolume_created(name='@/var', + device='/dev/sda1', + copy_on_write=False) == { + 'name': '@/var', + 'result': True, + 'changes': { + '@/var_no_cow': True + }, + 'comment': ['Subvolume @/var already present'], + } + salt_mock['btrfs.subvolume_exists'].assert_called_with('/tmp/xxx/@/var') + mount.assert_called_once() + umount.assert_called_once() + + @patch('salt.states.btrfs._umount') + @patch('salt.states.btrfs._mount') + def test_subvolume_created(self, mount, umount): + ''' + Test creating a subvolume. + ''' + mount.return_value = '/tmp/xxx' + salt_mock = { + 'btrfs.subvolume_exists': MagicMock(return_value=False), + 'btrfs.subvolume_create': MagicMock(), + } + states_mock = { + 'file.directory': MagicMock(return_value={'result': True}), + } + opts_mock = { + 'test': False, + } + with patch.dict(btrfs.__salt__, salt_mock), \ + patch.dict(btrfs.__states__, states_mock), \ + patch.dict(btrfs.__opts__, opts_mock): + assert btrfs.subvolume_created(name='@/var', + device='/dev/sda1') == { + 'name': '@/var', + 'result': True, + 'changes': { + '@/var': 'Created subvolume @/var' + }, + 'comment': [], + } + salt_mock['btrfs.subvolume_exists'].assert_called_with('/tmp/xxx/@/var') + salt_mock['btrfs.subvolume_create'].assert_called_once() + mount.assert_called_once() + umount.assert_called_once() + + @patch('salt.states.btrfs._umount') + @patch('salt.states.btrfs._mount') + def test_subvolume_created_fails_directory(self, mount, umount): + ''' + Test creating a subvolume. + ''' + mount.return_value = '/tmp/xxx' + salt_mock = { + 'btrfs.subvolume_exists': MagicMock(return_value=False), + } + states_mock = { + 'file.directory': MagicMock(return_value={'result': False}), + } + opts_mock = { + 'test': False, + } + with patch.dict(btrfs.__salt__, salt_mock), \ + patch.dict(btrfs.__states__, states_mock), \ + patch.dict(btrfs.__opts__, opts_mock): + assert btrfs.subvolume_created(name='@/var', + device='/dev/sda1') == { + 'name': '@/var', + 'result': False, + 'changes': {}, + 'comment': ['Error creating /tmp/xxx/@ directory'], + } + salt_mock['btrfs.subvolume_exists'].assert_called_with('/tmp/xxx/@/var') + mount.assert_called_once() + umount.assert_called_once() + + @patch('salt.states.btrfs._umount') + @patch('salt.states.btrfs._mount') + def test_subvolume_created_fails(self, mount, umount): + ''' + Test creating a subvolume. + ''' + mount.return_value = '/tmp/xxx' + salt_mock = { + 'btrfs.subvolume_exists': MagicMock(return_value=False), + 'btrfs.subvolume_create': MagicMock(side_effect=CommandExecutionError), + } + states_mock = { + 'file.directory': MagicMock(return_value={'result': True}), + } + opts_mock = { + 'test': False, + } + with patch.dict(btrfs.__salt__, salt_mock), \ + patch.dict(btrfs.__states__, states_mock), \ + patch.dict(btrfs.__opts__, opts_mock): + assert btrfs.subvolume_created(name='@/var', + device='/dev/sda1') == { + 'name': '@/var', + 'result': False, + 'changes': {}, + 'comment': ['Error creating subvolume @/var'], + } + salt_mock['btrfs.subvolume_exists'].assert_called_with('/tmp/xxx/@/var') + salt_mock['btrfs.subvolume_create'].assert_called_once() + mount.assert_called_once() + umount.assert_called_once() diff --git a/tests/unit/states/test_mount.py b/tests/unit/states/test_mount.py index 3e3a75d3cd8..2b35626b826 100644 --- a/tests/unit/states/test_mount.py +++ b/tests/unit/states/test_mount.py @@ -449,3 +449,596 @@ def test_mod_watch(self): 'changes': {}} self.assertDictEqual(mount.mod_watch(name, sfun='unmount'), ret) + + def test__convert_to_fast_none(self): + ''' + Test the device name conversor + ''' + assert mount._convert_to('/dev/sda1', None) == '/dev/sda1' + + def test__convert_to_fast_device(self): + ''' + Test the device name conversor + ''' + assert mount._convert_to('/dev/sda1', 'device') == '/dev/sda1' + + def test__convert_to_fast_token(self): + ''' + Test the device name conversor + ''' + assert mount._convert_to('LABEL=home', 'label') == 'LABEL=home' + + def test__convert_to_device_none(self): + ''' + Test the device name conversor + ''' + salt_mock = { + 'disk.blkid': MagicMock(return_value={}), + } + with patch.dict(mount.__salt__, salt_mock): + assert mount._convert_to('/dev/sda1', 'uuid') is None + salt_mock['disk.blkid'].assert_called_with('/dev/sda1') + + def test__convert_to_device_token(self): + ''' + Test the device name conversor + ''' + uuid = '988c663d-74a2-432b-ba52-3eea34015f22' + salt_mock = { + 'disk.blkid': MagicMock(return_value={ + '/dev/sda1': {'UUID': uuid} + }), + } + with patch.dict(mount.__salt__, salt_mock): + uuid = 'UUID={}'.format(uuid) + assert mount._convert_to('/dev/sda1', 'uuid') == uuid + salt_mock['disk.blkid'].assert_called_with('/dev/sda1') + + def test__convert_to_token_device(self): + ''' + Test the device name conversor + ''' + uuid = '988c663d-74a2-432b-ba52-3eea34015f22' + salt_mock = { + 'disk.blkid': MagicMock(return_value={ + '/dev/sda1': {'UUID': uuid} + }), + } + with patch.dict(mount.__salt__, salt_mock): + uuid = 'UUID={}'.format(uuid) + assert mount._convert_to(uuid, 'device') == '/dev/sda1' + salt_mock['disk.blkid'].assert_called_with(token=uuid) + + def test_fstab_present_macos_test_present(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': None, + 'changes': {}, + 'comment': ['/home entry is already in /etc/auto_salt.'], + } + + grains_mock = {'os': 'MacOS'} + opts_mock = {'test': True} + salt_mock = { + 'mount.set_automaster': MagicMock(return_value='present') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_automaster'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + opts='noowners', + config='/etc/auto_salt', + test=True) + + def test_fstab_present_aix_test_present(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': None, + 'changes': {}, + 'comment': ['/home entry is already in /etc/filesystems.'], + } + + grains_mock = {'os': 'AIX'} + opts_mock = {'test': True} + salt_mock = { + 'mount.set_filesystems': MagicMock(return_value='present') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_filesystems'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + mount=True, + opts='', + config='/etc/filesystems', + test=True, + match_on='auto') + + def test_fstab_present_test_present(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': None, + 'changes': {}, + 'comment': ['/home entry is already in /etc/fstab.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': True} + salt_mock = { + 'mount.set_fstab': MagicMock(return_value='present') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_fstab'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + opts='defaults', + dump=0, + pass_num=0, + config='/etc/fstab', + test=True, + match_on='auto') + + def test_fstab_present_test_new(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': None, + 'changes': {}, + 'comment': ['/home entry will be written in /etc/fstab.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': True} + salt_mock = { + 'mount.set_fstab': MagicMock(return_value='new') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_fstab'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + opts='defaults', + dump=0, + pass_num=0, + config='/etc/fstab', + test=True, + match_on='auto') + + def test_fstab_present_test_change(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': None, + 'changes': {}, + 'comment': ['/home entry will be updated in /etc/fstab.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': True} + salt_mock = { + 'mount.set_fstab': MagicMock(return_value='change') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_fstab'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + opts='defaults', + dump=0, + pass_num=0, + config='/etc/fstab', + test=True, + match_on='auto') + + def test_fstab_present_test_error(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': False, + 'changes': {}, + 'comment': ['/home entry cannot be created in /etc/fstab: error.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': True} + salt_mock = { + 'mount.set_fstab': MagicMock(return_value='error') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_fstab'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + opts='defaults', + dump=0, + pass_num=0, + config='/etc/fstab', + test=True, + match_on='auto') + + def test_fstab_present_macos_present(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': True, + 'changes': {}, + 'comment': ['/home entry was already in /etc/auto_salt.'], + } + + grains_mock = {'os': 'MacOS'} + opts_mock = {'test': False} + salt_mock = { + 'mount.set_automaster': MagicMock(return_value='present') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_automaster'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + opts='noowners', + config='/etc/auto_salt') + + def test_fstab_present_aix_present(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': True, + 'changes': {}, + 'comment': ['/home entry was already in /etc/filesystems.'], + } + + grains_mock = {'os': 'AIX'} + opts_mock = {'test': False} + salt_mock = { + 'mount.set_filesystems': MagicMock(return_value='present') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_filesystems'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + mount=True, + opts='', + config='/etc/filesystems', + match_on='auto') + + def test_fstab_present_present(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': True, + 'changes': {}, + 'comment': ['/home entry was already in /etc/fstab.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': False} + salt_mock = { + 'mount.set_fstab': MagicMock(return_value='present') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_fstab'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + opts='defaults', + dump=0, + pass_num=0, + config='/etc/fstab', + match_on='auto') + + def test_fstab_present_new(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': True, + 'changes': {'persist': 'new'}, + 'comment': ['/home entry added in /etc/fstab.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': False} + salt_mock = { + 'mount.set_fstab': MagicMock(return_value='new') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_fstab'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + opts='defaults', + dump=0, + pass_num=0, + config='/etc/fstab', + match_on='auto') + + def test_fstab_present_change(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': True, + 'changes': {'persist': 'change'}, + 'comment': ['/home entry updated in /etc/fstab.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': False} + salt_mock = { + 'mount.set_fstab': MagicMock(return_value='change') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_fstab'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + opts='defaults', + dump=0, + pass_num=0, + config='/etc/fstab', + match_on='auto') + + def test_fstab_present_fail(self): + ''' + Test fstab_present + ''' + ret = { + 'name': '/dev/sda1', + 'result': False, + 'changes': {}, + 'comment': ['/home entry cannot be changed in /etc/fstab: error.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': False} + salt_mock = { + 'mount.set_fstab': MagicMock(return_value='error') + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_present('/dev/sda1', '/home', 'ext2') == ret + salt_mock['mount.set_fstab'].assert_called_with(name='/home', + device='/dev/sda1', + fstype='ext2', + opts='defaults', + dump=0, + pass_num=0, + config='/etc/fstab', + match_on='auto') + + def test_fstab_absent_macos_test_absent(self): + ''' + Test fstab_absent + ''' + ret = { + 'name': '/dev/sda1', + 'result': None, + 'changes': {}, + 'comment': ['/home entry is already missing in /etc/auto_salt.'], + } + + grains_mock = {'os': 'MacOS'} + opts_mock = {'test': True} + salt_mock = { + 'mount.automaster': MagicMock(return_value={}) + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_absent('/dev/sda1', '/home') == ret + salt_mock['mount.automaster'].assert_called_with('/etc/auto_salt') + + def test_fstab_absent_aix_test_absent(self): + ''' + Test fstab_absent + ''' + ret = { + 'name': '/dev/sda1', + 'result': None, + 'changes': {}, + 'comment': ['/home entry is already missing in /etc/filesystems.'], + } + + grains_mock = {'os': 'AIX'} + opts_mock = {'test': True} + salt_mock = { + 'mount.filesystems': MagicMock(return_value={}) + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_absent('/dev/sda1', '/home') == ret + salt_mock['mount.filesystems'].assert_called_with('/etc/filesystems') + + def test_fstab_absent_test_absent(self): + ''' + Test fstab_absent + ''' + ret = { + 'name': '/dev/sda1', + 'result': None, + 'changes': {}, + 'comment': ['/home entry is already missing in /etc/fstab.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': True} + salt_mock = { + 'mount.fstab': MagicMock(return_value={}) + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_absent('/dev/sda1', '/home') == ret + salt_mock['mount.fstab'].assert_called_with('/etc/fstab') + + def test_fstab_absent_test_present(self): + ''' + Test fstab_absent + ''' + ret = { + 'name': '/dev/sda1', + 'result': None, + 'changes': {}, + 'comment': ['/home entry will be removed from /etc/fstab.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': True} + salt_mock = { + 'mount.fstab': MagicMock(return_value={'/home': {}}) + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_absent('/dev/sda1', '/home') == ret + salt_mock['mount.fstab'].assert_called_with('/etc/fstab') + + def test_fstab_absent_macos_present(self): + ''' + Test fstab_absent + ''' + ret = { + 'name': '/dev/sda1', + 'result': True, + 'changes': {'persist': 'removed'}, + 'comment': ['/home entry removed from /etc/auto_salt.'], + } + + grains_mock = {'os': 'MacOS'} + opts_mock = {'test': False} + salt_mock = { + 'mount.automaster': MagicMock(return_value={'/home': {}}), + 'mount.rm_automaster': MagicMock(return_value=True) + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_absent('/dev/sda1', '/home') == ret + salt_mock['mount.automaster'].assert_called_with('/etc/auto_salt') + salt_mock['mount.rm_automaster'].assert_called_with(name='/home', + device='/dev/sda1', + config='/etc/auto_salt') + + def test_fstab_absent_aix_present(self): + ''' + Test fstab_absent + ''' + ret = { + 'name': '/dev/sda1', + 'result': True, + 'changes': {'persist': 'removed'}, + 'comment': ['/home entry removed from /etc/filesystems.'], + } + + grains_mock = {'os': 'AIX'} + opts_mock = {'test': False} + salt_mock = { + 'mount.filesystems': MagicMock(return_value={'/home': {}}), + 'mount.rm_filesystems': MagicMock(return_value=True) + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_absent('/dev/sda1', '/home') == ret + salt_mock['mount.filesystems'].assert_called_with('/etc/filesystems') + salt_mock['mount.rm_filesystems'].assert_called_with(name='/home', + device='/dev/sda1', + config='/etc/filesystems') + + def test_fstab_absent_present(self): + ''' + Test fstab_absent + ''' + ret = { + 'name': '/dev/sda1', + 'result': True, + 'changes': {'persist': 'removed'}, + 'comment': ['/home entry removed from /etc/fstab.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': False} + salt_mock = { + 'mount.fstab': MagicMock(return_value={'/home': {}}), + 'mount.rm_fstab': MagicMock(return_value=True) + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_absent('/dev/sda1', '/home') == ret + salt_mock['mount.fstab'].assert_called_with('/etc/fstab') + salt_mock['mount.rm_fstab'].assert_called_with(name='/home', + device='/dev/sda1', + config='/etc/fstab') + + def test_fstab_absent_absent(self): + ''' + Test fstab_absent + ''' + ret = { + 'name': '/dev/sda1', + 'result': True, + 'changes': {}, + 'comment': ['/home entry is already missing in /etc/fstab.'], + } + + grains_mock = {'os': 'Linux'} + opts_mock = {'test': False} + salt_mock = { + 'mount.fstab': MagicMock(return_value={}) + } + with patch.dict(mount.__grains__, grains_mock), \ + patch.dict(mount.__opts__, opts_mock), \ + patch.dict(mount.__salt__, salt_mock): + assert mount.fstab_absent('/dev/sda1', '/home') == ret + salt_mock['mount.fstab'].assert_called_with('/etc/fstab') diff --git a/tests/unit/states/test_pkg.py b/tests/unit/states/test_pkg.py index 42fe6c6867a..d30e0641670 100644 --- a/tests/unit/states/test_pkg.py +++ b/tests/unit/states/test_pkg.py @@ -46,7 +46,7 @@ def test_uptodate_with_changes(self): pkgname: pkgver['new'] for pkgname, pkgver in six.iteritems(self.pkgs) }) upgrade = MagicMock(return_value=self.pkgs) - version = MagicMock(side_effect=lambda pkgname: self.pkgs[pkgname]['old']) + version = MagicMock(side_effect=lambda pkgname, **_: self.pkgs[pkgname]['old']) with patch.dict(pkg.__salt__, {'pkg.list_upgrades': list_upgrades, @@ -55,7 +55,6 @@ def test_uptodate_with_changes(self): # Run state with test=false with patch.dict(pkg.__opts__, {'test': False}): - ret = pkg.uptodate('dummy', test=True) self.assertTrue(ret['result']) self.assertDictEqual(ret['changes'], self.pkgs) @@ -81,7 +80,7 @@ def test_uptodate_with_pkgs_with_changes(self): pkgname: pkgver['new'] for pkgname, pkgver in six.iteritems(self.pkgs) }) upgrade = MagicMock(return_value=self.pkgs) - version = MagicMock(side_effect=lambda pkgname: pkgs[pkgname]['old']) + version = MagicMock(side_effect=lambda pkgname, **_: pkgs[pkgname]['old']) with patch.dict(pkg.__salt__, {'pkg.list_upgrades': list_upgrades, @@ -160,7 +159,7 @@ def test_uptodate_with_failed_changes(self): pkgname: pkgver['new'] for pkgname, pkgver in six.iteritems(self.pkgs) }) upgrade = MagicMock(return_value={}) - version = MagicMock(side_effect=lambda pkgname: pkgs[pkgname]['old']) + version = MagicMock(side_effect=lambda pkgname, **_: pkgs[pkgname]['old']) with patch.dict(pkg.__salt__, {'pkg.list_upgrades': list_upgrades,