1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2017, Ansible Project
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
10DOCUMENTATION = '''
11---
12module: xattr
13short_description: Manage user defined extended attributes
14description:
15    - Manages filesystem user defined extended attributes.
16    - Requires that extended attributes are enabled on the target filesystem
17      and that the setfattr/getfattr utilities are present.
18options:
19  path:
20    description:
21      - The full path of the file/object to get the facts of.
22      - Before 2.3 this option was only usable as I(name).
23    type: path
24    required: true
25    aliases: [ name ]
26  namespace:
27    description:
28      - Namespace of the named name/key.
29    type: str
30    default: user
31  key:
32    description:
33      - The name of a specific Extended attribute key to set/retrieve.
34    type: str
35  value:
36    description:
37      - The value to set the named name/key to, it automatically sets the C(state) to 'set'.
38    type: str
39  state:
40    description:
41      - defines which state you want to do.
42        C(read) retrieves the current value for a C(key) (default)
43        C(present) sets C(name) to C(value), default if value is set
44        C(all) dumps all data
45        C(keys) retrieves all keys
46        C(absent) deletes the key
47    type: str
48    choices: [ absent, all, keys, present, read ]
49    default: read
50  follow:
51    description:
52      - If C(yes), dereferences symlinks and sets/gets attributes on symlink target,
53        otherwise acts on symlink itself.
54    type: bool
55    default: yes
56notes:
57  - As of Ansible 2.3, the I(name) option has been changed to I(path) as default, but I(name) still works as well.
58author:
59- Brian Coca (@bcoca)
60'''
61
62EXAMPLES = '''
63- name: Obtain the extended attributes  of /etc/foo.conf
64  community.general.xattr:
65    path: /etc/foo.conf
66
67- name: Set the key 'user.foo' to value 'bar'
68  community.general.xattr:
69    path: /etc/foo.conf
70    key: foo
71    value: bar
72
73- name: Set the key 'trusted.glusterfs.volume-id' to value '0x817b94343f164f199e5b573b4ea1f914'
74  community.general.xattr:
75    path: /mnt/bricks/brick1
76    namespace: trusted
77    key: glusterfs.volume-id
78    value: "0x817b94343f164f199e5b573b4ea1f914"
79
80- name: Remove the key 'user.foo'
81  community.general.xattr:
82    path: /etc/foo.conf
83    key: foo
84    state: absent
85
86- name: Remove the key 'trusted.glusterfs.volume-id'
87  community.general.xattr:
88    path: /mnt/bricks/brick1
89    namespace: trusted
90    key: glusterfs.volume-id
91    state: absent
92'''
93
94import os
95
96# import module snippets
97from ansible.module_utils.basic import AnsibleModule
98from ansible.module_utils.common.text.converters import to_native
99
100
101def get_xattr_keys(module, path, follow):
102    cmd = [module.get_bin_path('getfattr', True), '--absolute-names']
103
104    if not follow:
105        cmd.append('-h')
106    cmd.append(path)
107
108    return _run_xattr(module, cmd)
109
110
111def get_xattr(module, path, key, follow):
112    cmd = [module.get_bin_path('getfattr', True), '--absolute-names']
113
114    if not follow:
115        cmd.append('-h')
116    if key is None:
117        cmd.append('-d')
118    else:
119        cmd.append('-n %s' % key)
120    cmd.append(path)
121
122    return _run_xattr(module, cmd, False)
123
124
125def set_xattr(module, path, key, value, follow):
126
127    cmd = [module.get_bin_path('setfattr', True)]
128    if not follow:
129        cmd.append('-h')
130    cmd.append('-n %s' % key)
131    cmd.append('-v %s' % value)
132    cmd.append(path)
133
134    return _run_xattr(module, cmd)
135
136
137def rm_xattr(module, path, key, follow):
138
139    cmd = [module.get_bin_path('setfattr', True)]
140    if not follow:
141        cmd.append('-h')
142    cmd.append('-x %s' % key)
143    cmd.append(path)
144
145    return _run_xattr(module, cmd, False)
146
147
148def _run_xattr(module, cmd, check_rc=True):
149
150    try:
151        (rc, out, err) = module.run_command(' '.join(cmd), check_rc=check_rc)
152    except Exception as e:
153        module.fail_json(msg="%s!" % to_native(e))
154
155    # result = {'raw': out}
156    result = {}
157    for line in out.splitlines():
158        if line.startswith('#') or line == '':
159            pass
160        elif '=' in line:
161            (key, val) = line.split('=')
162            result[key] = val.strip('"')
163        else:
164            result[line] = ''
165    return result
166
167
168def main():
169    module = AnsibleModule(
170        argument_spec=dict(
171            path=dict(type='path', required=True, aliases=['name']),
172            namespace=dict(type='str', default='user'),
173            key=dict(type='str', no_log=False),
174            value=dict(type='str'),
175            state=dict(type='str', default='read', choices=['absent', 'all', 'keys', 'present', 'read']),
176            follow=dict(type='bool', default=True),
177        ),
178        supports_check_mode=True,
179    )
180    path = module.params.get('path')
181    namespace = module.params.get('namespace')
182    key = module.params.get('key')
183    value = module.params.get('value')
184    state = module.params.get('state')
185    follow = module.params.get('follow')
186
187    if not os.path.exists(path):
188        module.fail_json(msg="path not found or not accessible!")
189
190    changed = False
191    msg = ""
192    res = {}
193
194    if key is None and state in ['absent', 'present']:
195        module.fail_json(msg="%s needs a key parameter" % state)
196
197    # Prepend the key with the namespace if defined
198    if (
199            key is not None and
200            namespace is not None and
201            len(namespace) > 0 and
202            not (namespace == 'user' and key.startswith('user.'))):
203        key = '%s.%s' % (namespace, key)
204
205    if (state == 'present' or value is not None):
206        current = get_xattr(module, path, key, follow)
207        if current is None or key not in current or value != current[key]:
208            if not module.check_mode:
209                res = set_xattr(module, path, key, value, follow)
210            changed = True
211        res = current
212        msg = "%s set to %s" % (key, value)
213    elif state == 'absent':
214        current = get_xattr(module, path, key, follow)
215        if current is not None and key in current:
216            if not module.check_mode:
217                res = rm_xattr(module, path, key, follow)
218            changed = True
219        res = current
220        msg = "%s removed" % (key)
221    elif state == 'keys':
222        res = get_xattr_keys(module, path, follow)
223        msg = "returning all keys"
224    elif state == 'all':
225        res = get_xattr(module, path, None, follow)
226        msg = "dumping all"
227    else:
228        res = get_xattr(module, path, key, follow)
229        msg = "returning %s" % key
230
231    module.exit_json(changed=changed, msg=msg, xattr=res)
232
233
234if __name__ == '__main__':
235    main()
236