diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py index 177396d4d7..6eea60d825 100644 --- a/lib/ansible/module_utils/basic.py +++ b/lib/ansible/module_utils/basic.py @@ -34,6 +34,29 @@ BOOLEANS = BOOLEANS_TRUE + BOOLEANS_FALSE SIZE_RANGES = { 'Y': 1<<80, 'Z': 1<<70, 'E': 1<<60, 'P': 1<<50, 'T': 1<<40, 'G': 1<<30, 'M': 1<<20, 'K': 1<<10, 'B': 1 } +FILE_ATTRIBUTES = { + 'A': 'noatime', + 'a': 'append', + 'c': 'compressed', + 'C': 'nocow', + 'd': 'nodump', + 'D': 'dirsync', + 'e': 'extents', + 'E': 'encrypted', + 'h': 'blocksize', + 'i': 'immutable', + 'I': 'indexed', + 'j': 'journalled', + 'N': 'inline', + 's': 'zero', + 'S': 'synchronous', + 't': 'notail', + 'T': 'blockroot', + 'u': 'undelete', + 'X': 'compressedraw', + 'Z': 'compresseddirty', +} + # ansible modules can be written in any language. To simplify # development of Python modules, the functions available here can # be used to do many common tasks @@ -203,6 +226,7 @@ FILE_COMMON_ARGUMENTS=dict( delimiter = dict(), # used by assemble directory_mode = dict(), # used by copy unsafe_writes = dict(type='bool'), # should be available to any module using atomic_move + attributes = dict(aliases=['attr']), ) PASSWD_ARG_RE = re.compile(r'^[-]{0,2}pass[-]?(word|wd)?') @@ -618,6 +642,19 @@ def _lenient_lowercase(lst): lowered.append(value) return lowered +def format_attributes(attributes): + attribute_list = [] + for attr in attributes: + if attr in FILE_ATTRIBUTES: + attribute_list.append(FILE_ATTRIBUTES[attr]) + return attribute_list + +def get_flags_from_attributes(attributes): + flags = [] + for key,attr in FILE_ATTRIBUTES.iteritems(): + if attr in attributes: + flags.append(key) + return ''.join(flags) class AnsibleFallbackNotFound(Exception): pass @@ -759,10 +796,11 @@ class AnsibleModule(object): if i is not None and secontext[i] == '_default': secontext[i] = default_secontext[i] + attributes = params.get('attributes', None) return dict( path=path, mode=mode, owner=owner, group=group, seuser=seuser, serole=serole, setype=setype, - selevel=selevel, secontext=secontext, + selevel=selevel, secontext=secontext, attributes=attributes, ) @@ -1058,6 +1096,56 @@ class AnsibleModule(object): changed = True return changed + def set_attributes_if_different(self, path, attributes, changed, diff=None): + + if attributes is None: + return changed + + b_path = to_bytes(path, errors='surrogate_or_strict') + b_path = os.path.expanduser(os.path.expandvars(b_path)) + existing = self.get_file_attributes(b_path) + + if existing.get('attr_flags','') != attributes: + attrcmd = self.get_bin_path('chattr') + if attrcmd: + attrcmd = [attrcmd, '=%s' % attributes, b_path] + changed = True + + if diff is not None: + if 'before' not in diff: + diff['before'] = {} + diff['before']['attributes'] = existing.get('attr_flags') + if 'after' not in diff: + diff['after'] = {} + diff['after']['attributes'] = attributes + + if not self.check_mode: + try: + rc, out, err = self.run_command(attrcmd) + if rc != 0 or err: + raise Exception("Error while setting attributes: %s" % (out + err)) + except: + e = get_exception() + self.fail_json(path=path, msg='chattr failed', details=str(e)) + return changed + + def get_file_attributes(self, path): + output = {} + attrcmd = self.get_bin_path('lsattr', False) + if attrcmd: + attrcmd = [attrcmd, '-vd', path] + try: + rc, out, err = self.run_command(attrcmd) + if rc == 0: + res = out.split(' ')[0:2] + output['attr_flags'] = res[1].replace('-','').strip() + output['version'] = res[0].strip() + output['attributes'] = format_attributes(output['attr_flags']) + except: + pass + return output + + def _symbolic_mode_to_octal(self, path_stat, symbolic_mode): new_mode = stat.S_IMODE(path_stat.st_mode) @@ -1167,6 +1255,9 @@ class AnsibleModule(object): changed = self.set_mode_if_different( file_args['path'], file_args['mode'], changed, diff ) + changed = self.set_attributes_if_different( + file_args['path'], file_args['attributes'], changed, diff + ) return changed def set_directory_attributes_if_different(self, file_args, changed, diff=None): diff --git a/lib/ansible/utils/module_docs_fragments/files.py b/lib/ansible/utils/module_docs_fragments/files.py index 9bc40a0929..fc1ca70892 100644 --- a/lib/ansible/utils/module_docs_fragments/files.py +++ b/lib/ansible/utils/module_docs_fragments/files.py @@ -70,4 +70,11 @@ options: required: false default: false version_added: "2.2" + attributes: + description: + - Attributes of the file or directory should be. To get supported flags look at the man page for I(chattr) on the taget system. + required: false + default: None + aliases: ['attr'] + version_added: "2.3" """ diff --git a/test/units/module_utils/test_basic.py b/test/units/module_utils/test_basic.py index 6da86db2a1..a03e3401f1 100644 --- a/test/units/module_utils/test_basic.py +++ b/test/units/module_utils/test_basic.py @@ -422,6 +422,7 @@ class TestModuleUtilsBasic(ModuleTestCase): final_params.update(dict( path = '/path/to/real_file', secontext=['unconfined_u', 'object_r', 'default_t', 's0'], + attributes=None, )) # with the proper params specified, the returned dictionary should represent