1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# (c) 2016, Adam Števko <adam.stevko@gmail.com>
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
10
11DOCUMENTATION = '''
12---
13module: zfs_facts
14short_description: Gather facts about ZFS datasets.
15description:
16  - Gather facts from ZFS dataset properties.
17author: Adam Števko (@xen0l)
18options:
19    name:
20        description:
21            - ZFS dataset name.
22        required: yes
23        aliases: [ "ds", "dataset" ]
24        type: str
25    recurse:
26        description:
27            - Specifies if properties for any children should be recursively
28              displayed.
29        type: bool
30        default: 'no'
31    parsable:
32        description:
33            - Specifies if property values should be displayed in machine
34              friendly format.
35        type: bool
36        default: 'no'
37    properties:
38        description:
39            - Specifies which dataset properties should be queried in comma-separated format.
40              For more information about dataset properties, check zfs(1M) man page.
41        default: all
42        type: str
43    type:
44        description:
45            - Specifies which datasets types to display. Multiple values have to be
46              provided in comma-separated form.
47        choices: [ 'all', 'filesystem', 'volume', 'snapshot', 'bookmark' ]
48        default: all
49        type: str
50    depth:
51        description:
52            - Specifies recursion depth.
53        type: int
54'''
55
56EXAMPLES = '''
57- name: Gather facts about ZFS dataset rpool/export/home
58  community.general.zfs_facts:
59    dataset: rpool/export/home
60
61- name: Report space usage on ZFS filesystems under data/home
62  community.general.zfs_facts:
63    name: data/home
64    recurse: yes
65    type: filesystem
66
67- ansible.builtin.debug:
68    msg: 'ZFS dataset {{ item.name }} consumes {{ item.used }} of disk space.'
69  with_items: '{{ ansible_zfs_datasets }}'
70'''
71
72RETURN = '''
73name:
74    description: ZFS dataset name
75    returned: always
76    type: str
77    sample: rpool/var/spool
78parsable:
79    description: if parsable output should be provided in machine friendly format.
80    returned: if 'parsable' is set to True
81    type: bool
82    sample: True
83recurse:
84    description: if we should recurse over ZFS dataset
85    returned: if 'recurse' is set to True
86    type: bool
87    sample: True
88zfs_datasets:
89    description: ZFS dataset facts
90    returned: always
91    type: str
92    sample:
93            {
94                "aclinherit": "restricted",
95                "aclmode": "discard",
96                "atime": "on",
97                "available": "43.8G",
98                "canmount": "on",
99                "casesensitivity": "sensitive",
100                "checksum": "on",
101                "compression": "off",
102                "compressratio": "1.00x",
103                "copies": "1",
104                "creation": "Thu Jun 16 11:37 2016",
105                "dedup": "off",
106                "devices": "on",
107                "exec": "on",
108                "filesystem_count": "none",
109                "filesystem_limit": "none",
110                "logbias": "latency",
111                "logicalreferenced": "18.5K",
112                "logicalused": "3.45G",
113                "mlslabel": "none",
114                "mounted": "yes",
115                "mountpoint": "/rpool",
116                "name": "rpool",
117                "nbmand": "off",
118                "normalization": "none",
119                "org.openindiana.caiman:install": "ready",
120                "primarycache": "all",
121                "quota": "none",
122                "readonly": "off",
123                "recordsize": "128K",
124                "redundant_metadata": "all",
125                "refcompressratio": "1.00x",
126                "referenced": "29.5K",
127                "refquota": "none",
128                "refreservation": "none",
129                "reservation": "none",
130                "secondarycache": "all",
131                "setuid": "on",
132                "sharenfs": "off",
133                "sharesmb": "off",
134                "snapdir": "hidden",
135                "snapshot_count": "none",
136                "snapshot_limit": "none",
137                "sync": "standard",
138                "type": "filesystem",
139                "used": "4.41G",
140                "usedbychildren": "4.41G",
141                "usedbydataset": "29.5K",
142                "usedbyrefreservation": "0",
143                "usedbysnapshots": "0",
144                "utf8only": "off",
145                "version": "5",
146                "vscan": "off",
147                "written": "29.5K",
148                "xattr": "on",
149                "zoned": "off"
150            }
151'''
152
153from collections import defaultdict
154
155from ansible.module_utils.basic import AnsibleModule
156from ansible.module_utils.six import iteritems
157
158
159SUPPORTED_TYPES = ['all', 'filesystem', 'volume', 'snapshot', 'bookmark']
160
161
162class ZFSFacts(object):
163    def __init__(self, module):
164
165        self.module = module
166
167        self.name = module.params['name']
168        self.recurse = module.params['recurse']
169        self.parsable = module.params['parsable']
170        self.properties = module.params['properties']
171        self.type = module.params['type']
172        self.depth = module.params['depth']
173
174        self._datasets = defaultdict(dict)
175        self.facts = []
176
177    def dataset_exists(self):
178        cmd = [self.module.get_bin_path('zfs'), 'list', self.name]
179
180        (rc, out, err) = self.module.run_command(cmd)
181
182        if rc == 0:
183            return True
184        else:
185            return False
186
187    def get_facts(self):
188        cmd = [self.module.get_bin_path('zfs'), 'get', '-H']
189        if self.parsable:
190            cmd.append('-p')
191        if self.recurse:
192            cmd.append('-r')
193        if int(self.depth) != 0:
194            cmd.append('-d')
195            cmd.append('%s' % self.depth)
196        if self.type:
197            cmd.append('-t')
198            cmd.append(self.type)
199        cmd.extend(['-o', 'name,property,value', self.properties, self.name])
200
201        (rc, out, err) = self.module.run_command(cmd)
202
203        if rc == 0:
204            for line in out.splitlines():
205                dataset, property, value = line.split('\t')
206
207                self._datasets[dataset].update({property: value})
208
209            for k, v in iteritems(self._datasets):
210                v.update({'name': k})
211                self.facts.append(v)
212
213            return {'ansible_zfs_datasets': self.facts}
214        else:
215            self.module.fail_json(msg='Error while trying to get facts about ZFS dataset: %s' % self.name,
216                                  stderr=err,
217                                  rc=rc)
218
219
220def main():
221    module = AnsibleModule(
222        argument_spec=dict(
223            name=dict(required=True, aliases=['ds', 'dataset'], type='str'),
224            recurse=dict(required=False, default=False, type='bool'),
225            parsable=dict(required=False, default=False, type='bool'),
226            properties=dict(required=False, default='all', type='str'),
227            type=dict(required=False, default='all', type='str', choices=SUPPORTED_TYPES),
228            depth=dict(required=False, default=0, type='int')
229        ),
230        supports_check_mode=True
231    )
232
233    zfs_facts = ZFSFacts(module)
234
235    result = {}
236    result['changed'] = False
237    result['name'] = zfs_facts.name
238
239    if zfs_facts.parsable:
240        result['parsable'] = zfs_facts.parsable
241
242    if zfs_facts.recurse:
243        result['recurse'] = zfs_facts.recurse
244
245    if zfs_facts.dataset_exists():
246        result['ansible_facts'] = zfs_facts.get_facts()
247    else:
248        module.fail_json(msg='ZFS dataset %s does not exist!' % zfs_facts.name)
249
250    module.exit_json(**result)
251
252
253if __name__ == '__main__':
254    main()
255