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