From 6ec4bbf95c051caae92d48fc8a8cf7f51ce7d05a Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Wed, 30 Jan 2019 17:17:17 +0100 Subject: [PATCH] Backport of: * blockdev: fix url from comment https://github.com/saltstack/salt/pull/49668 * Documentation: fix typo in "equivalent" https://github.com/saltstack/salt/pull/49669 * states_pt3: fix rST link format https://github.com/saltstack/salt/pull/49670 * parted: fix _validate_partition_boundary https://github.com/saltstack/salt/pull/49803 * parted: fix the ordering of list command https://github.com/saltstack/salt/pull/49804 * Fix lowpkg.diff documentation and parameter name https://github.com/saltstack/salt/pull/50126 * 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 * service: SUSE is not based on sysvinit anymore https://github.com/saltstack/salt/pull/50396 * Add new chroot module https://github.com/saltstack/salt/pull/50418 * Add new module freezer https://github.com/saltstack/salt/pull/50452 * parted: support variable length output for print https://github.com/saltstack/salt/pull/50473 * btrfs: add all subvolume commands https://github.com/saltstack/salt/pull/50541 * file: update attributes for lsattr and chattr https://github.com/saltstack/salt/pull/50607 * 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 * cmdmod: add sysfs into the chroot https://github.com/saltstack/salt/pull/51094 * mount: cache blkid information https://github.com/saltstack/salt/pull/51135 --- doc/man/salt.7 | 2 +- doc/topics/tutorials/states_pt3.rst | 4 +- salt/modules/btrfs.py | 496 ++++++++++++++++++++++- salt/modules/chroot.py | 165 ++++++++ salt/modules/cmdmod.py | 7 +- salt/modules/disk.py | 26 +- salt/modules/file.py | 8 +- salt/modules/freezer.py | 294 ++++++++++++++ salt/modules/groupadd.py | 177 +++++++-- salt/modules/mount.py | 4 +- salt/modules/parted.py | 140 +++++-- salt/modules/rpm.py | 8 +- salt/modules/service.py | 1 + salt/modules/shadow.py | 300 ++++++++++---- salt/modules/systemd.py | 209 ++++++---- salt/modules/useradd.py | 390 ++++++++++++++---- salt/modules/zypper.py | 2 +- salt/states/blockdev.py | 2 +- salt/states/btrfs.py | 258 ++++++++++++ salt/states/cmd.py | 18 +- salt/states/file.py | 2 +- salt/states/mount.py | 292 ++++++++++++++ salt/states/netconfig.py | 2 +- 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_parted.py | 90 ++++- tests/unit/modules/test_systemd.py | 4 +- tests/unit/modules/test_useradd.py | 5 +- tests/unit/states/test_btrfs.py | 512 ++++++++++++++++++++++++ tests/unit/states/test_mount.py | 593 ++++++++++++++++++++++++++++ 32 files changed, 4520 insertions(+), 335 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/doc/man/salt.7 b/doc/man/salt.7 index 72998c2dc9f..a5347178824 100644 --- a/doc/man/salt.7 +++ b/doc/man/salt.7 @@ -45993,7 +45993,7 @@ moe: .UNINDENT .sp One way to think about this might be that the \fBgid\fP key is being assigned -a value equivelent to the following python pseudo\-code: +a value equivalent to the following python pseudo\-code: .INDENT 0.0 .INDENT 3.5 .sp diff --git a/doc/topics/tutorials/states_pt3.rst b/doc/topics/tutorials/states_pt3.rst index d401606b22c..fd0d052abb0 100644 --- a/doc/topics/tutorials/states_pt3.rst +++ b/doc/topics/tutorials/states_pt3.rst @@ -141,7 +141,7 @@ The following example illustrates calling the ``group_to_gid`` function in the - gid: {{ salt['file.group_to_gid']('some_group_that_exists') }} One way to think about this might be that the ``gid`` key is being assigned -a value equivelent to the following python pseudo-code: +a value equivalent to the following python pseudo-code: .. code-block:: python @@ -159,7 +159,7 @@ MAC address for eth0: salt['network.hw_addr']('eth0') To examine the possible arguments to each execution module function, -one can examine the `module reference documentation `: +one can examine the `module reference documentation `_: Advanced SLS module syntax ========================== 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 0bfaaf38e95..a10f07c157d 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -3035,12 +3035,16 @@ def run_chroot(root, ''' __salt__['mount.mount']( os.path.join(root, 'dev'), - 'udev', + 'devtmpfs', fstype='devtmpfs') __salt__['mount.mount']( os.path.join(root, 'proc'), 'proc', fstype='proc') + __salt__['mount.mount']( + os.path.join(root, 'sys'), + 'sysfs', + fstype='sysfs') # Execute chroot routine sh_ = '/bin/sh' @@ -3092,6 +3096,7 @@ def run_chroot(root, log.error('Processes running in chroot could not be killed, ' 'filesystem will remain mounted') + __salt__['mount.umount'](os.path.join(root, 'sys')) __salt__['mount.umount'](os.path.join(root, 'proc')) __salt__['mount.umount'](os.path.join(root, 'dev')) if hide_output: 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/file.py b/salt/modules/file.py index 2e4eaf11bb9..f85103d18d1 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -577,7 +577,7 @@ def lsattr(path): for line in result.splitlines(): if not line.startswith('lsattr: '): vals = line.split(None, 1) - results[vals[1]] = re.findall(r"[acdijstuADST]", vals[0]) + results[vals[1]] = re.findall(r"[aAcCdDeijPsStTu]", vals[0]) return results @@ -594,8 +594,8 @@ def chattr(*files, **kwargs): should be added or removed from files attributes - One or more of the following characters: ``acdijstuADST``, representing - attributes to add to/remove from files + One or more of the following characters: ``aAcCdDeijPsStTu``, + representing attributes to add to/remove from files version a version number to assign to the file(s) @@ -620,7 +620,7 @@ def chattr(*files, **kwargs): raise SaltInvocationError( "Need an operator: 'add' or 'remove' to modify attributes.") if attributes is None: - raise SaltInvocationError("Need attributes: [AacDdijsTtSu]") + raise SaltInvocationError("Need attributes: [aAcCdDeijPsStTu]") cmd = ['chattr'] 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/mount.py b/salt/modules/mount.py index 68cfd1fc6af..1b3d02de9fd 100644 --- a/salt/modules/mount.py +++ b/salt/modules/mount.py @@ -63,7 +63,9 @@ def _active_mountinfo(ret): msg = 'File not readable {0}' raise CommandExecutionError(msg.format(filename)) - blkid_info = __salt__['disk.blkid']() + if 'disk.blkid' not in __context__: + __context__['disk.blkid'] = __salt__['disk.blkid']() + blkid_info = __context__['disk.blkid'] with salt.utils.files.fopen(filename) as ifile: for line in ifile: diff --git a/salt/modules/parted.py b/salt/modules/parted.py index b5f50e1e687..03582df1f3d 100644 --- a/salt/modules/parted.py +++ b/salt/modules/parted.py @@ -19,6 +19,7 @@ # Import python libs import os +import re import stat import string import logging @@ -43,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__(): ''' @@ -91,15 +101,15 @@ def _validate_partition_boundary(boundary): ''' Ensure valid partition boundaries are supplied. ''' - try: - for unit in VALID_UNITS: - if six.text_type(boundary).endswith(unit): - return - int(boundary) - except Exception: - raise CommandExecutionError( - 'Invalid partition boundary passed: "{0}"'.format(boundary) - ) + boundary = six.text_type(boundary) + match = re.search(r'^([\d.]+)(\D*)$', boundary) + if match: + unit = match.group(2) + if not unit or unit in VALID_UNITS: + return + raise CommandExecutionError( + 'Invalid partition boundary passed: "{0}"'.format(boundary) + ) def probe(*devices): @@ -155,7 +165,7 @@ def list_(device, unit=None): for line in out: if line in ('BYT;', 'CHS;', 'CYL;'): continue - cols = line.replace(';', '').split(':') + cols = line.rstrip(';').split(':') if mode == 'info': if 7 <= len(cols) <= 8: ret['info'] = { @@ -177,15 +187,26 @@ def list_(device, unit=None): raise CommandExecutionError( 'Problem encountered while parsing output from parted') else: - if len(cols) == 7: - ret['partitions'][cols[0]] = { - 'number': cols[0], - 'start': cols[1], - 'end': cols[2], - 'size': cols[3], - 'type': cols[4], - 'file system': cols[5], - 'flags': cols[6]} + # Parted (v3.1) have a variable field list in machine + # readable output: + # + # number:start:end:[size:]([file system:name:flags;]|[free;]) + # + # * If units are in CHS 'size' is not printed. + # * If is a logical partition with PED_PARTITION_FREESPACE + # set, the last three fields are replaced with the + # 'free' text. + # + fields = ['number', 'start', 'end'] + if unit != 'chs': + fields.append('size') + if cols[-1] == 'free': + # Drop the last element from the list + cols.pop() + else: + fields.extend(['file system', 'name', 'flags']) + if len(fields) == len(cols): + ret['partitions'][cols[0]] = dict(six.moves.zip(fields, cols)) else: raise CommandExecutionError( 'Problem encountered while parsing output from parted') @@ -629,8 +650,25 @@ 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: @@ -647,8 +685,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']): @@ -679,8 +716,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) @@ -688,6 +724,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/rpm.py b/salt/modules/rpm.py index 21c9232c1ac..e577c4391a0 100644 --- a/salt/modules/rpm.py +++ b/salt/modules/rpm.py @@ -461,13 +461,13 @@ def owner(*paths, **kwargs): @salt.utils.decorators.path.which('rpm2cpio') @salt.utils.decorators.path.which('cpio') @salt.utils.decorators.path.which('diff') -def diff(package, path): +def diff(package_path, path): ''' Return a formatted diff between current file and original in a package. NOTE: this function includes all files (configuration and not), but does not work on binary content. - :param package: The name of the package + :param package: Full pack of the RPM file :param path: Full path to the installed file :return: Difference or empty string. For binary files only a notification. @@ -475,13 +475,13 @@ def diff(package, path): .. code-block:: bash - salt '*' lowpkg.diff apache2 /etc/apache2/httpd.conf + salt '*' lowpkg.diff /path/to/apache2.rpm /etc/apache2/httpd.conf ''' cmd = "rpm2cpio {0} " \ "| cpio -i --quiet --to-stdout .{1} " \ "| diff -u --label 'A {1}' --from-file=- --label 'B {1}' {1}" - res = __salt__['cmd.shell'](cmd.format(package, path), + res = __salt__['cmd.shell'](cmd.format(package_path, path), output_loglevel='trace') if res and res.startswith('Binary file'): return 'File \'{0}\' is binary and its content has been ' \ diff --git a/salt/modules/service.py b/salt/modules/service.py index 81090826e48..002b070c303 100644 --- a/salt/modules/service.py +++ b/salt/modules/service.py @@ -41,6 +41,7 @@ def __virtual__(): 'elementary OS', 'McAfee OS Server', 'Raspbian', + 'SUSE', )) if __grains__.get('os') in disable: return (False, 'Your OS is on the disabled list') diff --git a/salt/modules/shadow.py b/salt/modules/shadow.py index 9659867f050..98c7369c5eb 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'] - post_info = info(name) + if root is not None: + cmd.extend(('-R', root)) - return post_info['passwd'][0] != '!' + cmd.extend(('-u', name)) + + __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,54 +394,67 @@ 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: .. code-block:: bash 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/systemd.py b/salt/modules/systemd.py index fb349d30e6c..3efd455ba68 100644 --- a/salt/modules/systemd.py +++ b/salt/modules/systemd.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/zypper.py b/salt/modules/zypper.py index bab9e22dec5..4c09f66016d 100644 --- a/salt/modules/zypper.py +++ b/salt/modules/zypper.py @@ -986,7 +986,7 @@ def _get_configured_repos(root=None): if os.path.exists(repos): repos_cfg.read([repos + '/' + fname for fname in os.listdir(repos) if fname.endswith(".repo")]) else: - log.error('Repositories not found in {}'.format(repos)) + log.warning('Repositories not found in {}'.format(repos)) return repos_cfg diff --git a/salt/states/blockdev.py b/salt/states/blockdev.py index 9ff9091ec7b..38543ac8a07 100644 --- a/salt/states/blockdev.py +++ b/salt/states/blockdev.py @@ -163,7 +163,7 @@ def formatted(name, fs_type='ext4', force=False, **kwargs): # Repeat fstype check up to 10 times with 3s sleeping between each # to avoid detection failing although mkfs has succeeded - # see https://github.com/saltstack/salt/issues/25775i + # see https://github.com/saltstack/salt/issues/25775 # This retry maybe superfluous - switching to blkid for i in range(10): 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/file.py b/salt/states/file.py index dfbe672daa7..4ba20d3e57a 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -2116,7 +2116,7 @@ def managed(name, attrs The attributes to have on this file, e.g. ``a``, ``i``. The attributes can be any or a combination of the following characters: - ``acdijstuADST``. + ``aAcCdDeijPsStTu``. .. note:: This option is **not** supported on Windows. 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/netconfig.py b/salt/states/netconfig.py index b1f8ed65e4d..535a96805d3 100644 --- a/salt/states/netconfig.py +++ b/salt/states/netconfig.py @@ -303,7 +303,7 @@ def saved(name, attrs The attributes to have on this file, e.g. ``a``, ``i``. The attributes can be any or a combination of the following characters: - ``acdijstuADST``. + ``aAcCdDeijPsStTu``. .. note:: This option is **not** supported on Windows. 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_parted.py b/tests/unit/modules/test_parted.py index 0bf9bd53056..4a35002f3f9 100644 --- a/tests/unit/modules/test_parted.py +++ b/tests/unit/modules/test_parted.py @@ -116,6 +116,12 @@ def parted_print_output(k): '''1:17.4kB:150MB:150MB:ext3::boot;\n''' '''2:3921GB:4000GB:79.3GB:linux-swap(v1)::;\n''' ), + "valid chs": ( + '''CHS;\n''' + '''/dev/sda:3133,0,2:scsi:512:512:gpt:AMCC 9650SE-24M DISK:;\n''' + '''1:0,0,34:2431,134,43:ext3::boot;\n''' + '''2:2431,134,44:2492,80,42:linux-swap(v1)::;\n''' + ), "valid_legacy": ( '''BYT;\n''' '''/dev/sda:4000GB:scsi:512:512:gpt:AMCC 9650SE-24M DISK;\n''' @@ -207,17 +213,17 @@ def test_list__valid_cmd_output(self): 'end': '150MB', 'number': '1', 'start': '17.4kB', - 'file system': '', + 'file system': 'ext3', 'flags': 'boot', - 'type': 'ext3', + 'name': '', 'size': '150MB'}, '2': { 'end': '4000GB', 'number': '2', 'start': '3921GB', - 'file system': '', + 'file system': 'linux-swap(v1)', 'flags': '', - 'type': 'linux-swap(v1)', + 'name': '', 'size': '79.3GB' } } @@ -245,23 +251,58 @@ def test_list__valid_unit_valid_cmd_output(self): 'end': '150MB', 'number': '1', 'start': '17.4kB', - 'file system': '', + 'file system': 'ext3', 'flags': 'boot', - 'type': 'ext3', + 'name': '', 'size': '150MB'}, '2': { 'end': '4000GB', 'number': '2', 'start': '3921GB', - 'file system': '', + 'file system': 'linux-swap(v1)', 'flags': '', - 'type': 'linux-swap(v1)', + 'name': '', 'size': '79.3GB' } } } self.assertEqual(output, expected) + def test_list__valid_unit_chs_valid_cmd_output(self): + with patch('salt.modules.parted._validate_device', MagicMock()): + self.cmdrun_stdout.return_value = self.parted_print_output('valid chs') + output = parted.list_('/dev/sda', unit='chs') + self.cmdrun_stdout.assert_called_once_with('parted -m -s /dev/sda unit chs print') + expected = { + 'info': { + 'logical sector': '512', + 'physical sector': '512', + 'interface': 'scsi', + 'model': 'AMCC 9650SE-24M DISK', + 'disk': '/dev/sda', + 'disk flags': '', + 'partition table': 'gpt', + 'size': '3133,0,2' + }, + 'partitions': { + '1': { + 'end': '2431,134,43', + 'number': '1', + 'start': '0,0,34', + 'file system': 'ext3', + 'flags': 'boot', + 'name': ''}, + '2': { + 'end': '2492,80,42', + 'number': '2', + 'start': '2431,134,44', + 'file system': 'linux-swap(v1)', + 'flags': '', + 'name': ''} + } + } + self.assertEqual(output, expected) + def test_list__valid_legacy_cmd_output(self): with patch('salt.modules.parted._validate_device', MagicMock()): self.cmdrun_stdout.return_value = self.parted_print_output('valid_legacy') @@ -282,17 +323,17 @@ def test_list__valid_legacy_cmd_output(self): 'end': '150MB', 'number': '1', 'start': '17.4kB', - 'file system': '', + 'file system': 'ext3', 'flags': 'boot', - 'type': 'ext3', + 'name': '', 'size': '150MB'}, '2': { 'end': '4000GB', 'number': '2', 'start': '3921GB', - 'file system': '', + 'file system': 'linux-swap(v1)', 'flags': '', - 'type': 'linux-swap(v1)', + 'name': '', 'size': '79.3GB' } } @@ -319,19 +360,36 @@ def test_list__valid_unit_valid_legacy_cmd_output(self): 'end': '150MB', 'number': '1', 'start': '17.4kB', - 'file system': '', + 'file system': 'ext3', 'flags': 'boot', - 'type': 'ext3', + 'name': '', 'size': '150MB'}, '2': { 'end': '4000GB', 'number': '2', 'start': '3921GB', - 'file system': '', + 'file system': 'linux-swap(v1)', 'flags': '', - 'type': 'linux-swap(v1)', + 'name': '', 'size': '79.3GB' } } } 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_systemd.py b/tests/unit/modules/test_systemd.py index 75c727bd49a..2552f026c15 100644 --- a/tests/unit/modules/test_systemd.py +++ b/tests/unit/modules/test_systemd.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/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')