1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2015, Nate Coraor <nate@coraor.org>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10ANSIBLE_METADATA = {'status': ['preview'],
11                    'supported_by': 'community',
12                    'metadata_version': '1.1'}
13
14DOCUMENTATION = r'''
15---
16module: zfs_delegate_admin
17short_description: Manage ZFS delegated administration (user admin privileges)
18description:
19  - Manages ZFS file system delegated administration permissions, which allow unprivileged users to perform ZFS
20    operations normally restricted to the superuser.
21  - See the C(zfs allow) section of C(zfs(1M)) for detailed explanations of options.
22  - This module attempts to adhere to the behavior of the command line tool as much as possible.
23requirements:
24  - "A ZFS/OpenZFS implementation that supports delegation with `zfs allow`, including: Solaris >= 10, illumos (all
25    versions), FreeBSD >= 8.0R, ZFS on Linux >= 0.7.0."
26version_added: '2.8'
27options:
28  name:
29    description:
30      - File system or volume name e.g. C(rpool/myfs).
31    required: true
32    type: str
33  state:
34    description:
35      - Whether to allow (C(present)), or unallow (C(absent)) a permission.
36      - When set to C(present), at least one "entity" param of I(users), I(groups), or I(everyone) are required.
37      - When set to C(absent), removes permissions from the specified entities, or removes all permissions if no entity params are specified.
38    required: true
39    choices: [ absent, present ]
40    default: present
41  users:
42    description:
43      - List of users to whom permission(s) should be granted.
44    type: list
45  groups:
46    description:
47      - List of groups to whom permission(s) should be granted.
48    type: list
49  everyone:
50    description:
51      - Apply permissions to everyone.
52    type: bool
53    default: no
54  permissions:
55    description:
56      - The list of permission(s) to delegate (required if C(state) is C(present)).
57    type: list
58    choices: [ allow, clone, create, destroy, diff, hold, mount, promote, readonly, receive, release, rename, rollback, send, share, snapshot, unallow ]
59  local:
60    description:
61      - Apply permissions to C(name) locally (C(zfs allow -l)).
62    type: bool
63  descendents:
64    description:
65      - Apply permissions to C(name)'s descendents (C(zfs allow -d)).
66    type: bool
67  recursive:
68    description:
69      - Unallow permissions recursively (ignored when C(state) is C(present)).
70    type: bool
71    default: no
72author:
73- Nate Coraor (@natefoo)
74'''
75
76EXAMPLES = r'''
77- name: Grant `zfs allow` and `unallow` permission to the `adm` user with the default local+descendents scope
78  zfs_delegate_admin:
79    name: rpool/myfs
80    users: adm
81    permissions: allow,unallow
82
83- name: Grant `zfs send` to everyone, plus the group `backup`
84  zfs_delegate_admin:
85    name: rpool/myvol
86    groups: backup
87    everyone: yes
88    permissions: send
89
90- name: Grant `zfs send,receive` to users `foo` and `bar` with local scope only
91  zfs_delegate_admin:
92    name: rpool/myfs
93    users: foo,bar
94    permissions: send,receive
95    local: yes
96
97- name: Revoke all permissions from everyone (permissions specifically assigned to users and groups remain)
98- zfs_delegate_admin:
99    name: rpool/myfs
100    everyone: yes
101    state: absent
102'''
103
104# This module does not return anything other than the standard
105# changed/state/msg/stdout
106RETURN = '''
107'''
108
109from itertools import product
110
111from ansible.module_utils.basic import AnsibleModule
112
113
114class ZfsDelegateAdmin(object):
115    def __init__(self, module):
116        self.module = module
117        self.name = module.params.get('name')
118        self.state = module.params.get('state')
119        self.users = module.params.get('users')
120        self.groups = module.params.get('groups')
121        self.everyone = module.params.get('everyone')
122        self.perms = module.params.get('permissions')
123        self.scope = None
124        self.changed = False
125        self.initial_perms = None
126        self.subcommand = 'allow'
127        self.recursive_opt = []
128        self.run_method = self.update
129
130        self.setup(module)
131
132    def setup(self, module):
133        """ Validate params and set up for run.
134        """
135        if self.state == 'absent':
136            self.subcommand = 'unallow'
137            if module.params.get('recursive'):
138                self.recursive_opt = ['-r']
139
140        local = module.params.get('local')
141        descendents = module.params.get('descendents')
142        if (local and descendents) or (not local and not descendents):
143            self.scope = 'ld'
144        elif local:
145            self.scope = 'l'
146        elif descendents:
147            self.scope = 'd'
148        else:
149            self.module.fail_json(msg='Impossible value for local and descendents')
150
151        if not (self.users or self.groups or self.everyone):
152            if self.state == 'present':
153                self.module.fail_json(msg='One of `users`, `groups`, or `everyone` must be set')
154            elif self.state == 'absent':
155                self.run_method = self.clear
156            # ansible ensures the else cannot happen here
157
158        self.zfs_path = module.get_bin_path('zfs', True)
159
160    @property
161    def current_perms(self):
162        """ Parse the output of `zfs allow <name>` to retrieve current permissions.
163        """
164        out = self.run_zfs_raw(subcommand='allow')
165        perms = {
166            'l': {'u': {}, 'g': {}, 'e': []},
167            'd': {'u': {}, 'g': {}, 'e': []},
168            'ld': {'u': {}, 'g': {}, 'e': []},
169        }
170        linemap = {
171            'Local permissions:': 'l',
172            'Descendent permissions:': 'd',
173            'Local+Descendent permissions:': 'ld',
174        }
175        scope = None
176        for line in out.splitlines():
177            scope = linemap.get(line, scope)
178            if not scope:
179                continue
180            try:
181                if line.startswith('\tuser ') or line.startswith('\tgroup '):
182                    ent_type, ent, cur_perms = line.split()
183                    perms[scope][ent_type[0]][ent] = cur_perms.split(',')
184                elif line.startswith('\teveryone '):
185                    perms[scope]['e'] = line.split()[1].split(',')
186            except ValueError:
187                self.module.fail_json(msg="Cannot parse user/group permission output by `zfs allow`: '%s'" % line)
188        return perms
189
190    def run_zfs_raw(self, subcommand=None, args=None):
191        """ Run a raw zfs command, fail on error.
192        """
193        cmd = [self.zfs_path, subcommand or self.subcommand] + (args or []) + [self.name]
194        rc, out, err = self.module.run_command(cmd)
195        if rc:
196            self.module.fail_json(msg='Command `%s` failed: %s' % (' '.join(cmd), err))
197        return out
198
199    def run_zfs(self, args):
200        """ Run zfs allow/unallow with appropriate options as per module arguments.
201        """
202        args = self.recursive_opt + ['-' + self.scope] + args
203        if self.perms:
204            args.append(','.join(self.perms))
205        return self.run_zfs_raw(args=args)
206
207    def clear(self):
208        """ Called by run() to clear all permissions.
209        """
210        changed = False
211        stdout = ''
212        for scope, ent_type in product(('ld', 'l', 'd'), ('u', 'g')):
213            for ent in self.initial_perms[scope][ent_type].keys():
214                stdout += self.run_zfs(['-%s' % ent_type, ent])
215                changed = True
216        for scope in ('ld', 'l', 'd'):
217            if self.initial_perms[scope]['e']:
218                stdout += self.run_zfs(['-e'])
219                changed = True
220        return (changed, stdout)
221
222    def update(self):
223        """ Update permissions as per module arguments.
224        """
225        stdout = ''
226        for ent_type, entities in (('u', self.users), ('g', self.groups)):
227            if entities:
228                stdout += self.run_zfs(['-%s' % ent_type, ','.join(entities)])
229        if self.everyone:
230            stdout += self.run_zfs(['-e'])
231        return (self.initial_perms != self.current_perms, stdout)
232
233    def run(self):
234        """ Run an operation, return results for Ansible.
235        """
236        exit_args = {'state': self.state}
237        self.initial_perms = self.current_perms
238        exit_args['changed'], stdout = self.run_method()
239        if exit_args['changed']:
240            exit_args['msg'] = 'ZFS delegated admin permissions updated'
241            exit_args['stdout'] = stdout
242        self.module.exit_json(**exit_args)
243
244
245def main():
246    module = AnsibleModule(
247        argument_spec=dict(
248            name=dict(type='str', required=True),
249            state=dict(type='str', default='present', choices=['absent', 'present']),
250            users=dict(type='list'),
251            groups=dict(type='list'),
252            everyone=dict(type='bool', default=False),
253            permissions=dict(type='list',
254                             choices=['allow', 'clone', 'create', 'destroy', 'diff', 'hold', 'mount', 'promote',
255                                      'readonly', 'receive', 'release', 'rename', 'rollback', 'send', 'share',
256                                      'snapshot', 'unallow']),
257            local=dict(type='bool'),
258            descendents=dict(type='bool'),
259            recursive=dict(type='bool', default=False),
260        ),
261        supports_check_mode=False,
262        required_if=[('state', 'present', ['permissions'])],
263    )
264    zfs_delegate_admin = ZfsDelegateAdmin(module)
265    zfs_delegate_admin.run()
266
267
268if __name__ == '__main__':
269    main()
270