A module for managing ZFS admin privileges (#19240)

* Create a module for managing ZFS admin privileges

* Actually support check mode in zfs_permissions

* Fix Python 2.4 invalid syntax

* Update added_version to 2.1

* Remove superflous sys.exit() and add an empty RETURN doc (empty because the module does not return anything other than status-related stuff)

* Update version_added since zfs_permissions was not accepted in 2.1.

* - Rename zfs_permissions to zfs_delegate_admin to more accurately reflect
  its purpose.
- Update with current Ansible module best practices.
- Stop checking whether changes need to be made (this was flawed since
  it was not able to detect whether, for example, changes would be made
  with `local=True` when the permissions already existed for
  `Local+Descendent`. Instead, just make the changes (`zfs allow` is
  idempotent) and compare the state before and after.
- Generally write things in a better way.

* Fix test errors

* Fix more tests.

* Catch exceptions when parsing `zfs allow` output. At least on one
version of ZFS (on a 2016 version of illumos) will have a single space
in the user or group column if the name can't be resolved by the
system's name services).
This commit is contained in:
Nate Coraor 2019-01-22 21:51:06 -05:00 committed by Dag Wieers
parent d2c739e9e7
commit 64b57908bc

View file

@ -0,0 +1,263 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2015, Nate Coraor <nate@coraor.org>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import absolute_import
__metaclass__ = type
ANSIBLE_METADATA = {'status': ['preview'],
'supported_by': 'community',
'metadata_version': '1.1'}
DOCUMENTATION = '''
---
module: zfs_delegate_admin
short_description: Manage ZFS delegated administration (user admin privileges)
description:
- Manages ZFS file system delegated administration permissions, which allow unprivileged users to perform ZFS
operations normally restricted to the superuser.
- See the "zfs allow" section of C(zfs(1M)) for detailed explanations of options. This module attempts to adhere to
the behavior of the command line tool as much as possible.
requirements:
- "A ZFS/OpenZFS implementation that supports delegation with `zfs allow`, including: Solaris >= 10, illumos (all
versions), FreeBSD >= 8.0R, ZFS on Linux >= 0.7.0."
version_added: "2.5"
options:
name:
description:
- File system or volume name e.g. C(rpool/myfs)
required: true
state:
description:
- Whether to allow (C(present)), or unallow (C(absent)) a permission. When set to C(present), at least one
"entity" param of I(users), I(groups), or I(everyone) are required. When set to C(absent), removes permissions
from the specified entities, or removes all permissions if no entity params are specified.
required: true
choices: [present, absent]
users:
description:
- List of users to whom permission(s) should be granted
groups:
description:
- List of groups to whom permission(s) should be granted
everyone:
description:
- Apply permissions to everyone.
default: false
type: bool
permissions:
description:
- The list of permission(s) to delegate (required if C(state) is C(present))
choices: ['allow','clone','create','destroy',...]
local:
description:
- Apply permissions to C(name) locally (C(zfs allow -l))
default: null
type: bool
descendents:
description:
- Apply permissions to C(name)'s descendents (C(zfs allow -d))
default: null
type: bool
recursive:
description:
- Unallow permissions recursively (ignored when C(state) is C(present))
default: false
type: bool
author: "Nate Coraor (@natefoo)"
'''
EXAMPLES = '''
# Grant `zfs allow` and `unallow` permission to the `adm` user with the default local+descendents scope
- zfs_delegate_admin: name=rpool/myfs users=adm permissions=allow,unallow
# Grant `zfs send` to everyone, plus the group `backup`
- zfs_delegate_admin: name=rpool/myvol groups=backup everyone=yes permissions=send
# Grant `zfs send,receive` to users `foo` and `bar` with local scope only
- zfs_delegate_admin: name=rpool/myfs users=foo,bar permissions=send,receive local=yes
# Revoke all permissions from everyone (permissions specifically assigned to users and groups remain)
- zfs_delegate_admin: name=rpool/myfs state=absent everyone=yes
'''
# This module does not return anything other than the standard
# changed/state/msg/stdout
RETURN = '''
'''
from itertools import product
from ansible.module_utils.basic import AnsibleModule
class ZfsDelegateAdmin(object):
def __init__(self, module):
self.module = module
self.name = module.params.get('name')
self.state = module.params.get('state')
self.users = module.params.get('users')
self.groups = module.params.get('groups')
self.everyone = module.params.get('everyone')
self.perms = module.params.get('permissions')
self.scope = None
self.changed = False
self.initial_perms = None
self.subcommand = 'allow'
self.recursive_opt = []
self.run_method = self.update
self.setup(module)
def setup(self, module):
""" Validate params and set up for run.
"""
if self.state == 'absent':
self.subcommand = 'unallow'
if module.params.get('recursive'):
self.recursive_opt = ['-r']
local = module.params.get('local')
descendents = module.params.get('descendents')
if (local and descendents) or (not local and not descendents):
self.scope = 'ld'
elif local:
self.scope = 'l'
elif descendents:
self.scope = 'd'
else:
self.module.fail_json(msg='Impossible value for local and descendents')
if not (self.users or self.groups or self.everyone):
if self.state == 'present':
self.module.fail_json(msg='One of `users`, `groups`, or `everyone` must be set')
elif self.state == 'absent':
self.run_method = self.clear
# ansible ensures the else cannot happen here
self.zfs_path = module.get_bin_path('zfs', True)
@property
def current_perms(self):
""" Parse the output of `zfs allow <name>` to retrieve current permissions.
"""
out = self.run_zfs_raw(subcommand='allow')
perms = {
'l': {'u': {}, 'g': {}, 'e': []},
'd': {'u': {}, 'g': {}, 'e': []},
'ld': {'u': {}, 'g': {}, 'e': []},
}
linemap = {
'Local permissions:': 'l',
'Descendent permissions:': 'd',
'Local+Descendent permissions:': 'ld',
}
scope = None
for line in out.splitlines():
scope = linemap.get(line, scope)
if not scope:
continue
try:
if line.startswith('\tuser ') or line.startswith('\tgroup '):
ent_type, ent, cur_perms = line.split()
perms[scope][ent_type[0]][ent] = cur_perms.split(',')
elif line.startswith('\teveryone '):
perms[scope]['e'] = line.split()[1].split(',')
except ValueError:
self.module.fail_json(msg="Cannot parse user/group permission output by `zfs allow`: '%s'" % line)
return perms
def run_zfs_raw(self, subcommand=None, args=None):
""" Run a raw zfs command, fail on error.
"""
cmd = [self.zfs_path, subcommand or self.subcommand] + (args or []) + [self.name]
rc, out, err = self.module.run_command(cmd)
if rc:
self.module.fail_json(msg='Command `%s` failed: %s' % (' '.join(cmd), err))
return out
def run_zfs(self, args):
""" Run zfs allow/unallow with appropriate options as per module arguments.
"""
args = self.recursive_opt + ['-' + self.scope] + args
if self.perms:
args.append(','.join(self.perms))
return self.run_zfs_raw(args=args)
def clear(self):
""" Called by run() to clear all permissions.
"""
changed = False
stdout = ''
for scope, ent_type in product(('ld', 'l', 'd'), ('u', 'g')):
for ent in self.initial_perms[scope][ent_type].keys():
stdout += self.run_zfs(['-%s' % ent_type, ent])
changed = True
for scope in ('ld', 'l', 'd'):
if self.initial_perms[scope]['e']:
stdout += self.run_zfs(['-e'])
changed = True
return (changed, stdout)
def update(self):
""" Update permissions as per module arguments.
"""
stdout = ''
for ent_type, entities in (('u', self.users), ('g', self.groups)):
if entities:
stdout += self.run_zfs(['-%s' % ent_type, ','.join(entities)])
if self.everyone:
stdout += self.run_zfs(['-e'])
return (self.initial_perms != self.current_perms, stdout)
def run(self):
""" Run an operation, return results for Ansible.
"""
exit_args = {'state': self.state}
self.initial_perms = self.current_perms
exit_args['changed'], stdout = self.run_method()
if exit_args['changed']:
exit_args['msg'] = 'ZFS delegated admin permissions updated'
exit_args['stdout'] = stdout
self.module.exit_json(**exit_args)
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(required=True),
state=dict(default='present', choices=['absent', 'present']),
users=dict(default=[], type='list'),
groups=dict(default=[], type='list'),
everyone=dict(default=False, type='bool'),
permissions=dict(default=[], type='list'),
local=dict(default=None, type='bool'),
descendents=dict(default=None, type='bool'),
recursive=dict(default=False, type='bool')
),
supports_check_mode=False,
required_if=[('state', 'present', ['permissions'])]
)
zfs_delegate_admin = ZfsDelegateAdmin(module)
zfs_delegate_admin.run()
if __name__ == '__main__':
main()