1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/plugins/modules/storage/zfs/zfs.py
froebela baa721ac22
zfs.py: treated received properties as local and added diff mode support (#502)
* zfs.py: treated received properties as local and added diff mode support

If you use "zfs set" to explicitly set ZFS properties, they are marked as from source "local". If ZFS properties are implicitly set by using "zfs send" and "zfs receive", for example as part of a template based installation, they are marked as from source "received". But as there is no technical difference between both types of them, the “received” ZFS properties should also be considered “local”. Otherwise Ansible would detect changes, which aren’t actual changes. Therefore I changed line 202/207 to reflect this.

For us it’s quite important, that Ansible modules support the diff mode in order to qualify changes. Therefore I added some code lines to address this.

* added changelog fragment for PR #502

* fixed typos in changelog fragment for PR #502

* minor changes in changelog fragment for PR #502

* added link to pull request in changelog fragment for PR #502

* extended the diff data structure to always include the name of the zfs filesystem

* added code to also maintain the diff data structure after a change

* reverted back some code lines for better code readability

* added an extra dict in the diff data structure to hold the zfs properties
2021-08-31 07:11:58 +02:00

297 lines
9.6 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2013, Johan Wiren <johan.wiren.se@gmail.com>
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: zfs
short_description: Manage zfs
description:
- Manages ZFS file systems, volumes, clones and snapshots
options:
name:
description:
- File system, snapshot or volume name e.g. C(rpool/myfs).
required: true
type: str
state:
description:
- Whether to create (C(present)), or remove (C(absent)) a
file system, snapshot or volume. All parents/children
will be created/destroyed as needed to reach the desired state.
choices: [ absent, present ]
required: true
type: str
origin:
description:
- Snapshot from which to create a clone.
type: str
extra_zfs_properties:
description:
- A dictionary of zfs properties to be set.
- See the zfs(8) man page for more information.
type: dict
notes:
- C(check_mode) is supported, but in certain situations it may report a task
as changed that will not be reported as changed when C(check_mode) is disabled.
For example, this might occur when the zpool C(altroot) option is set or when
a size is written using human-readable notation, such as C(1M) or C(1024K),
instead of as an unqualified byte count, such as C(1048576).
author:
- Johan Wiren (@johanwiren)
'''
EXAMPLES = '''
- name: Create a new file system called myfs in pool rpool with the setuid property turned off
community.general.zfs:
name: rpool/myfs
state: present
extra_zfs_properties:
setuid: off
- name: Create a new volume called myvol in pool rpool.
community.general.zfs:
name: rpool/myvol
state: present
extra_zfs_properties:
volsize: 10M
- name: Create a snapshot of rpool/myfs file system.
community.general.zfs:
name: rpool/myfs@mysnapshot
state: present
- name: Create a new file system called myfs2 with snapdir enabled
community.general.zfs:
name: rpool/myfs2
state: present
extra_zfs_properties:
snapdir: enabled
- name: Create a new file system by cloning a snapshot
community.general.zfs:
name: rpool/cloned_fs
state: present
origin: rpool/myfs@mysnapshot
- name: Destroy a filesystem
community.general.zfs:
name: rpool/myfs
state: absent
'''
import os
from ansible.module_utils.basic import AnsibleModule
class Zfs(object):
def __init__(self, module, name, properties):
self.module = module
self.name = name
self.properties = properties
self.changed = False
self.zfs_cmd = module.get_bin_path('zfs', True)
self.zpool_cmd = module.get_bin_path('zpool', True)
self.pool = name.split('/')[0].split('@')[0]
self.is_solaris = os.uname()[0] == 'SunOS'
self.is_openzfs = self.check_openzfs()
self.enhanced_sharing = self.check_enhanced_sharing()
def check_openzfs(self):
cmd = [self.zpool_cmd]
cmd.extend(['get', 'version'])
cmd.append(self.pool)
(rc, out, err) = self.module.run_command(cmd, check_rc=True)
version = out.splitlines()[-1].split()[2]
if version == '-':
return True
if int(version) == 5000:
return True
return False
def check_enhanced_sharing(self):
if self.is_solaris and not self.is_openzfs:
cmd = [self.zpool_cmd]
cmd.extend(['get', 'version'])
cmd.append(self.pool)
(rc, out, err) = self.module.run_command(cmd, check_rc=True)
version = out.splitlines()[-1].split()[2]
if int(version) >= 34:
return True
return False
def exists(self):
cmd = [self.zfs_cmd, 'list', '-t', 'all', self.name]
(rc, out, err) = self.module.run_command(' '.join(cmd))
if rc == 0:
return True
else:
return False
def create(self):
if self.module.check_mode:
self.changed = True
return
properties = self.properties
origin = self.module.params.get('origin', None)
cmd = [self.zfs_cmd]
if "@" in self.name:
action = 'snapshot'
elif origin:
action = 'clone'
else:
action = 'create'
cmd.append(action)
if action in ['create', 'clone']:
cmd += ['-p']
if properties:
for prop, value in properties.items():
if prop == 'volsize':
cmd += ['-V', value]
elif prop == 'volblocksize':
cmd += ['-b', value]
else:
cmd += ['-o', '%s="%s"' % (prop, value)]
if origin and action == 'clone':
cmd.append(origin)
cmd.append(self.name)
(rc, out, err) = self.module.run_command(' '.join(cmd))
if rc == 0:
self.changed = True
else:
self.module.fail_json(msg=err)
def destroy(self):
if self.module.check_mode:
self.changed = True
return
cmd = [self.zfs_cmd, 'destroy', '-R', self.name]
(rc, out, err) = self.module.run_command(' '.join(cmd))
if rc == 0:
self.changed = True
else:
self.module.fail_json(msg=err)
def set_property(self, prop, value):
if self.module.check_mode:
self.changed = True
return
cmd = [self.zfs_cmd, 'set', prop + '=' + str(value), self.name]
(rc, out, err) = self.module.run_command(cmd)
if rc != 0:
self.module.fail_json(msg=err)
def set_properties_if_changed(self):
diff = {'before': {'extra_zfs_properties': {}}, 'after': {'extra_zfs_properties': {}}}
current_properties = self.get_current_properties()
for prop, value in self.properties.items():
current_value = current_properties.get(prop, None)
if current_value != value:
self.set_property(prop, value)
diff['before']['extra_zfs_properties'][prop] = current_value
diff['after']['extra_zfs_properties'][prop] = value
if self.module.check_mode:
return diff
updated_properties = self.get_current_properties()
for prop in self.properties:
value = updated_properties.get(prop, None)
if value is None:
self.module.fail_json(msg="zfsprop was not present after being successfully set: %s" % prop)
if current_properties.get(prop, None) != value:
self.changed = True
if prop in diff['after']['extra_zfs_properties']:
diff['after']['extra_zfs_properties'][prop] = value
return diff
def get_current_properties(self):
cmd = [self.zfs_cmd, 'get', '-H', '-p', '-o', "property,value,source"]
if self.enhanced_sharing:
cmd += ['-e']
cmd += ['all', self.name]
rc, out, err = self.module.run_command(" ".join(cmd))
properties = dict()
for line in out.splitlines():
prop, value, source = line.split('\t')
# include source '-' so that creation-only properties are not removed
# to avoids errors when the dataset already exists and the property is not changed
# this scenario is most likely when the same playbook is run more than once
if source == 'local' or source == 'received' or source == '-':
properties[prop] = value
# Add alias for enhanced sharing properties
if self.enhanced_sharing:
properties['sharenfs'] = properties.get('share.nfs', None)
properties['sharesmb'] = properties.get('share.smb', None)
return properties
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True),
state=dict(type='str', required=True, choices=['absent', 'present']),
origin=dict(type='str', default=None),
extra_zfs_properties=dict(type='dict', default={}),
),
supports_check_mode=True,
)
state = module.params.get('state')
name = module.params.get('name')
if module.params.get('origin') and '@' in name:
module.fail_json(msg='cannot specify origin when operating on a snapshot')
# Reverse the boolification of zfs properties
for prop, value in module.params['extra_zfs_properties'].items():
if isinstance(value, bool):
if value is True:
module.params['extra_zfs_properties'][prop] = 'on'
else:
module.params['extra_zfs_properties'][prop] = 'off'
else:
module.params['extra_zfs_properties'][prop] = value
result = dict(
name=name,
state=state,
)
zfs = Zfs(module, name, module.params['extra_zfs_properties'])
if state == 'present':
if zfs.exists():
result['diff'] = zfs.set_properties_if_changed()
else:
zfs.create()
result['diff'] = {'before': {'state': 'absent'}, 'after': {'state': state}}
elif state == 'absent':
if zfs.exists():
zfs.destroy()
result['diff'] = {'before': {'state': 'present'}, 'after': {'state': state}}
else:
result['diff'] = {}
result['diff']['before_header'] = name
result['diff']['after_header'] = name
result.update(zfs.properties)
result['changed'] = zfs.changed
module.exit_json(**result)
if __name__ == '__main__':
main()