diff --git a/library/snapshot.py b/library/snapshot.py index 578dc61..0a7f31a 100644 --- a/library/snapshot.py +++ b/library/snapshot.py @@ -1,15 +1,180 @@ -from __future__ import print_function +#!/usr/bin/python + +# Copyright: (c) 2024, Red Hat, Inc. +# 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 -import argparse -import json -import logging -import math -import os -import re -import stat -import subprocess -import sys from os.path import join as path_join +import sys +import subprocess +import stat +import re +import os +import math +import logging +import json +import argparse + +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = """ +--- +module: snapshot + +short_description: Module for snapshots + +version_added: "1.0.0" + +description: Manage LVM snapshots. + +options: + snapshot_lvm_action: + description: action to perform + type: str + snapshot_lvm_set: + description: set of volumes + type: str + snapshot_lvm_verify_only: + description: only verify system is in correct state + type: bool + snapshot_lvm_percent_space_required + description: + See the LVM man page for lvcreate with the -s (snapshot) and -L (size) options. + The snapshot role will ensure that there is at least snapshot_lvm_percent_space_required + space available in the VG. When used inside of a snapset definition, use + percent_space_required parameter. + type: str + snapshot_lvm_all_vgs + description: This is a boolean value with default false. If true the role will snapshot + all VGs on the target system. If false, the snapshot_lvm_vg or snapshot_lvm_set + must be set. + type: bool + snapshot_lvm_vg + description: + If set, the role will create snapshots for all the logical volumes in the volume group. + If snapshot_lvm_lv is also set, a snapshot will be created for only that logical volume + in the volume group. If neither snapshot_lvm_all_vgs or snapshot_lvm_set are set, + snapshot_lvm_vg is required. When used inside of a snapset definition, use + vg parameter. + type: str + snapshot_lvm_lv + description: + If set, the role will create snapshots for the single logical volume in the volume group + specified by snapshot_lvm_vg. The parameter requires snapshot_lvm_vg is set to a valid + volume group. When used inside of a snapset definition, use lv parameter. + type: str + snapshot_lvm_verify_only + description: + If true, the check and remove commands verify that the system is in the correct state. + For the remove command, the target system will be searched for any snapshots that would + be removed by the remove command without snapshot_lvm_verify_only. + type: str + snapshot_lvm_mountpoint_create + description: + If the mount point specified doesn't currently exist, create the mount point and any + parent directories necessary for the mount point. When used inside of a snapset definition, + use mountpoint_create parameter. + type: bool + snapshot_lvm_mountpoint + description: + The mount target for the block device. When used inside of a snapset definition, + use mountpoint parameter. + type: str + snapshot_lvm_mount_origin + description: + If set to true, mount the origin of the snapshot rather than the snapshot. + When used inside of a snapset definition, use mount_origin parameter. + type: bool + snapshot_lvm_mount_options + description: + Options to pass to the mount command for the filesystem. The argument is + a comma separated list. See the man page for mount for details. + Note that XFS by default will not allow multiple filesystems with the + same UUID to be mounted at the same time. Using the "nouuid" will + bypass the duplicate UUID check and allow a snapshot to be mounted + at the same time as the snapshot source. + type: str + snapshot_lvm_unmount_all + description: + If set to true, unmount all mountpoint for the resulting blockdevice. + Linux allows filesystems to be mounted in multiple locations. Setting + this flag will unmount all locations. + type: bool + snapshot_lvm_vg_include + description: + When using `snapshot_lvm_all_vgs`, there may be some + subset of all volume groups that you want to use. Set `snapshot_lvm_vg_include` + to a regex pattern that matches the names of the volume groups you want to use and + the rest will be excluded + type: str + +author: + - Todd Gill (tgill@redhat.com) +""" + +EXAMPLES = """ +# Create Snapshots of all VGs +--- +- name: Extend all snapshots + hosts: all + vars: + snapshot_lvm_percent_space_required: 40 + snapshot_lvm_all_vgs: true + snapshot_lvm_action: extend + snapshot_lvm_set: + name: snapshot + volumes: + - name: data1 snapshot + vg: data_vg + lv: data1 + - name: data2 snapshot + vg: data_vg + lv: data2 + + + tasks: + - name: Extend the snapshot set + vars: + snapshot_lvm_percent_space_required: 40 + snapshot_lvm_all_vgs: true + snapshot_lvm_set: "{{ snapshot_lvm_set }}" + snapshot_lvm_action: extend + + + roles: + - redhat.rhel_system_roles.snapshot + +""" + +RETURN = """ +# Examples of possible return values. +msg: + description: On success an empty string. On failure a message to + indicate the type of failure. + type: str +data: + description: json with an entry for each snapshot. data is included + for the list command only. + type: json +return_code: + description: 0 is returned for success. On failure a return code from + the SnapshotStatus class. + type: int +changed: + description: an indicator set to true if any action was taken, otherwize + set to false. + type: bool + +""" + logger = logging.getLogger("snapshot-role") @@ -17,6 +182,7 @@ MAX_LVM_NAME = 127 CHUNK_SIZE = 65536 DEV_PREFIX = "/dev" +VG_INCLUDE = None # Minimum LVM snapshot size (512MiB) LVM_MIN_SNAPSHOT_SIZE = 512 * 1024**2 @@ -423,17 +589,20 @@ def vgs_lvs_iterator(vg_name, lv_name, omit_empty_lvs=False): yield (vg, lvs) -def vgs_lvs_dict(vg_name, lv_name): +def vgs_lvs_dict(vg_name, lv_name, vg_include): """Return a dict using vgs_lvs_iterator. Key is vg name, value is list of lvs corresponding to vg. The returned dict will not have vgs that have no lvs.""" return dict( - [(vg["vg_name"], lvs) for vg, lvs in vgs_lvs_iterator(vg_name, lv_name, True)] + [ + (vg["vg_name"], lvs) + for vg, lvs in vgs_lvs_iterator(vg_name, lv_name, vg_include, True) + ] ) -def lvm_list_json(vg_name, lv_name): - vg_dict = vgs_lvs_dict(vg_name, lv_name) +def lvm_list_json(vg_name, lv_name, vg_include): + vg_dict = vgs_lvs_dict(vg_name, lv_name, vg_include) fs_dict = dict() top_level = dict() for lv_list in vg_dict.values(): @@ -1164,10 +1333,9 @@ def mount_snapshot_set( def mount_verify(origin, mountpoint, blockdev, vg_name, lv_name, snapset_name): logger.info( - "mount_verify_lv : %d %s %s %s %s %s", + "mount_verify_lv : %d %s %s %s %s", origin, mountpoint, - blockdev, vg_name, lv_name, snapset_name, @@ -1179,7 +1347,7 @@ def mount_verify(origin, mountpoint, blockdev, vg_name, lv_name, snapset_name): "must provide mountpoint", ) - if not blockdev and (not vg_name or not lv_name): + if not vg_name or not lv_name: return ( SnapshotStatus.ERROR_MOUNT_INVALID_PARAMS, "must provide blockdev or vg/lv for mount source", @@ -1715,40 +1883,57 @@ def get_required_space(required_space_str): return SnapshotStatus.SNAPSHOT_OK, "", percent_space_required -def validate_general_args(args): +def validate_general_args(module_args): rc = SnapshotStatus.ERROR_CMD_INVALID message = "" - if args.all and args.volume_group: + if module_args["snapshot_lvm_all_vgs"] and module_args["snapshot_lvm_vg"]: return ( rc, "--all and --volume_group are mutually exclusive for operation " - + args.operation, + + module_args["snapshot_lvm_action"], ) - if not args.all and args.volume_group is None and args.suffix is None: + if ( + not module_args["snapshot_lvm_all_vgs"] + and module_args["snapshot_lvm_vg"] is None + and module_args["snapshot_lvm_snapset_name"] is None + ): return ( rc, "must specify either --all, --volume_group or --snapset for operation " - + args.operation, + + module_args["snapshot_lvm_action"], ) - if not args.all and args.volume_group is None and args.logical_volume: + if ( + not module_args["snapshot_lvm_all_vgs"] + and module_args["snapshot_lvm_vg"] is None + and module_args["snapshot_lvm_lv"] + ): return ( rc, "--logical_volume requires --volume_group parameter for operation " - + args.operation, + + module_args["snapshot_lvm_action"], ) - if not args.suffix: - return rc, "--snapset is required for operation " + args.operation + if not module_args["snapshot_lvm_snapset_name"]: + return ( + rc, + "--snapset is required for operation " + module_args["snapshot_lvm_action"], + ) - if len(args.suffix) == 0: - return rc, "Snapset name must be provided for operation " + args.operation + if len(module_args["snapshot_lvm_snapset_name"]) == 0: + return ( + rc, + "Snapset name must be provided for operation " + + module_args["snapshot_lvm_action"], + ) - # not all commands include required_space - if hasattr(args, "required_space"): - rc, message, _required_space = get_required_space(args.required_space) + # not all commands include snapshot_lvm_percent_space_required + if module_args["snapshot_lvm_percent_space_required"]: + rc, message, _required_space = get_required_space( + module_args["snapshot_lvm_percent_space_required"] + ) if rc != SnapshotStatus.SNAPSHOT_OK: return rc, message @@ -1756,8 +1941,16 @@ def validate_general_args(args): return SnapshotStatus.SNAPSHOT_OK, message -def validate_snapshot_args(args): - rc, message, _required_space = get_required_space(args.required_space) +def validate_snapshot_args(module_args): + if not module_args["snapshot_lvm_percent_space_required"]: + return ( + SnapshotStatus.ERROR_JSON_PARSER_ERROR, + "snapset snapshot_lvm_percent_space_required entry not found", + ) + + rc, message, _required_space = get_required_space( + module_args["snapshot_lvm_percent_space_required"] + ) if rc != SnapshotStatus.SNAPSHOT_OK: return {"return_code": rc, "errors": [message], "changed": False} @@ -1765,57 +1958,56 @@ def validate_snapshot_args(args): return SnapshotStatus.SNAPSHOT_OK, "" -def validate_mount_args(args): - if not args.blockdev and (not args.volume_group or not args.logical_volume): +def validate_mount_args(module_args): + if not module_args["snapshot_lvm_vg"] or not module_args["snapshot_lvm_lv"]: return ( SnapshotStatus.ERROR_MOUNT_INVALID_PARAMS, - "must provide blockdev or vg/lv for mount source", + "must provide vg/lv for mount source", ) - if not args.mountpoint: + if not module_args["snapshot_lvm_mountpoint"]: return SnapshotStatus.ERROR_MOUNT_INVALID_PARAMS, "mountpoint is required" return SnapshotStatus.SNAPSHOT_OK, "" -def validate_umount_args(args): +def validate_umount_args(module_args): - if not args.volume_group or not args.logical_volume: + if not module_args["snapshot_lvm_vg"] or not module_args["snapshot_lvm_lv"]: return ( SnapshotStatus.ERROR_MOUNT_INVALID_PARAMS, "must provide vg/lv for umount source", ) - if not args.mountpoint and not args.blockdev: + if not module_args["snapshot_lvm_mountpoint"]: return ( SnapshotStatus.ERROR_MOUNT_INVALID_PARAMS, - "--mountpoint or --blockdev is required", + "--mountpoint is required", ) return SnapshotStatus.SNAPSHOT_OK, "" -def validate_snapset_args(args): - cmd = get_command_const(args.operation) +def validate_snapset_args(cmd, module_args): - rc, message = validate_general_args(args) + rc, message = validate_general_args(module_args) if rc != SnapshotStatus.SNAPSHOT_OK: return {"return_code": rc, "errors": [message], "changed": False}, None if cmd == SnapshotCommand.SNAPSHOT: - rc, message = validate_snapshot_args(args) + rc, message = validate_snapshot_args(module_args) # # Currently check, remove, revert, extend and list don't need extra validation # elif cmd == SnapshotCommand.MOUNT: - rc, message = validate_mount_args(args) + rc, message = validate_mount_args(module_args) elif cmd == SnapshotCommand.UMOUNT: - rc, message = validate_umount_args(args) + rc, message = validate_umount_args(module_args) if rc != SnapshotStatus.SNAPSHOT_OK: return {"return_code": rc, "errors": [message], "changed": False}, None - rc, message, snapset_dict = get_json_from_args(args) + rc, message, snapset_dict = get_json_from_args(module_args) return {"return_code": rc, "errors": [message], "changed": False}, snapset_dict @@ -1948,83 +2140,53 @@ def validate_snapset_json(cmd, snapset, verify_only): return {"return_code": rc, "errors": [message], "changed": False}, snapset_dict -def get_mount_json_from_args(args): - volume_list = [] - args_json = {} - volume = {} - - args_json["name"] = args.suffix - - volume["create"] = args.create - volume["origin"] = args.origin - volume["verify"] = args.verify - volume["mountpoint"] = args.mountpoint - volume["fstype"] = args.fstype - volume["options"] = args.options - volume["suffix"] = args.suffix - volume["lv"] = args.logical_volume - volume["vg"] = args.volume_group - - volume_list.append(volume) - args_json["volumes"] = volume_list - - return SnapshotStatus.SNAPSHOT_OK, "", args_json - - -def get_umount_json_from_args(args): - volume_list = [] - args_json = {} - volume = {} - - args_json["name"] = args.suffix - volume["lv"] = args.logical_volume - volume["vg"] = args.volume_group - volume["mountpoint"] = args.mountpoint - volume["all_targets"] = args.all_targets - - volume_list.append(volume) - args_json["volumes"] = volume_list - - return SnapshotStatus.SNAPSHOT_OK, "", args_json - - -def get_json_from_args(args): +def get_json_from_args(module_args): volume_list = [] args_json = {} - cmd = get_command_const(args.operation) + cmd = get_command_const(module_args["snapshot_lvm_action"]) - if not args.all and cmd != SnapshotCommand.UMOUNT: - rc, message = verify_source_lvs_exist(args.volume_group, args.logical_volume) + if not module_args["snapshot_lvm_all_vgs"] and cmd != SnapshotCommand.UMOUNT: + rc, message = verify_source_lvs_exist( + module_args["snapshot_lvm_vg"], module_args["snapshot_lvm_lv"] + ) if rc != SnapshotStatus.SNAPSHOT_OK: return rc, message, "" - if args.suffix: - args_json["name"] = args.suffix + if module_args["snapshot_lvm_snapset_name"]: + args_json["name"] = module_args["snapshot_lvm_snapset_name"] - for vg, lv_list in vgs_lvs_iterator(args.volume_group, args.logical_volume): + for vg, lv_list in vgs_lvs_iterator( + module_args["snapshot_lvm_vg"], module_args["snapshot_lvm_lv"] + ): vg_str = vg["vg_name"] for lv in lv_list: - if lv["lv_name"].endswith(args.suffix): + if lv["lv_name"].endswith(module_args["snapshot_lvm_snapset_name"]): + continue + continue volume = {} volume["name"] = ("snapshot : " + vg_str + "/" + lv["lv_name"],) volume["vg"] = vg_str volume["lv"] = lv["lv_name"] - if hasattr(args, "required_space"): - volume["percent_space_required"] = args.required_space + + volume["percent_space_required"] = module_args[ + "snapshot_lvm_percent_space_required" + ] if cmd == SnapshotCommand.MOUNT: - volume["mountpoint_create"] = args.create - volume["mountpoint"] = args.mountpoint - volume["mount_origin"] = args.origin - volume["fstype"] = args.fstype - volume["options"] = args.options + volume["mountpoint_create"] = module_args[ + "snapshot_lvm_mountpoint_create" + ] + volume["mountpoint"] = module_args["snapshot_lvm_mountpoint"] + volume["mount_origin"] = module_args["snapshot_lvm_mount_origin"] + volume["fstype"] = module_args["snapshot_lvm_fstype"] + volume["options"] = module_args["snapshot_lvm_mount_options"] if cmd == SnapshotCommand.UMOUNT: - volume["mountpoint"] = args.mountpoint - volume["all_targets"] = args.all_targets + volume["mountpoint"] = module_args["snapshot_lvm_mountpoint"] + volume["all_targets"] = module_args["snapshot_lvm_unmount_all"] volume_list.append(volume) @@ -2033,36 +2195,18 @@ def get_json_from_args(args): return SnapshotStatus.SNAPSHOT_OK, "", args_json -def snapshot_cmd(args, snapset_dict): - logger.info( - "snapshot_cmd: %s %s %s %s %s %s %s", - args.operation, - args.required_space, - args.volume_group, - args.logical_volume, - args.suffix, - args.set_json, - args.check_mode, - ) +def snapshot_cmd(module_args, snapset_dict): + logger.info("snapshot_cmd: %s ", snapset_dict) - rc, message, changed = snapshot_set(snapset_dict, args.check_mode) + rc, message, changed = snapshot_set(snapset_dict, module_args["ansible_check_mode"]) return {"return_code": rc, "errors": [message], "changed": changed} -def check_cmd(args, snapset_dict): - logger.info( - "check_cmd: %s %s %s %s %s %d %s", - args.operation, - args.required_space, - args.volume_group, - args.logical_volume, - args.suffix, - args.verify, - args.set_json, - ) +def check_cmd(module_args, snapset_dict): + logger.info("check_cmd: %s", snapset_dict) - if args.verify: + if module_args["snapshot_lvm_verify_only"]: rc, message = check_verify_lvs_set(snapset_dict) else: rc, message, _current_space_dict = snapshot_precheck_lv_set(snapset_dict) @@ -2070,401 +2214,198 @@ def check_cmd(args, snapset_dict): return {"return_code": rc, "errors": [message], "changed": False} -def remove_cmd(args, snapset_dict): - logger.info( - "remove_cmd: %s %s %s %s %d %s", - args.operation, - args.volume_group, - args.logical_volume, - args.suffix, - args.verify, - args.set_json, - ) +def remove_cmd(module_args, snapset_dict): + logger.info("remove_cmd: %s ", snapset_dict) + changed = False - if args.verify: + if module_args["snapshot_lvm_verify_only"]: rc, message = remove_verify_snapshot_set(snapset_dict) else: - rc, message, changed = remove_snapshot_set(snapset_dict, args.check_mode) + rc, message, changed = remove_snapshot_set( + snapset_dict, module_args["ansible_check_mode"] + ) return {"return_code": rc, "errors": [message], "changed": changed} -def revert_cmd(args, snapset_dict): +def revert_cmd(module_args, snapset_dict): logger.info( - "revert_cmd: %s %s %s %s %d %s", - args.operation, - args.volume_group, - args.logical_volume, - args.suffix, - args.verify, - args.set_json, + "revert_cmd: %s %d", snapset_dict, module_args["snapshot_lvm_verify_only"] ) + changed = False - if args.verify: + if module_args["snapshot_lvm_verify_only"]: # revert re-uses the remove verify since both commands should # cause the snapshot to no longer exist rc, message = remove_verify_snapshot_set(snapset_dict) else: - rc, message, changed = revert_snapshot_set(snapset_dict, args.check_mode) + rc, message, changed = revert_snapshot_set( + snapset_dict, module_args["ansible_check_mode"] + ) return {"return_code": rc, "errors": [message], "changed": changed} -def extend_cmd(args, snapset_dict): +def extend_cmd(module_args, snapset_dict): logger.info( - "extend_cmd: %s %s %s %s %d %s", - args.operation, - args.volume_group, - args.logical_volume, - args.suffix, - args.verify, - args.set_json, + "extend_cmd: %s %d", snapset_dict, module_args["snapshot_lvm_verify_only"] ) + changed = False - if args.verify: + if module_args["snapshot_lvm_verify_only"]: rc, message = extend_verify_snapshot_set(snapset_dict) else: - rc, message, changed = extend_snapshot_set(snapset_dict, args.check_mode) + rc, message, changed = extend_snapshot_set( + snapset_dict, module_args["ansible_check_mode"] + ) return {"return_code": rc, "errors": [message], "changed": changed} -def list_cmd(args, snapset_dict): +def list_cmd(module_args, vg_include): logger.info( - "list_cmd: %d %s %s %s %s %s", - args.all, - args.operation, - args.volume_group, - args.logical_volume, - args.suffix, - args.set_json, + "list_cmd: %s %s", + module_args["snapshot_lvm_vg"], + module_args["snapshot_lvm_lv"], ) - rc, data = lvm_list_json(args.volume_group, args.logical_volume) + rc, data = lvm_list_json( + module_args["snapshot_lvm_vg"], module_args["snapshot_lvm_lv"], vg_include + ) return {"return_code": rc, "errors": [], "data": data, "changed": False} -def mount_cmd(args, snapset_dict): +def mount_cmd(module_args, snapset_dict): logger.info( - "mount_cmd: %d %d %d %s %s %s %s %s %s %s %s", - args.create, - args.origin, - args.verify, - args.mountpoint, - args.fstype, - args.blockdev, - args.options, - args.suffix, - args.logical_volume, - args.volume_group, - args.set_json, + "mount_cmd: %d %d %d %s ", + module_args["snapshot_lvm_verify_only"], + module_args["snapshot_lvm_mountpoint_create"], + module_args["ansible_check_mode"], + snapset_dict, ) rc, message, changed = mount_snapshot_set( - snapset_dict, args.verify, args.create, args.check_mode + snapset_dict, + module_args["snapshot_lvm_verify_only"], + module_args["snapshot_lvm_mountpoint_create"], + module_args["ansible_check_mode"], ) return {"return_code": rc, "errors": [message], "changed": changed} -def umount_cmd(args, snapset_dict): +def umount_cmd(module_args, snapset_dict): logger.info( - "umount_cmd: %d %s %s %s %s", - args.all_targets, - args.mountpoint, - args.logical_volume, - args.volume_group, - args.set_json, + "umount_cmd: %d %s %s", + module_args["ansible_check_mode"], + module_args["snapshot_lvm_mountpoint"], + snapset_dict, ) rc, message, changed = umount_snapshot_set( - snapset_dict, args.verify, args.check_mode + snapset_dict, + module_args["snapshot_lvm_verify_only"], + module_args["ansible_check_mode"], ) return {"return_code": rc, "errors": [message], "changed": changed} -if __name__ == "__main__": - set_up_logging() - - # Ensure that we get consistent output for parsing stdout/stderr and that we - # are using the lvmdbusd profile. - os.environ["LC_ALL"] = "C" - os.environ["LVM_COMMAND_PROFILE"] = "lvmdbusd" - - common_parser = argparse.ArgumentParser(add_help=False) - # arguments common to most operations - common_parser.add_argument( - "-a", - "--all", - action="store_true", - default=False, - dest="all", - help="snapshot all VGs and LVs", - ) - common_parser.add_argument( - "-s", - "--snapset", - dest="suffix", - type=str, - help="name for snapshot set", - ) - common_parser.add_argument( - "-p", - "--prefix", - dest="prefix", - type=str, - help="prefix to add to volume name for snapshot", - ) - common_parser.add_argument( - "--vg-include", - dest="vg_include", - type=str, - help=( - "Used with --all - only include vgs whose names match the given" - "pattern. Uses python re.search to match." - ), - ) - common_parser.add_argument( - "--check-mode", - action="store_true", - default=False, - dest="check_mode", - help="Are we running in Ansible check-mode?", +def run_module(): + logger.info("run_module()") + # define available arguments/parameters a user can pass to the module + # available arguments/parameters that a user can pass + module_args = dict( + snapshot_lvm_action=dict(type="str"), + ansible_check_mode=dict(type=bool), + snapshot_lvm_all_vgs=dict(type=bool), + snapshot_lvm_verify_only=dict(type=bool), + snapshot_lvm_mount_origin=dict(type=bool), + snapshot_lvm_mountpoint_create=dict(type=bool), + snapshot_lvm_unmount_all=dict(type=bool), + snapshot_lvm_percent_space_required=dict(type="str"), + snapshot_lvm_vg=dict(type="str"), + snapshot_lvm_lv=dict(type="str"), + snapshot_lvm_snapset_name=dict(type="str"), + snapshot_lvm_mount_options=dict(type="str"), + snapshot_lvm_mountpoint=dict(type="str"), + snapshot_lvm_set=dict(type="str"), + snapshot_lvm_fstype=dict(type="str"), + snapshot_lvm_vg_include=dict(type="str"), ) - # Group parser - group_parser = argparse.ArgumentParser(add_help=False) - group_parser.add_argument( - "-g", - "--group", - nargs="?", - action="store", - required=False, - default=None, - dest="set_json", - ) + result = dict(changed=False, return_code="", message="") - # LVM VG/LV parser - lvm_parser = argparse.ArgumentParser(add_help=False) - lvm_parser.add_argument( - "-vg", - "--volumegroup", - nargs="?", - action="store", - default=None, - dest="volume_group", - help="volume group to snapshot", - ) - lvm_parser.add_argument( - "-lv", - "--logicalvolume", - nargs="?", - action="store", - default=None, - dest="logical_volume", - help="logical volume to snapshot", - ) + module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) - # arguments for operations that do verify - verify_parser = argparse.ArgumentParser(add_help=False) - verify_parser.add_argument( - "-v", - "--verify", - action="store_true", - default=False, - dest="verify", - help="verify VGs and LVs have snapshots", - ) + logger.info("module params: %s", module.params) - # arguments for operations that deal with required space - req_space_parser = argparse.ArgumentParser(add_help=False) - # TODO: range check required space - setting choices to a range makes the help ugly. - req_space_parser.add_argument( - "-r", - "--requiredspace", - dest="required_space", - required=False, - type=int, # choices=range(10,100) - default=20, - help="percent of required space in the volume group to be reserved for snapshot", - ) + cmd = get_command_const(module.params["snapshot_lvm_action"]) - # arguments for operations that deal with mount of filesytems - mountpoint_parser = argparse.ArgumentParser(add_help=False) - mountpoint_parser.add_argument( - "-m", - "--mountpoint", - dest="mountpoint", - required=False, - type=str, - help="mount point for block device", - ) + if cmd == SnapshotCommand.INVALID: + result["message"] = "Invalid command: " + module.params["snapshot_lvm_action"] + module.exit_json(**result) - # arguments for operations that deal with mount of filesytems - mount_parser = argparse.ArgumentParser(add_help=False) - mount_parser.add_argument( - "-b", - "--blockdev", - dest="blockdev", - required=False, - type=str, - help="mount point for block device", - ) - mount_parser.add_argument( - "-t", - "--type", - dest="fstype", - required=False, - default="", - type=str, - help="filesystem type", - ) - mount_parser.add_argument( - "-o", - "--options", - dest="options", - required=False, - type=str, - help="mount options", - ) - mount_parser.add_argument( - "-c", - "--create", - action="store_true", - default=False, - dest="create", - help="create the directory for the mount point if it doesn't already exist", - ) - mount_parser.add_argument( - "-O", - "--origin", - action="store_true", - default=False, - dest="origin", - help="mount the origin", - ) - parser = argparse.ArgumentParser(description="Snapshot Operations") - - # sub-parsers - subparsers = parser.add_subparsers(dest="operation", help="Available operations") + if module.params["snapshot_lvm_vg_include"]: + VG_INCLUDE = re.compile(module.params["snapshot_lvm_vg_include"]) - # sub-parser for 'snapshot' - snapshot_parser = subparsers.add_parser( - SnapshotCommand.SNAPSHOT, - help="Snapshot given VG/LVs", - parents=[common_parser, lvm_parser, group_parser, req_space_parser], - ) - snapshot_parser.set_defaults(func=snapshot_cmd) - - # sub-parser for 'check' - check_parser = subparsers.add_parser( - SnapshotCommand.CHECK, - help="Check space for given VG/LV", - parents=[ - common_parser, - lvm_parser, - group_parser, - req_space_parser, - verify_parser, - ], - ) - check_parser.set_defaults(func=check_cmd) - - # sub-parser for 'remove' - remove_parser = subparsers.add_parser( - SnapshotCommand.REMOVE, - help="Remove snapshots", - parents=[common_parser, group_parser, lvm_parser, verify_parser], - ) - remove_parser.set_defaults(func=remove_cmd) + if module.params["snapshot_lvm_set"]: + cmd_result, snapset_dict = validate_snapset_json( + get_command_const(module.params["snapshot_lvm_action"]), + module.params["snapshot_lvm_set"].replace("'", '"'), + False, + ) + else: + cmd_result, snapset_dict = validate_snapset_args(cmd, module.params) + + if cmd_result["return_code"] == SnapshotStatus.SNAPSHOT_OK: + if cmd == SnapshotCommand.SNAPSHOT: + cmd_result = snapshot_cmd(module.params, snapset_dict) + elif cmd == SnapshotCommand.CHECK: + cmd_result = check_cmd(module.params, snapset_dict) + elif cmd == SnapshotCommand.REMOVE: + cmd_result = remove_cmd(module.params, snapset_dict) + elif cmd == SnapshotCommand.REVERT: + cmd_result = revert_cmd(module.params, snapset_dict) + elif cmd == SnapshotCommand.EXTEND: + cmd_result = extend_cmd(module.params, snapset_dict) + elif cmd == SnapshotCommand.LIST: + cmd_result = list_cmd(module.params, vg_include) + elif cmd == SnapshotCommand.MOUNT: + cmd_result = mount_cmd(module.params, snapset_dict) + elif cmd == SnapshotCommand.UMOUNT: + cmd_result = umount_cmd(module.params, snapset_dict) + + logger.info("cmd_result: %s", cmd_result) + + result["errors"] = cmd_result["errors"] + result["msg"] = cmd_result["errors"] + result["return_code"] = cmd_result["return_code"] + result["changed"] = cmd_result["changed"] + if "data" in cmd_result: + result["data"] = cmd_result["data"] + + logger.info("result: %s", result) - # sub-parser for 'revert' - revert_parser = subparsers.add_parser( - SnapshotCommand.REVERT, - help="Revert to snapshots", - parents=[common_parser, group_parser, lvm_parser, verify_parser], - ) - revert_parser.set_defaults(func=revert_cmd) - - # sub-parser for 'extend' - extend_parser = subparsers.add_parser( - SnapshotCommand.EXTEND, - help="Extend given LVs", - parents=[ - common_parser, - group_parser, - lvm_parser, - verify_parser, - req_space_parser, - ], - ) - extend_parser.set_defaults(func=extend_cmd) + if result["return_code"] == SnapshotStatus.SNAPSHOT_OK: + module.exit_json(**result) + else: + module.fail_json(**result) - # sub-parser for 'list' - list_parser = subparsers.add_parser( - SnapshotCommand.LIST, - help="List snapshots", - parents=[common_parser, group_parser, lvm_parser], - ) - list_parser.set_defaults(func=list_cmd) - - # sub-parser for 'mount' - mount_parser = subparsers.add_parser( - SnapshotCommand.MOUNT, - help="mount filesystems", - parents=[ - common_parser, - mountpoint_parser, - mount_parser, - group_parser, - lvm_parser, - verify_parser, - ], - ) - mount_parser.set_defaults(func=mount_cmd) - - # sub-parser for 'umount' - umount_parser = subparsers.add_parser( - SnapshotCommand.UMOUNT, - help="umount filesystems", - parents=[ - common_parser, - mountpoint_parser, - group_parser, - lvm_parser, - verify_parser, - ], - ) - umount_parser.add_argument( - "-A", - "--all-targets", - action="store_true", - default=True, - dest="all_targets", - help="unmount all mountpoints for the given device", - ) - umount_parser.set_defaults(func=umount_cmd) - args = parser.parse_args() - if args.vg_include: - VG_INCLUDE = re.compile(args.vg_include) - else: - VG_INCLUDE = None +def main(): + set_up_logging() - if args.set_json: - result, snapset_dict = validate_snapset_json( - get_command_const(args.operation), args.set_json, False - ) - else: - result, snapset_dict = validate_snapset_args(args) + # Ensure that we get consistent output for parsing stdout/stderr and that we + # are using the lvmdbusd profile. + os.environ["LC_ALL"] = "C" + os.environ["LVM_COMMAND_PROFILE"] = "lvmdbusd" + run_module() - if result["return_code"] == SnapshotStatus.SNAPSHOT_OK: - result = args.func(args, snapset_dict) - print_result(result) - sys.exit(result["return_code"]) +if __name__ == "__main__": + main() diff --git a/tasks/main.yml b/tasks/main.yml index b61e203..e7989b8 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -10,20 +10,29 @@ use: "{{ (__snapshot_is_ostree | d(false)) | ternary('ansible.posix.rhel_rpm_ostree', omit) }}" -- name: Show snapshot command - debug: - var: __snapshot_cmd - verbosity: 2 - -- name: Run snapshot command and handle errors +- name: Run snapshot module and handle errors block: - - name: Run snapshot command {{ snapshot_lvm_action }} - ansible.builtin.script: "{{ __snapshot_cmd }}" - args: - executable: "{{ __snapshot_python }}" + - name: Run snapshot module {{ snapshot_lvm_action }} + snapshot: + snapshot_lvm_action: "{{ snapshot_lvm_action }}" + ansible_check_mode: "{{ ansible_check_mode | d(false) }}" + snapshot_lvm_all_vgs: "{{ snapshot_lvm_all_vgs | d(false) }}" + snapshot_lvm_verify_only: "{{ snapshot_lvm_verify_only | d(false) }}" + snapshot_lvm_mount_origin: "{{ snapshot_lvm_mount_origin | d(false) }}" + snapshot_lvm_mountpoint_create: + "{{ snapshot_lvm_mountpoint_create | d(false) }}" + snapshot_lvm_unmount_all: "{{ snapshot_lvm_unmount_all | d(false) }}" + snapshot_lvm_percent_space_required: + "{{ snapshot_lvm_percent_space_required | d(omit) }}" + snapshot_lvm_vg: "{{ snapshot_lvm_vg | d(omit) }}" + snapshot_lvm_lv: "{{ snapshot_lvm_lv | d(omit) }}" + snapshot_lvm_snapset_name: "{{ snapshot_lvm_snapset_name | d(omit) }}" + snapshot_lvm_mount_options: "{{ snapshot_lvm_mount_options | d(omit) }}" + snapshot_lvm_fstype: "{{ snapshot_lvm_fstype | d(omit) }}" + snapshot_lvm_mountpoint: "{{ snapshot_lvm_mountpoint | d(omit) }}" + snapshot_lvm_set: "{{ snapshot_lvm_set | d(omit) }}" + snapshot_lvm_vg_include: "{{ snapshot_lvm_vg_include | d(false) }}" register: snapshot_cmd_raw - no_log: true - changed_when: false # change handled below too rescue: - name: Raise error @@ -36,16 +45,13 @@ var: snapshot_cmd_raw verbosity: 2 - - name: Parse raw output + - name: Set result set_fact: - snapshot_cmd: "{{ snapshot_cmd_raw.stdout_lines | - reject('search', ': warning: setlocale: LC_ALL: cannot change locale') | - join('') | from_json }}" - changed_when: snapshot_cmd["changed"] + snapshot_cmd: "{{ snapshot_cmd_raw }}" - name: Set snapshot_facts to the JSON results set_fact: - snapshot_facts: "{{ snapshot_cmd['data'] }}" + snapshot_facts: "{{ snapshot_cmd_raw['data'] }}" when: snapshot_lvm_action == "list" - name: Show errors diff --git a/tests/roles/linux-system-roles.snapshot/library b/tests/roles/linux-system-roles.snapshot/library new file mode 120000 index 0000000..ba40d2f --- /dev/null +++ b/tests/roles/linux-system-roles.snapshot/library @@ -0,0 +1 @@ +../../../library \ No newline at end of file diff --git a/vars/main.yml b/vars/main.yml index 110f676..205eb6f 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -15,35 +15,3 @@ __snapshot_required_facts: # the 'gather_subset' parameter of the 'setup' module __snapshot_required_facts_subsets: "{{ ['!all', '!min'] + __snapshot_required_facts }}" - -__snapshot_cmd: "{{ 'snapshot.py ' ~ snapshot_lvm_action ~ ' ' ~ - ('--check-mode ' if ansible_check_mode else '') ~ - ('-a ' if snapshot_lvm_all_vgs else '') ~ - ('-v ' if snapshot_lvm_verify_only else '') ~ - ('-O ' if snapshot_lvm_mount_origin else '') ~ - ('-c ' if snapshot_lvm_mountpoint_create else '') ~ - ('-A ' if snapshot_lvm_unmount_all else '') ~ - ('-r ' if snapshot_lvm_percent_space_required else '') ~ ' ' ~ - (snapshot_lvm_percent_space_required | string | quote - if snapshot_lvm_percent_space_required else '') ~ ' ' ~ - ('-vg ' if snapshot_lvm_vg else '') ~ ' ' ~ - (snapshot_lvm_vg | quote - if snapshot_lvm_vg else '') ~ ' ' ~ - ('-lv ' if snapshot_lvm_lv else '') ~ ' ' ~ - (snapshot_lvm_lv | quote - if snapshot_lvm_lv else '') ~ ' ' ~ - ('-s ' if snapshot_lvm_snapset_name else '') ~ ' ' ~ - (snapshot_lvm_snapset_name | quote - if snapshot_lvm_snapset_name else '') ~ ' ' ~ - ('-o ' if snapshot_lvm_mount_options else '') ~ ' ' ~ - (snapshot_lvm_mount_options | quote - if snapshot_lvm_mount_options else '') ~ ' ' ~ - ('-m ' if snapshot_lvm_mountpoint else '') ~ ' ' ~ - (snapshot_lvm_mountpoint | quote - if snapshot_lvm_mountpoint else '') ~ ' ' ~ - ('-g ' if snapshot_lvm_set else '') ~ ' ' ~ - (snapshot_lvm_set | to_json | quote - if snapshot_lvm_set else '') ~ - ('--vg-include ' if snapshot_lvm_vg_include else '') ~ ' ' ~ - (snapshot_lvm_vg_include | quote - if snapshot_lvm_vg_include else '') }}"