From 46dea95c119e590c73047c20399b81a516375c20 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sun, 21 Jul 2013 11:18:31 -0400 Subject: [PATCH 1/6] initial draft acl module Signed-off-by: Brian Coca --- files/acl | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 files/acl diff --git a/files/acl b/files/acl new file mode 100644 index 00000000000..6ba1047b7e2 --- /dev/null +++ b/files/acl @@ -0,0 +1,156 @@ +#!/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 . + +DOCUMENTATION = ''' +--- +module: acl +version_added: "1.3" +short_description: set and retrieve file acl +description: + - Sets and retrivies acl for a file +options: + name: + required: true + default: None + description: + - The full path of the file/object to get the facts of + aliases: ['path'] + entry: + required: false + default: None + description: + - The acl to set/remove. MUST always quote! In form of '::', qualifier may be empty for some types but type and perms are always requried. '-' can be used as placeholder when you don't care about permissions. + state: + required: false + default: get + choices: [ 'get', 'present', 'absent' ] + description: + - defines which operation you want to do. C(get) get the current acl C(present) sets/changes the acl, requires entry field C(absent) deletes the acl, requires entry field + follow: + required: false + default: yes + choices: [ 'yes', 'no' ] + description: + - if yes, dereferences symlinks and sets/gets attributes on symlink target, otherwise acts on symlink itself. +author: Brian Coca +notes: + - The "acl" module requires that acl is enabled on the target filesystem and that the setfacl and getfacl binaries are installed. +''' + +EXAMPLES = ''' +# Obtain the acl of /etc/foo.conf +- acl: name=/etc/foo.conf + +# Grants joe read access to foo +- acl: name=/etc/foo.conf entry="u:joe:r" state=present + +# Removes the acl for joe +- acl: name=/etc/foo.conf entry="u:joe" state=absent +''' + +try: + import posix1e +except: + module.fail_json(msg="Could not import required module pylibacl (posix1e)") + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True,aliases=['path']), + entry = dict(required=False, default=None), + state = dict(required=False, default='get', choices=[ 'get', 'present', 'absent' ], type='str'), + follow = dict(required=False, type='bool', default=True), + ), + supports_check_mode=True, + ) + 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!") + + if entry is None and state in ['present','absent']: + module.fail_json(msg="%s needs entry to be set" % state) + + if entry.count(":") != 3: + module.fail_json(msg="Invalid entry: '%s', it requires 3 sections divided by ':'" % entry) + + changed=False + changes=0 + msg = "" + currentacl = posix1e.ACL(file=path) + newacl = currentacl + res = currentacl + + if (state == 'present'): + for newe in posix1e.ACL(text=entry): + matched = False + for olde in currentacl: + diff = False + if olde.tag_type == newe.tag_type: + if newe.tag_type in [ posix1e.ACL_GROUP, posix1e.ACL_USER ]: + if olde.qualifier == newe.qualifier: + matched = True + if not str(olde.permset) == str(newe.permset): + diff = True + else: + matched = True + if not str(olde.permset) == str(newe.permset): + diff = True + if diff: + newacl.delete_entry(olde) + newacl.append(newe) + changes=changes+1 + if matched: + break + if not matched: + newacl.append(newe) + changes=changes+1 + msg="%s is present" % (entry) + elif state == 'absent': + for rme in posix1e.ACL(text=entry): + for olde in currentacl: + if olde.tag_type == rme.tag_type: + if rme.tag_type in [ posix1e.ACL_GROUP, posix1e.ACL_USER ]: + if olde.qualifier == rme.qualifier: + newacl.delete_entry(olde) + changes=changes+1 + break + else: + newacl.delete_entry(olde) + changes=changes+1 + break + msg="%s is absent" % (entry) + else: + msg="current acl" + + if changes > 0: + if not newacl.valid(): + module.fail_json("Invalid acl constructed: %s" % newacl.to_any_text()) + if not module.check_mode: + newacl.applyto(path) + changed=True + res=newacl + + msg="%s. %d entries changed" % (msg,changes) + module.exit_json(changed=changed, msg=msg, acl=res.to_any_text().split()) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From 9bb2e6a63e397932caf27c7d3eeb14ff11974d36 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sun, 21 Jul 2013 11:32:56 -0400 Subject: [PATCH 2/6] corrected absent example Signed-off-by: Brian Coca --- files/acl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/acl b/files/acl index 6ba1047b7e2..a9a46bbb029 100644 --- a/files/acl +++ b/files/acl @@ -58,7 +58,7 @@ EXAMPLES = ''' - acl: name=/etc/foo.conf entry="u:joe:r" state=present # Removes the acl for joe -- acl: name=/etc/foo.conf entry="u:joe" state=absent +- acl: name=/etc/foo.conf entry="u:joe:-" state=absent ''' try: From a2226228d93d14a578737477f7139e08043344a6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 25 Jul 2013 21:51:29 -0400 Subject: [PATCH 3/6] fixed error on detecting missing requirements Signed-off-by: Brian Coca --- files/acl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/files/acl b/files/acl index a9a46bbb029..a95a032c93f 100644 --- a/files/acl +++ b/files/acl @@ -60,11 +60,11 @@ EXAMPLES = ''' # Removes the acl for joe - acl: name=/etc/foo.conf entry="u:joe:-" state=absent ''' - +NO_PYLIBACL=False try: import posix1e except: - module.fail_json(msg="Could not import required module pylibacl (posix1e)") + NO_PYLIBACL=True def main(): module = AnsibleModule( @@ -76,6 +76,10 @@ def main(): ), supports_check_mode=True, ) + + if NO_PYLIBACL: + module.fail_json(msg="Could not import required module pylibacl (posix1e)") + path = module.params.get('name') entry = module.params.get('entry') state = module.params.get('state') From 73ffb0c0202e239e7216737cbc9adc31fa6a3864 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 22 Aug 2013 23:35:24 -0400 Subject: [PATCH 4/6] - fixed typos and errors from feedback - now makes sure a proper mask is added - now captures I/O error produced when group, user or permissions are invalid Signed-off-by: Brian Coca --- files/acl | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/files/acl b/files/acl index a95a032c93f..57dbb838ae1 100644 --- a/files/acl +++ b/files/acl @@ -47,7 +47,7 @@ options: - if yes, dereferences symlinks and sets/gets attributes on symlink target, otherwise acts on symlink itself. author: Brian Coca notes: - - The "acl" module requires that acl is enabled on the target filesystem and that the setfacl and getfacl binaries are installed. + - The "acl" module requires the posix1e module on the target machine and that acl is enabled on the target filesystem. ''' EXAMPLES = ''' @@ -66,6 +66,12 @@ try: except: NO_PYLIBACL=True +def gen_acl(module,entry): + try: + return posix1e.ACL(text=entry) + except IOError, e: + module.fail_json(msg="Invalid entry: '%s', check that user/groups exist and permissions are correct" % entry) + def main(): module = AnsibleModule( argument_spec = dict( @@ -91,7 +97,7 @@ def main(): if entry is None and state in ['present','absent']: module.fail_json(msg="%s needs entry to be set" % state) - if entry.count(":") != 3: + if entry.count(":") != 2: module.fail_json(msg="Invalid entry: '%s', it requires 3 sections divided by ':'" % entry) changed=False @@ -101,8 +107,9 @@ def main(): newacl = currentacl res = currentacl + if (state == 'present'): - for newe in posix1e.ACL(text=entry): + for newe in gen_acl(module, entry): matched = False for olde in currentacl: diff = False @@ -127,7 +134,7 @@ def main(): changes=changes+1 msg="%s is present" % (entry) elif state == 'absent': - for rme in posix1e.ACL(text=entry): + for rme in gen_acl(module, entry): for olde in currentacl: if olde.tag_type == rme.tag_type: if rme.tag_type in [ posix1e.ACL_GROUP, posix1e.ACL_USER ]: @@ -144,8 +151,9 @@ def main(): msg="current acl" if changes > 0: + newacl.calc_mask() if not newacl.valid(): - module.fail_json("Invalid acl constructed: %s" % newacl.to_any_text()) + module.fail_json(msg="Invalid acl constructed: %s" % newacl.to_any_text()) if not module.check_mode: newacl.applyto(path) changed=True From 1ddcc9574bffdc193576d09c6fa1f36b4393c673 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 10 Sep 2013 23:13:36 -0400 Subject: [PATCH 5/6] now w/o python module dependencies Signed-off-by: Brian Coca --- files/acl | 157 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 93 insertions(+), 64 deletions(-) diff --git a/files/acl b/files/acl index 57dbb838ae1..458a49dbd6f 100644 --- a/files/acl +++ b/files/acl @@ -35,10 +35,10 @@ options: - The acl to set/remove. MUST always quote! In form of '::', qualifier may be empty for some types but type and perms are always requried. '-' can be used as placeholder when you don't care about permissions. state: required: false - default: get - choices: [ 'get', 'present', 'absent' ] + default: query + choices: [ 'query', 'present', 'absent' ] description: - - defines which operation you want to do. C(get) get the current acl C(present) sets/changes the acl, requires entry field C(absent) deletes the acl, requires entry field + - defines which operation you want to do. C(query) get the current acl C(present) sets/changes the acl, requires permissions field C(absent) deletes the acl, requires permissions field follow: required: false default: yes @@ -47,7 +47,10 @@ options: - if yes, dereferences symlinks and sets/gets attributes on symlink target, otherwise acts on symlink itself. author: Brian Coca notes: - - The "acl" module requires the posix1e module on the target machine and that acl is enabled on the target filesystem. + - The "acl" module requires that acl is enabled on the target filesystem and that the setfacl and getfacl binaries are installed. +author: Brian Coca +notes: + - The "acl" module requires the acl command line utilities be installed on the target machine and that acl is enabled on the target filesystem. ''' EXAMPLES = ''' @@ -55,37 +58,64 @@ EXAMPLES = ''' - acl: name=/etc/foo.conf # Grants joe read access to foo -- acl: name=/etc/foo.conf entry="u:joe:r" state=present +- acl: name=/etc/foo.conf entry="user:joe:r" state=present # Removes the acl for joe -- acl: name=/etc/foo.conf entry="u:joe:-" state=absent +- acl: name=/etc/foo.conf entry="user:joe:-" state=absent ''' -NO_PYLIBACL=False -try: - import posix1e -except: - NO_PYLIBACL=True -def gen_acl(module,entry): +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 + cmd.append('-cp') + 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): + try: - return posix1e.ACL(text=entry) - except IOError, e: - module.fail_json(msg="Invalid entry: '%s', check that user/groups exist and permissions are correct" % entry) + (rc, out, err) = module.run_command(' '.join(cmd), check_rc=check_rc) + except Exception, e: + module.fail_json(msg=e.strerror) + + return out.splitlines() def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True,aliases=['path']), entry = dict(required=False, default=None), - state = dict(required=False, default='get', choices=[ 'get', 'present', 'absent' ], type='str'), + state = dict(required=False, default='query', choices=[ 'query', 'present', 'absent' ], type='str'), follow = dict(required=False, type='bool', default=True), ), supports_check_mode=True, ) - if NO_PYLIBACL: - module.fail_json(msg="Could not import required module pylibacl (posix1e)") - path = module.params.get('name') entry = module.params.get('entry') state = module.params.get('state') @@ -94,73 +124,72 @@ def main(): if not os.path.exists(path): module.fail_json(msg="path not found or not accessible!") - if entry is None and state in ['present','absent']: - module.fail_json(msg="%s needs entry to be set" % state) - - if entry.count(":") != 2: - module.fail_json(msg="Invalid entry: '%s', it requires 3 sections divided by ':'" % entry) + 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) changed=False changes=0 msg = "" - currentacl = posix1e.ACL(file=path) - newacl = currentacl - res = currentacl + currentacl = get_acl(module,path,entry,follow) if (state == 'present'): - for newe in gen_acl(module, entry): - matched = False - for olde in currentacl: - diff = False - if olde.tag_type == newe.tag_type: - if newe.tag_type in [ posix1e.ACL_GROUP, posix1e.ACL_USER ]: - if olde.qualifier == newe.qualifier: - matched = True - if not str(olde.permset) == str(newe.permset): - diff = True - else: + 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]: matched = True - if not str(olde.permset) == str(newe.permset): + if not olde[2] == newe[2]: diff = True - if diff: - newacl.delete_entry(olde) - newacl.append(newe) - changes=changes+1 - if matched: - break - if not matched: - newacl.append(newe) + else: + matched = True + if not olde[2] == newe[2]: + diff = True + if diff: changes=changes+1 + 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) msg="%s is present" % (entry) elif state == 'absent': - for rme in gen_acl(module, entry): - for olde in currentacl: - if olde.tag_type == rme.tag_type: - if rme.tag_type in [ posix1e.ACL_GROUP, posix1e.ACL_USER ]: - if olde.qualifier == rme.qualifier: - newacl.delete_entry(olde) - changes=changes+1 - break - else: - newacl.delete_entry(olde) + 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]: changes=changes+1 + if not module.check_mode: + rm_acl(module,path,entry,follow) break + else: + changes=changes+1 + if not module.check_mode: + rm_acl(module,path,entry,follow) + break msg="%s is absent" % (entry) else: msg="current acl" if changes > 0: - newacl.calc_mask() - if not newacl.valid(): - module.fail_json(msg="Invalid acl constructed: %s" % newacl.to_any_text()) - if not module.check_mode: - newacl.applyto(path) changed=True - res=newacl + currentacl = get_acl(module,path,entry,follow) msg="%s. %d entries changed" % (msg,changes) - module.exit_json(changed=changed, msg=msg, acl=res.to_any_text().split()) + module.exit_json(changed=changed, msg=msg, acl=currentacl) # this is magic, see lib/ansible/module_common.py #<> From 6db8c642d53ea2bc8b6d64e2fbeb2b7c4eccae6c Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 14 Oct 2013 10:48:30 -0400 Subject: [PATCH 6/6] added long names to support older version as per feedback Signed-off-by: Brian Coca --- files/acl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/files/acl b/files/acl index 458a49dbd6f..1b50d21c58f 100644 --- a/files/acl +++ b/files/acl @@ -70,7 +70,8 @@ def get_acl(module,path,entry,follow): if not follow: cmd.append('-h') # prevents absolute path warnings and removes headers - cmd.append('-cp') + cmd.append('--omit-header') + cmd.append('--absolute-names') cmd.append(path) return _run_acl(module,cmd)