diff options
Diffstat (limited to 'ansible_collections/community/general/plugins/modules/beadm.py')
-rw-r--r-- | ansible_collections/community/general/plugins/modules/beadm.py | 415 |
1 files changed, 415 insertions, 0 deletions
diff --git a/ansible_collections/community/general/plugins/modules/beadm.py b/ansible_collections/community/general/plugins/modules/beadm.py new file mode 100644 index 000000000..8857fd846 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/beadm.py @@ -0,0 +1,415 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2016, Adam Števko <adam.stevko@gmail.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: beadm +short_description: Manage ZFS boot environments on FreeBSD/Solaris/illumos systems +description: + - Create, delete or activate ZFS boot environments. + - Mount and unmount ZFS boot environments. +author: Adam Števko (@xen0l) +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: none +options: + name: + description: + - ZFS boot environment name. + type: str + required: true + aliases: [ "be" ] + snapshot: + description: + - If specified, the new boot environment will be cloned from the given + snapshot or inactive boot environment. + type: str + description: + description: + - Associate a description with a new boot environment. This option is + available only on Solarish platforms. + type: str + options: + description: + - Create the datasets for new BE with specific ZFS properties. + - Multiple options can be specified. + - This option is available only on Solarish platforms. + type: str + mountpoint: + description: + - Path where to mount the ZFS boot environment. + type: path + state: + description: + - Create or delete ZFS boot environment. + type: str + choices: [ absent, activated, mounted, present, unmounted ] + default: present + force: + description: + - Specifies if the unmount should be forced. + type: bool + default: false +''' + +EXAMPLES = r''' +- name: Create ZFS boot environment + community.general.beadm: + name: upgrade-be + state: present + +- name: Create ZFS boot environment from existing inactive boot environment + community.general.beadm: + name: upgrade-be + snapshot: be@old + state: present + +- name: Create ZFS boot environment with compression enabled and description "upgrade" + community.general.beadm: + name: upgrade-be + options: "compression=on" + description: upgrade + state: present + +- name: Delete ZFS boot environment + community.general.beadm: + name: old-be + state: absent + +- name: Mount ZFS boot environment on /tmp/be + community.general.beadm: + name: BE + mountpoint: /tmp/be + state: mounted + +- name: Unmount ZFS boot environment + community.general.beadm: + name: BE + state: unmounted + +- name: Activate ZFS boot environment + community.general.beadm: + name: upgrade-be + state: activated +''' + +RETURN = r''' +name: + description: BE name + returned: always + type: str + sample: pre-upgrade +snapshot: + description: ZFS snapshot to create BE from + returned: always + type: str + sample: rpool/ROOT/oi-hipster@fresh +description: + description: BE description + returned: always + type: str + sample: Upgrade from 9.0 to 10.0 +options: + description: BE additional options + returned: always + type: str + sample: compression=on +mountpoint: + description: BE mountpoint + returned: always + type: str + sample: /mnt/be +state: + description: state of the target + returned: always + type: str + sample: present +force: + description: If forced action is wanted + returned: always + type: bool + sample: false +''' + +import os +from ansible.module_utils.basic import AnsibleModule + + +class BE(object): + def __init__(self, module): + self.module = module + + self.name = module.params['name'] + self.snapshot = module.params['snapshot'] + self.description = module.params['description'] + self.options = module.params['options'] + self.mountpoint = module.params['mountpoint'] + self.state = module.params['state'] + self.force = module.params['force'] + self.is_freebsd = os.uname()[0] == 'FreeBSD' + + def _beadm_list(self): + cmd = [self.module.get_bin_path('beadm'), 'list', '-H'] + if '@' in self.name: + cmd.append('-s') + return self.module.run_command(cmd) + + def _find_be_by_name(self, out): + if '@' in self.name: + for line in out.splitlines(): + if self.is_freebsd: + check = line.split() + if check == []: + continue + full_name = check[0].split('/') + if full_name == []: + continue + check[0] = full_name[len(full_name) - 1] + if check[0] == self.name: + return check + else: + check = line.split(';') + if check[0] == self.name: + return check + else: + for line in out.splitlines(): + if self.is_freebsd: + check = line.split() + if check[0] == self.name: + return check + else: + check = line.split(';') + if check[0] == self.name: + return check + return None + + def exists(self): + (rc, out, dummy) = self._beadm_list() + + if rc == 0: + if self._find_be_by_name(out): + return True + else: + return False + else: + return False + + def is_activated(self): + (rc, out, dummy) = self._beadm_list() + + if rc == 0: + line = self._find_be_by_name(out) + if line is None: + return False + if self.is_freebsd: + if 'R' in line[1]: + return True + else: + if 'R' in line[2]: + return True + + return False + + def activate_be(self): + cmd = [self.module.get_bin_path('beadm'), 'activate', self.name] + return self.module.run_command(cmd) + + def create_be(self): + cmd = [self.module.get_bin_path('beadm'), 'create'] + + if self.snapshot: + cmd.extend(['-e', self.snapshot]) + if not self.is_freebsd: + if self.description: + cmd.extend(['-d', self.description]) + if self.options: + cmd.extend(['-o', self.options]) + + cmd.append(self.name) + + return self.module.run_command(cmd) + + def destroy_be(self): + cmd = [self.module.get_bin_path('beadm'), 'destroy', '-F', self.name] + return self.module.run_command(cmd) + + def is_mounted(self): + (rc, out, dummy) = self._beadm_list() + + if rc == 0: + line = self._find_be_by_name(out) + if line is None: + return False + if self.is_freebsd: + # On FreeBSD, we exclude currently mounted BE on /, as it is + # special and can be activated even if it is mounted. That is not + # possible with non-root BEs. + if line[2] != '-' and line[2] != '/': + return True + else: + if line[3]: + return True + + return False + + def mount_be(self): + cmd = [self.module.get_bin_path('beadm'), 'mount', self.name] + + if self.mountpoint: + cmd.append(self.mountpoint) + + return self.module.run_command(cmd) + + def unmount_be(self): + cmd = [self.module.get_bin_path('beadm'), 'unmount'] + if self.force: + cmd.append('-f') + cmd.append(self.name) + + return self.module.run_command(cmd) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(type='str', required=True, aliases=['be']), + snapshot=dict(type='str'), + description=dict(type='str'), + options=dict(type='str'), + mountpoint=dict(type='path'), + state=dict(type='str', default='present', choices=['absent', 'activated', 'mounted', 'present', 'unmounted']), + force=dict(type='bool', default=False), + ), + supports_check_mode=True, + ) + + be = BE(module) + + rc = None + out = '' + err = '' + result = {} + result['name'] = be.name + result['state'] = be.state + + if be.snapshot: + result['snapshot'] = be.snapshot + + if be.description: + result['description'] = be.description + + if be.options: + result['options'] = be.options + + if be.mountpoint: + result['mountpoint'] = be.mountpoint + + if be.state == 'absent': + # beadm on FreeBSD and Solarish systems differs in delete behaviour in + # that we are not allowed to delete activated BE on FreeBSD while on + # Solarish systems we cannot delete BE if it is mounted. We add mount + # check for both platforms as BE should be explicitly unmounted before + # being deleted. On FreeBSD, we also check if the BE is activated. + if be.exists(): + if not be.is_mounted(): + if module.check_mode: + module.exit_json(changed=True) + + if be.is_freebsd: + if be.is_activated(): + module.fail_json(msg='Unable to remove active BE!') + + (rc, out, err) = be.destroy_be() + + if rc != 0: + module.fail_json(msg='Error while destroying BE: "%s"' % err, + name=be.name, + stderr=err, + rc=rc) + else: + module.fail_json(msg='Unable to remove BE as it is mounted!') + + elif be.state == 'present': + if not be.exists(): + if module.check_mode: + module.exit_json(changed=True) + + (rc, out, err) = be.create_be() + + if rc != 0: + module.fail_json(msg='Error while creating BE: "%s"' % err, + name=be.name, + stderr=err, + rc=rc) + + elif be.state == 'activated': + if not be.is_activated(): + if module.check_mode: + module.exit_json(changed=True) + + # On FreeBSD, beadm is unable to activate mounted BEs, so we add + # an explicit check for that case. + if be.is_freebsd: + if be.is_mounted(): + module.fail_json(msg='Unable to activate mounted BE!') + + (rc, out, err) = be.activate_be() + + if rc != 0: + module.fail_json(msg='Error while activating BE: "%s"' % err, + name=be.name, + stderr=err, + rc=rc) + elif be.state == 'mounted': + if not be.is_mounted(): + if module.check_mode: + module.exit_json(changed=True) + + (rc, out, err) = be.mount_be() + + if rc != 0: + module.fail_json(msg='Error while mounting BE: "%s"' % err, + name=be.name, + stderr=err, + rc=rc) + + elif be.state == 'unmounted': + if be.is_mounted(): + if module.check_mode: + module.exit_json(changed=True) + + (rc, out, err) = be.unmount_be() + + if rc != 0: + module.fail_json(msg='Error while unmounting BE: "%s"' % err, + name=be.name, + stderr=err, + rc=rc) + + if rc is None: + result['changed'] = False + else: + result['changed'] = True + + if out: + result['stdout'] = out + if err: + result['stderr'] = err + + module.exit_json(**result) + + +if __name__ == '__main__': + main() |