acl 6.14 KB
Newer Older
Brian Coca committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#!/usr/bin/python
# 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/>.

DOCUMENTATION = '''
---
module: acl
20 21
version_added: "1.4"
short_description: Sets and retrieves file ACL information.
Brian Coca committed
22
description:
23
     - Sets and retrieves file ACL information.
Brian Coca committed
24 25 26 27 28
options:
  name:
    required: true
    default: None
    description:
29
      - The full path of the file or object.
Brian Coca committed
30 31 32 33 34
    aliases: ['path']
  entry:
    required: false
    default: None
    description:
35 36
      - The acl to set or remove.  This must always be quoted in the form of '<type>:<qualifier>:<perms>'.  The qualifier may be empty for some types, but the type and perms are always requried. '-' can be used as placeholder when you do not care about permissions.

Brian Coca committed
37 38
  state:
    required: false
39 40
    default: query
    choices: [ 'query', 'present', 'absent' ]
Brian Coca committed
41
    description:
42
      - defines whether the ACL should be present or not.  The C(query) state gets the current acl C(present) without changing it, for use in 'register' operations.
Brian Coca committed
43 44 45 46 47
  follow:
    required: false
    default: yes
    choices: [ 'yes', 'no' ]
    description:
48
      - whether to follow symlinks on the path if a symlink is encountered.
49 50
author: Brian Coca
notes:
51
    - The "acl" module requires that acls are enabled on the target filesystem and that the setfacl and getfacl binaries are installed.
Brian Coca committed
52 53 54
'''

EXAMPLES = '''
55
# Grant user Joe read access to a file
56
- acl: name=/etc/foo.conf entry="user:joe:r" state=present
Brian Coca committed
57

58
# Removes the acl for Joe on a specific file
59
- acl: name=/etc/foo.conf entry="user:joe:-" state=absent
60 61 62 63

# Obtain the acl for a specific file
- acl: name=/etc/foo.conf
  register: acl_info
Brian Coca committed
64 65
'''

66 67 68 69 70 71
def get_acl(module,path,entry,follow):

    cmd = [ module.get_bin_path('getfacl', True) ]
    if not follow:
        cmd.append('-h')
    # prevents absolute path warnings and removes headers
72 73
    cmd.append('--omit-header')
    cmd.append('--absolute-names')
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
    cmd.append(path)

    return _run_acl(module,cmd)

def set_acl(module,path,entry,follow):

    cmd = [ module.get_bin_path('setfacl', True) ]
    if not follow:
        cmd.append('-h')
    cmd.append('-m "%s"' % entry)
    cmd.append(path)

    return _run_acl(module,cmd)

def rm_acl(module,path,entry,follow):

    cmd = [ module.get_bin_path('setfacl', True) ]
    if not follow:
        cmd.append('-h')
    entry = entry[0:entry.rfind(':')]
    cmd.append('-x "%s"' % entry)
    cmd.append(path)

    return _run_acl(module,cmd,False)

def _run_acl(module,cmd,check_rc=True):

101
    try:
102 103 104 105 106
        (rc, out, err) = module.run_command(' '.join(cmd), check_rc=check_rc)
    except Exception, e:
        module.fail_json(msg=e.strerror)

    return out.splitlines()
107

Brian Coca committed
108 109 110 111 112
def main():
    module = AnsibleModule(
        argument_spec = dict(
            name = dict(required=True,aliases=['path']),
            entry = dict(required=False, default=None),
113
            state = dict(required=False, default='query', choices=[ 'query', 'present', 'absent' ], type='str'),
Brian Coca committed
114 115 116 117
            follow = dict(required=False, type='bool', default=True),
        ),
        supports_check_mode=True,
    )
118

Brian Coca committed
119 120 121 122 123 124 125 126
    path = module.params.get('name')
    entry = module.params.get('entry')
    state = module.params.get('state')
    follow = module.params.get('follow')

    if not os.path.exists(path):
        module.fail_json(msg="path not found or not accessible!")

127 128 129 130 131 132
    if entry is None:
        if state in ['present','absent']:
            module.fail_json(msg="%s needs entry to be set" % state)
    else:
        if entry.count(":") != 2:
            module.fail_json(msg="Invalid entry: '%s', it requires 3 sections divided by ':'" % entry)
Brian Coca committed
133 134 135 136

    changed=False
    changes=0
    msg = ""
137
    currentacl = get_acl(module,path,entry,follow)
Brian Coca committed
138

139

Brian Coca committed
140
    if (state == 'present'):
141 142 143 144 145 146 147 148
        newe = entry.split(':')
        matched = False
        for oldentry in currentacl:
            diff = False
            olde = oldentry.split(':')
            if olde[0] == newe[0]:
                if newe[0] in ['user', 'group']:
                    if olde[1] == newe[1]:
Brian Coca committed
149
                        matched = True
150
                        if not olde[2] == newe[2]:
Brian Coca committed
151
                            diff = True
152 153 154 155 156
                else:
                    matched = True
                    if not olde[2] == newe[2]:
                        diff = True
            if diff:
Brian Coca committed
157
                changes=changes+1
158 159 160 161 162 163 164 165
                if not module.check_mode:
                    set_acl(module,path,entry,follow)
            if matched:
                break
        if not matched:
            changes=changes+1
            if not module.check_mode:
                set_acl(module,path,entry,follow)
Brian Coca committed
166 167
        msg="%s is present" % (entry)
    elif state == 'absent':
168 169 170 171 172 173
        rme = entry.split(':')
        for oldentry in currentacl:
            olde = oldentry.split(':')
            if olde[0] == rme[0]:
                if rme[0] in ['user', 'group']:
                    if olde[1] == rme[1]:
Brian Coca committed
174
                        changes=changes+1
175 176
                        if not module.check_mode:
                            rm_acl(module,path,entry,follow)
Brian Coca committed
177
                        break
178 179 180 181 182
                else:
                    changes=changes+1
                    if not module.check_mode:
                        rm_acl(module,path,entry,follow)
                    break
Brian Coca committed
183 184 185 186 187 188
        msg="%s is absent" % (entry)
    else:
        msg="current acl"

    if changes > 0:
        changed=True
189
        currentacl = get_acl(module,path,entry,follow)
Brian Coca committed
190 191

    msg="%s. %d entries changed" % (msg,changes)
192
    module.exit_json(changed=changed, msg=msg, acl=currentacl)
Brian Coca committed
193 194 195 196 197

# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>

main()