mirror of
				https://github.com/ansible-collections/community.general.git
				synced 2025-10-26 05:50:36 -07:00 
			
		
		
		
	Use semantic markup (modules a-c) (#6671)
* Use semantic markup.
* E() now works better.
(cherry picked from commit 6fc1df9b83)
Co-authored-by: Felix Fontein <felix@fontein.de>
		
	
			
		
			
				
	
	
		
			682 lines
		
	
	
	
		
			28 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			682 lines
		
	
	
	
		
			28 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/python
 | |
| 
 | |
| # Copyright (c) 2022, Gregory Furlong <gnfzdz@fzdz.io>
 | |
| # GNU General Public License v3.0+ (see COPYING 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: btrfs_subvolume
 | |
| short_description: Manage btrfs subvolumes
 | |
| version_added: "6.6.0"
 | |
| 
 | |
| description: Creates, updates and deletes btrfs subvolumes and snapshots.
 | |
| 
 | |
| options:
 | |
|     automount:
 | |
|         description:
 | |
|         - Allow the module to temporarily mount the targeted btrfs filesystem in order to validate the current state and make any required changes.
 | |
|         type: bool
 | |
|         default: false
 | |
|     default:
 | |
|         description:
 | |
|         - Make the subvolume specified by O(name) the filesystem's default subvolume.
 | |
|         type: bool
 | |
|         default: false
 | |
|     filesystem_device:
 | |
|         description:
 | |
|         - A block device contained within the btrfs filesystem to be targeted.
 | |
|         - Useful when multiple btrfs filesystems are present to specify which filesystem should be targeted.
 | |
|         type: path
 | |
|     filesystem_label:
 | |
|         description:
 | |
|         - A descriptive label assigned to the btrfs filesystem to be targeted.
 | |
|         - Useful when multiple btrfs filesystems are present to specify which filesystem should be targeted.
 | |
|         type: str
 | |
|     filesystem_uuid:
 | |
|         description:
 | |
|         - A unique identifier assigned to the btrfs filesystem to be targeted.
 | |
|         - Useful when multiple btrfs filesystems are present to specify which filesystem should be targeted.
 | |
|         type: str
 | |
|     name:
 | |
|         description:
 | |
|         - Name of the subvolume/snapshot to be targeted.
 | |
|         required: true
 | |
|         type: str
 | |
|     recursive:
 | |
|         description:
 | |
|         - When true, indicates that parent/child subvolumes should be created/removedas necessary
 | |
|           to complete the operation (for O(state=present) and O(state=absent) respectively).
 | |
|         type: bool
 | |
|         default: false
 | |
|     snapshot_source:
 | |
|         description:
 | |
|         - Identifies the source subvolume for the created snapshot.
 | |
|         - Infers that the created subvolume is a snapshot.
 | |
|         type: str
 | |
|     snapshot_conflict:
 | |
|         description:
 | |
|         - Policy defining behavior when a subvolume already exists at the path of the requested snapshot.
 | |
|         - V(skip) - Create a snapshot only if a subvolume does not yet exist at the target location, otherwise indicate that no change is required.
 | |
|           Warning, this option does not yet verify that the target subvolume was generated from a snapshot of the requested source.
 | |
|         - V(clobber) - If a subvolume already exists at the requested location, delete it first.
 | |
|           This option is not idempotent and will result in a new snapshot being generated on every execution.
 | |
|         - V(error) - If a subvolume already exists at the requested location, return an error.
 | |
|           This option is not idempotent and will result in an error on replay of the module.
 | |
|         type: str
 | |
|         choices: [ skip, clobber, error ]
 | |
|         default: skip
 | |
|     state:
 | |
|         description:
 | |
|             - Indicates the current state of the targeted subvolume.
 | |
|         type: str
 | |
|         choices: [ absent, present ]
 | |
|         default: present
 | |
| 
 | |
| notes:
 | |
|   - If any or all of the options O(filesystem_device), O(filesystem_label) or O(filesystem_uuid) parameters are provided, there is expected
 | |
|     to be a matching btrfs filesystem. If none are provided and only a single btrfs filesystem exists or only a single
 | |
|     btrfs filesystem is mounted, that filesystem will be used; otherwise, the module will take no action and return an error.
 | |
| 
 | |
| extends_documentation_fragment:
 | |
|   - community.general.attributes
 | |
| 
 | |
| attributes:
 | |
|   check_mode:
 | |
|     support: partial
 | |
|     details:
 | |
|       - In some scenarios it may erroneously report intermediate subvolumes being created.
 | |
|         After mounting, if a directory like file is found where the subvolume would have been created, the operation is skipped.
 | |
|   diff_mode:
 | |
|     support: none
 | |
| 
 | |
| author:
 | |
|     - Gregory Furlong (@gnfzdz)
 | |
| '''
 | |
| 
 | |
| EXAMPLES = r'''
 | |
| 
 | |
| - name: Create a @home subvolume under the root subvolume
 | |
|   community.general.btrfs_subvolume:
 | |
|     name: /@home
 | |
|     device: /dev/vda2
 | |
| 
 | |
| - name: Remove the @home subvolume if it exists
 | |
|   community.general.btrfs_subvolume:
 | |
|     name: /@home
 | |
|     state: absent
 | |
|     device: /dev/vda2
 | |
| 
 | |
| - name: Create a snapshot of the root subvolume named @
 | |
|   community.general.btrfs_subvolume:
 | |
|     name: /@
 | |
|     snapshot_source: /
 | |
|     device: /dev/vda2
 | |
| 
 | |
| - name: Create a snapshot of the root subvolume and make it the new default subvolume
 | |
|   community.general.btrfs_subvolume:
 | |
|     name: /@
 | |
|     snapshot_source: /
 | |
|     default: Yes
 | |
|     device: /dev/vda2
 | |
| 
 | |
| - name: Create a snapshot of the /@ subvolume and recursively creating intermediate subvolumes as required
 | |
|   community.general.btrfs_subvolume:
 | |
|     name: /@snapshots/@2022_06_09
 | |
|     snapshot_source: /@
 | |
|     recursive: True
 | |
|     device: /dev/vda2
 | |
| 
 | |
| - name: Remove the /@ subvolume and recursively delete child subvolumes as required
 | |
|   community.general.btrfs_subvolume:
 | |
|     name: /@snapshots/@2022_06_09
 | |
|     snapshot_source: /@
 | |
|     recursive: True
 | |
|     device: /dev/vda2
 | |
| 
 | |
| '''
 | |
| 
 | |
| RETURN = r'''
 | |
| 
 | |
| filesystem:
 | |
|     description:
 | |
|     - A summary of the final state of the targeted btrfs filesystem.
 | |
|     type: dict
 | |
|     returned: success
 | |
|     contains:
 | |
|         uuid:
 | |
|             description: A unique identifier assigned to the filesystem.
 | |
|             returned: success
 | |
|             type: str
 | |
|             sample: 96c9c605-1454-49b8-a63a-15e2584c208e
 | |
|         label:
 | |
|             description: An optional label assigned to the filesystem.
 | |
|             returned: success
 | |
|             type: str
 | |
|             sample: Tank
 | |
|         devices:
 | |
|             description: A list of devices assigned to the filesystem.
 | |
|             returned: success
 | |
|             type: list
 | |
|             sample:
 | |
|                 - /dev/sda1
 | |
|                 - /dev/sdb1
 | |
|         default_subvolume:
 | |
|             description: The ID of the filesystem's default subvolume.
 | |
|             returned: success and if filesystem is mounted
 | |
|             type: int
 | |
|             sample: 5
 | |
|         subvolumes:
 | |
|             description: A list of dicts containing metadata for all of the filesystem's subvolumes.
 | |
|             returned: success and if filesystem is mounted
 | |
|             type: list
 | |
|             elements: dict
 | |
|             contains:
 | |
|                 id:
 | |
|                     description: An identifier assigned to the subvolume, unique within the containing filesystem.
 | |
|                     type: int
 | |
|                     sample: 256
 | |
|                 mountpoints:
 | |
|                     description: Paths where the subvolume is mounted on the targeted host.
 | |
|                     type: list
 | |
|                     sample: ['/home']
 | |
|                 parent:
 | |
|                     description: The identifier of this subvolume's parent.
 | |
|                     type: int
 | |
|                     sample: 5
 | |
|                 path:
 | |
|                     description: The full path of the subvolume relative to the btrfs fileystem's root.
 | |
|                     type: str
 | |
|                     sample: /@home
 | |
| 
 | |
| modifications:
 | |
|     description:
 | |
|     - A list where each element describes a change made to the target btrfs filesystem.
 | |
|     type: list
 | |
|     returned: Success
 | |
|     elements: str
 | |
| 
 | |
| target_subvolume_id:
 | |
|     description:
 | |
|     - The ID of the subvolume specified with the O(name) parameter, either pre-existing or created as part of module execution.
 | |
|     type: int
 | |
|     sample: 257
 | |
|     returned: Success and subvolume exists after module execution
 | |
| '''
 | |
| 
 | |
| from ansible_collections.community.general.plugins.module_utils.btrfs import BtrfsFilesystemsProvider, BtrfsCommands, BtrfsModuleException
 | |
| from ansible_collections.community.general.plugins.module_utils.btrfs import normalize_subvolume_path
 | |
| from ansible.module_utils.basic import AnsibleModule
 | |
| import os
 | |
| import tempfile
 | |
| 
 | |
| 
 | |
| class BtrfsSubvolumeModule(object):
 | |
| 
 | |
|     __BTRFS_ROOT_SUBVOLUME = '/'
 | |
|     __BTRFS_ROOT_SUBVOLUME_ID = 5
 | |
|     __BTRFS_SUBVOLUME_INODE_NUMBER = 256
 | |
| 
 | |
|     __CREATE_SUBVOLUME_OPERATION = 'create'
 | |
|     __CREATE_SNAPSHOT_OPERATION = 'snapshot'
 | |
|     __DELETE_SUBVOLUME_OPERATION = 'delete'
 | |
|     __SET_DEFAULT_SUBVOLUME_OPERATION = 'set-default'
 | |
| 
 | |
|     __UNKNOWN_SUBVOLUME_ID = '?'
 | |
| 
 | |
|     def __init__(self, module):
 | |
|         self.module = module
 | |
|         self.__btrfs_api = BtrfsCommands(module)
 | |
|         self.__provider = BtrfsFilesystemsProvider(module)
 | |
| 
 | |
|         #  module parameters
 | |
|         name = self.module.params['name']
 | |
|         self.__name = normalize_subvolume_path(name) if name is not None else None
 | |
|         self.__state = self.module.params['state']
 | |
| 
 | |
|         self.__automount = self.module.params['automount']
 | |
|         self.__default = self.module.params['default']
 | |
|         self.__filesystem_device = self.module.params['filesystem_device']
 | |
|         self.__filesystem_label = self.module.params['filesystem_label']
 | |
|         self.__filesystem_uuid = self.module.params['filesystem_uuid']
 | |
|         self.__recursive = self.module.params['recursive']
 | |
|         self.__snapshot_conflict = self.module.params['snapshot_conflict']
 | |
|         snapshot_source = self.module.params['snapshot_source']
 | |
|         self.__snapshot_source = normalize_subvolume_path(snapshot_source) if snapshot_source is not None else None
 | |
| 
 | |
|         # execution state
 | |
|         self.__filesystem = None
 | |
|         self.__required_mounts = []
 | |
|         self.__unit_of_work = []
 | |
|         self.__completed_work = []
 | |
|         self.__temporary_mounts = dict()
 | |
| 
 | |
|     def run(self):
 | |
|         error = None
 | |
|         try:
 | |
|             self.__load_filesystem()
 | |
|             self.__prepare_unit_of_work()
 | |
| 
 | |
|             if not self.module.check_mode:
 | |
|                 # check required mounts & mount
 | |
|                 if len(self.__unit_of_work) > 0:
 | |
|                     self.__execute_unit_of_work()
 | |
|                     self.__filesystem.refresh()
 | |
|             else:
 | |
|                 # check required mounts
 | |
|                 self.__completed_work.extend(self.__unit_of_work)
 | |
|         except Exception as e:
 | |
|             error = e
 | |
|         finally:
 | |
|             self.__cleanup_mounts()
 | |
|             if self.__filesystem is not None:
 | |
|                 self.__filesystem.refresh_mountpoints()
 | |
| 
 | |
|         return (error, self.get_results())
 | |
| 
 | |
|     # Identify the targeted filesystem and obtain the current state
 | |
|     def __load_filesystem(self):
 | |
|         if self.__has_filesystem_criteria():
 | |
|             filesystem = self.__find_matching_filesytem()
 | |
|         else:
 | |
|             filesystem = self.__find_default_filesystem()
 | |
| 
 | |
|         # The filesystem must be mounted to obtain the current state (subvolumes, default, etc)
 | |
|         if not filesystem.is_mounted():
 | |
|             if not self.__automount:
 | |
|                 raise BtrfsModuleException(
 | |
|                     "Target filesystem uuid=%s is not currently mounted and automount=False."
 | |
|                     "Mount explicitly before module execution or pass automount=True" % filesystem.uuid)
 | |
|             elif self.module.check_mode:
 | |
|                 # TODO is failing the module an appropriate outcome in this scenario?
 | |
|                 raise BtrfsModuleException(
 | |
|                     "Target filesystem uuid=%s is not currently mounted. Unable to validate the current"
 | |
|                     "state while running with check_mode=True" % filesystem.uuid)
 | |
|             else:
 | |
|                 self.__mount_subvolume_id_to_tempdir(filesystem, self.__BTRFS_ROOT_SUBVOLUME_ID)
 | |
|                 filesystem.refresh()
 | |
|         self.__filesystem = filesystem
 | |
| 
 | |
|     def __has_filesystem_criteria(self):
 | |
|         return self.__filesystem_uuid is not None or self.__filesystem_label is not None or self.__filesystem_device is not None
 | |
| 
 | |
|     def __find_matching_filesytem(self):
 | |
|         criteria = {
 | |
|             'uuid': self.__filesystem_uuid,
 | |
|             'label': self.__filesystem_label,
 | |
|             'device': self.__filesystem_device,
 | |
|         }
 | |
|         return self.__provider.get_matching_filesystem(criteria)
 | |
| 
 | |
|     def __find_default_filesystem(self):
 | |
|         filesystems = self.__provider.get_filesystems()
 | |
|         filesystem = None
 | |
| 
 | |
|         if len(filesystems) == 1:
 | |
|             filesystem = filesystems[0]
 | |
|         else:
 | |
|             mounted_filesystems = [x for x in filesystems if x.is_mounted()]
 | |
|             if len(mounted_filesystems) == 1:
 | |
|                 filesystem = mounted_filesystems[0]
 | |
| 
 | |
|         if filesystem is not None:
 | |
|             return filesystem
 | |
|         else:
 | |
|             raise BtrfsModuleException(
 | |
|                 "Failed to automatically identify targeted filesystem. "
 | |
|                 "No explicit device indicated and found %d available filesystems." % len(filesystems)
 | |
|             )
 | |
| 
 | |
|     # Prepare unit of work
 | |
|     def __prepare_unit_of_work(self):
 | |
|         if self.__state == "present":
 | |
|             if self.__snapshot_source is None:
 | |
|                 self.__prepare_subvolume_present()
 | |
|             else:
 | |
|                 self.__prepare_snapshot_present()
 | |
| 
 | |
|             if self.__default:
 | |
|                 self.__prepare_set_default()
 | |
|         elif self.__state == "absent":
 | |
|             self.__prepare_subvolume_absent()
 | |
| 
 | |
|     def __prepare_subvolume_present(self):
 | |
|         subvolume = self.__filesystem.get_subvolume_by_name(self.__name)
 | |
|         if subvolume is None:
 | |
|             self.__prepare_before_create_subvolume(self.__name)
 | |
|             self.__stage_create_subvolume(self.__name)
 | |
| 
 | |
|     def __prepare_before_create_subvolume(self, subvolume_name):
 | |
|         closest_parent = self.__filesystem.get_nearest_subvolume(subvolume_name)
 | |
|         self.__stage_required_mount(closest_parent)
 | |
|         if self.__recursive:
 | |
|             self.__prepare_create_intermediates(closest_parent, subvolume_name)
 | |
| 
 | |
|     def __prepare_create_intermediates(self, closest_subvolume, subvolume_name):
 | |
|         relative_path = closest_subvolume.get_child_relative_path(self.__name)
 | |
|         missing_subvolumes = [x for x in relative_path.split(os.path.sep) if len(x) > 0]
 | |
|         if len(missing_subvolumes) > 1:
 | |
|             current = closest_subvolume.path
 | |
|             for s in missing_subvolumes[:-1]:
 | |
|                 separator = os.path.sep if current[-1] != os.path.sep else ""
 | |
|                 current = current + separator + s
 | |
|                 self.__stage_create_subvolume(current, True)
 | |
| 
 | |
|     def __prepare_snapshot_present(self):
 | |
|         source_subvolume = self.__filesystem.get_subvolume_by_name(self.__snapshot_source)
 | |
|         subvolume = self.__filesystem.get_subvolume_by_name(self.__name)
 | |
|         subvolume_exists = subvolume is not None
 | |
| 
 | |
|         if subvolume_exists:
 | |
|             if self.__snapshot_conflict == "skip":
 | |
|                 # No change required
 | |
|                 return
 | |
|             elif self.__snapshot_conflict == "error":
 | |
|                 raise BtrfsModuleException("Target subvolume=%s already exists and snapshot_conflict='error'" % self.__name)
 | |
| 
 | |
|         if source_subvolume is None:
 | |
|             raise BtrfsModuleException("Source subvolume %s does not exist" % self.__snapshot_source)
 | |
|         elif subvolume is not None and source_subvolume.id == subvolume.id:
 | |
|             raise BtrfsModuleException("Snapshot source and target are the same.")
 | |
|         else:
 | |
|             self.__stage_required_mount(source_subvolume)
 | |
| 
 | |
|         if subvolume_exists and self.__snapshot_conflict == "clobber":
 | |
|             self.__prepare_delete_subvolume_tree(subvolume)
 | |
|         elif not subvolume_exists:
 | |
|             self.__prepare_before_create_subvolume(self.__name)
 | |
| 
 | |
|         self.__stage_create_snapshot(source_subvolume, self.__name)
 | |
| 
 | |
|     def __prepare_subvolume_absent(self):
 | |
|         subvolume = self.__filesystem.get_subvolume_by_name(self.__name)
 | |
|         if subvolume is not None:
 | |
|             self.__prepare_delete_subvolume_tree(subvolume)
 | |
| 
 | |
|     def __prepare_delete_subvolume_tree(self, subvolume):
 | |
|         if subvolume.is_filesystem_root():
 | |
|             raise BtrfsModuleException("Can not delete the filesystem's root subvolume")
 | |
|         if not self.__recursive and len(subvolume.get_child_subvolumes()) > 0:
 | |
|             raise BtrfsModuleException("Subvolume targeted for deletion %s has children and recursive=False."
 | |
|                                        "Either explicitly delete the child subvolumes first or pass "
 | |
|                                        "parameter recursive=True." % subvolume.path)
 | |
| 
 | |
|         self.__stage_required_mount(subvolume.get_parent_subvolume())
 | |
|         queue = self.__prepare_recursive_delete_order(subvolume) if self.__recursive else [subvolume]
 | |
|         # prepare unit of work
 | |
|         for s in queue:
 | |
|             if s.is_mounted():
 | |
|                 # TODO potentially unmount the subvolume if automount=True ?
 | |
|                 raise BtrfsModuleException("Can not delete mounted subvolume=%s" % s.path)
 | |
|             if s.is_filesystem_default():
 | |
|                 self.__stage_set_default_subvolume(self.__BTRFS_ROOT_SUBVOLUME, self.__BTRFS_ROOT_SUBVOLUME_ID)
 | |
|             self.__stage_delete_subvolume(s)
 | |
| 
 | |
|     def __prepare_recursive_delete_order(self, subvolume):
 | |
|         """Return the subvolume and all descendents as a list, ordered so that descendents always occur before their ancestors"""
 | |
|         pending = [subvolume]
 | |
|         ordered = []
 | |
|         while len(pending) > 0:
 | |
|             next = pending.pop()
 | |
|             ordered.append(next)
 | |
|             pending.extend(next.get_child_subvolumes())
 | |
|         ordered.reverse()  # reverse to ensure children are deleted before their parent
 | |
|         return ordered
 | |
| 
 | |
|     def __prepare_set_default(self):
 | |
|         subvolume = self.__filesystem.get_subvolume_by_name(self.__name)
 | |
|         subvolume_id = subvolume.id if subvolume is not None else None
 | |
| 
 | |
|         if self.__filesystem.default_subvolid != subvolume_id:
 | |
|             self.__stage_set_default_subvolume(self.__name, subvolume_id)
 | |
| 
 | |
|     # Stage operations to the unit of work
 | |
|     def __stage_required_mount(self, subvolume):
 | |
|         if subvolume.get_mounted_path() is None:
 | |
|             if self.__automount:
 | |
|                 self.__required_mounts.append(subvolume)
 | |
|             else:
 | |
|                 raise BtrfsModuleException("The requested changes will require the subvolume '%s' to be mounted, but automount=False" % subvolume.path)
 | |
| 
 | |
|     def __stage_create_subvolume(self, subvolume_path, intermediate=False):
 | |
|         """
 | |
|         Add required creation of an intermediate subvolume to the unit of work
 | |
|         If intermediate is true, the action will be skipped if a directory like file is found at target
 | |
|         after mounting a parent subvolume
 | |
|         """
 | |
|         self.__unit_of_work.append({
 | |
|             'action': self.__CREATE_SUBVOLUME_OPERATION,
 | |
|             'target': subvolume_path,
 | |
|             'intermediate': intermediate,
 | |
|         })
 | |
| 
 | |
|     def __stage_create_snapshot(self, source_subvolume, target_subvolume_path):
 | |
|         """Add creation of a snapshot from source to target to the unit of work"""
 | |
|         self.__unit_of_work.append({
 | |
|             'action': self.__CREATE_SNAPSHOT_OPERATION,
 | |
|             'source': source_subvolume.path,
 | |
|             'source_id': source_subvolume.id,
 | |
|             'target': target_subvolume_path,
 | |
|         })
 | |
| 
 | |
|     def __stage_delete_subvolume(self, subvolume):
 | |
|         """Add deletion of the target subvolume to the unit of work"""
 | |
|         self.__unit_of_work.append({
 | |
|             'action': self.__DELETE_SUBVOLUME_OPERATION,
 | |
|             'target': subvolume.path,
 | |
|             'target_id': subvolume.id,
 | |
|         })
 | |
| 
 | |
|     def __stage_set_default_subvolume(self, subvolume_path, subvolume_id=None):
 | |
|         """Add update of the filesystem's default subvolume to the unit of work"""
 | |
|         self.__unit_of_work.append({
 | |
|             'action': self.__SET_DEFAULT_SUBVOLUME_OPERATION,
 | |
|             'target': subvolume_path,
 | |
|             'target_id': subvolume_id,
 | |
|         })
 | |
| 
 | |
|     # Execute the unit of work
 | |
|     def __execute_unit_of_work(self):
 | |
|         self.__check_required_mounts()
 | |
|         for op in self.__unit_of_work:
 | |
|             if op['action'] == self.__CREATE_SUBVOLUME_OPERATION:
 | |
|                 self.__execute_create_subvolume(op)
 | |
|             elif op['action'] == self.__CREATE_SNAPSHOT_OPERATION:
 | |
|                 self.__execute_create_snapshot(op)
 | |
|             elif op['action'] == self.__DELETE_SUBVOLUME_OPERATION:
 | |
|                 self.__execute_delete_subvolume(op)
 | |
|             elif op['action'] == self.__SET_DEFAULT_SUBVOLUME_OPERATION:
 | |
|                 self.__execute_set_default_subvolume(op)
 | |
|             else:
 | |
|                 raise ValueError("Unknown operation type '%s'" % op['action'])
 | |
| 
 | |
|     def __execute_create_subvolume(self, operation):
 | |
|         target_mounted_path = self.__filesystem.get_mountpath_as_child(operation['target'])
 | |
|         if not self.__is_existing_directory_like(target_mounted_path):
 | |
|             self.__btrfs_api.subvolume_create(target_mounted_path)
 | |
|             self.__completed_work.append(operation)
 | |
| 
 | |
|     def __execute_create_snapshot(self, operation):
 | |
|         source_subvolume = self.__filesystem.get_subvolume_by_name(operation['source'])
 | |
|         source_mounted_path = source_subvolume.get_mounted_path()
 | |
|         target_mounted_path = self.__filesystem.get_mountpath_as_child(operation['target'])
 | |
| 
 | |
|         self.__btrfs_api.subvolume_snapshot(source_mounted_path, target_mounted_path)
 | |
|         self.__completed_work.append(operation)
 | |
| 
 | |
|     def __execute_delete_subvolume(self, operation):
 | |
|         target_mounted_path = self.__filesystem.get_mountpath_as_child(operation['target'])
 | |
|         self.__btrfs_api.subvolume_delete(target_mounted_path)
 | |
|         self.__completed_work.append(operation)
 | |
| 
 | |
|     def __execute_set_default_subvolume(self, operation):
 | |
|         target = operation['target']
 | |
|         target_id = operation['target_id']
 | |
| 
 | |
|         if target_id is None:
 | |
|             target_subvolume = self.__filesystem.get_subvolume_by_name(target)
 | |
| 
 | |
|             if target_subvolume is None:
 | |
|                 self.__filesystem.refresh()  # the target may have been created earlier in module execution
 | |
|                 target_subvolume = self.__filesystem.get_subvolume_by_name(target)
 | |
| 
 | |
|             if target_subvolume is None:
 | |
|                 raise BtrfsModuleException("Failed to find existing subvolume '%s'" % target)
 | |
|             else:
 | |
|                 target_id = target_subvolume.id
 | |
| 
 | |
|         self.__btrfs_api.subvolume_set_default(self.__filesystem.get_any_mountpoint(), target_id)
 | |
|         self.__completed_work.append(operation)
 | |
| 
 | |
|     def __is_existing_directory_like(self, path):
 | |
|         return os.path.exists(path) and (
 | |
|             os.path.isdir(path) or
 | |
|             os.stat(path).st_ino == self.__BTRFS_SUBVOLUME_INODE_NUMBER
 | |
|         )
 | |
| 
 | |
|     def __check_required_mounts(self):
 | |
|         filtered = self.__filter_child_subvolumes(self.__required_mounts)
 | |
|         if len(filtered) > 0:
 | |
|             for subvolume in filtered:
 | |
|                 self.__mount_subvolume_id_to_tempdir(self.__filesystem, subvolume.id)
 | |
|             self.__filesystem.refresh_mountpoints()
 | |
| 
 | |
|     def __filter_child_subvolumes(self, subvolumes):
 | |
|         """Filter the provided list of subvolumes to remove any that are a child of another item in the list"""
 | |
|         filtered = []
 | |
|         last = None
 | |
|         ordered = sorted(subvolumes, key=lambda x: x.path)
 | |
|         for next in ordered:
 | |
|             if last is None or not next.path[0:len(last)] == last:
 | |
|                 filtered.append(next)
 | |
|                 last = next.path
 | |
|         return filtered
 | |
| 
 | |
|     # Create/cleanup temporary mountpoints
 | |
|     def __mount_subvolume_id_to_tempdir(self, filesystem, subvolid):
 | |
|         # this check should be redundant
 | |
|         if self.module.check_mode or not self.__automount:
 | |
|             raise BtrfsModuleException("Unable to temporarily mount required subvolumes"
 | |
|                                        "with automount=%s and check_mode=%s" % (self.__automount, self.module.check_mode))
 | |
| 
 | |
|         cache_key = "%s:%d" % (filesystem.uuid, subvolid)
 | |
|         # The subvolume was already mounted, so return the current path
 | |
|         if cache_key in self.__temporary_mounts:
 | |
|             return self.__temporary_mounts[cache_key]
 | |
| 
 | |
|         device = filesystem.devices[0]
 | |
|         mountpoint = tempfile.mkdtemp(dir="/tmp")
 | |
|         self.__temporary_mounts[cache_key] = mountpoint
 | |
| 
 | |
|         mount = self.module.get_bin_path("mount", required=True)
 | |
|         command = "%s -o noatime,subvolid=%d %s %s " % (mount,
 | |
|                                                         subvolid,
 | |
|                                                         device,
 | |
|                                                         mountpoint)
 | |
|         result = self.module.run_command(command, check_rc=True)
 | |
| 
 | |
|         return mountpoint
 | |
| 
 | |
|     def __cleanup_mounts(self):
 | |
|         for key in self.__temporary_mounts.keys():
 | |
|             self.__cleanup_mount(self.__temporary_mounts[key])
 | |
| 
 | |
|     def __cleanup_mount(self, mountpoint):
 | |
|         umount = self.module.get_bin_path("umount", required=True)
 | |
|         result = self.module.run_command("%s %s" % (umount, mountpoint))
 | |
|         if result[0] == 0:
 | |
|             rmdir = self.module.get_bin_path("rmdir", required=True)
 | |
|             self.module.run_command("%s %s" % (rmdir, mountpoint))
 | |
| 
 | |
|     # Format and return results
 | |
|     def get_results(self):
 | |
|         target = self.__filesystem.get_subvolume_by_name(self.__name)
 | |
|         return dict(
 | |
|             changed=len(self.__completed_work) > 0,
 | |
|             filesystem=self.__filesystem.get_summary(),
 | |
|             modifications=self.__get_formatted_modifications(),
 | |
|             target_subvolume_id=(target.id if target is not None else None)
 | |
|         )
 | |
| 
 | |
|     def __get_formatted_modifications(self):
 | |
|         return [self.__format_operation_result(op) for op in self.__completed_work]
 | |
| 
 | |
|     def __format_operation_result(self, operation):
 | |
|         action_type = operation['action']
 | |
|         if action_type == self.__CREATE_SUBVOLUME_OPERATION:
 | |
|             return self.__format_create_subvolume_result(operation)
 | |
|         elif action_type == self.__CREATE_SNAPSHOT_OPERATION:
 | |
|             return self.__format_create_snapshot_result(operation)
 | |
|         elif action_type == self.__DELETE_SUBVOLUME_OPERATION:
 | |
|             return self.__format_delete_subvolume_result(operation)
 | |
|         elif action_type == self.__SET_DEFAULT_SUBVOLUME_OPERATION:
 | |
|             return self.__format_set_default_subvolume_result(operation)
 | |
|         else:
 | |
|             raise ValueError("Unknown operation type '%s'" % operation['action'])
 | |
| 
 | |
|     def __format_create_subvolume_result(self, operation):
 | |
|         target = operation['target']
 | |
|         target_subvolume = self.__filesystem.get_subvolume_by_name(target)
 | |
|         target_id = target_subvolume.id if target_subvolume is not None else self.__UNKNOWN_SUBVOLUME_ID
 | |
|         return "Created subvolume '%s' (%s)" % (target, target_id)
 | |
| 
 | |
|     def __format_create_snapshot_result(self, operation):
 | |
|         source = operation['source']
 | |
|         source_id = operation['source_id']
 | |
| 
 | |
|         target = operation['target']
 | |
|         target_subvolume = self.__filesystem.get_subvolume_by_name(target)
 | |
|         target_id = target_subvolume.id if target_subvolume is not None else self.__UNKNOWN_SUBVOLUME_ID
 | |
|         return "Created snapshot '%s' (%s) from '%s' (%s)" % (target, target_id, source, source_id)
 | |
| 
 | |
|     def __format_delete_subvolume_result(self, operation):
 | |
|         target = operation['target']
 | |
|         target_id = operation['target_id']
 | |
|         return "Deleted subvolume '%s' (%s)" % (target, target_id)
 | |
| 
 | |
|     def __format_set_default_subvolume_result(self, operation):
 | |
|         target = operation['target']
 | |
|         if 'target_id' in operation:
 | |
|             target_id = operation['target_id']
 | |
|         else:
 | |
|             target_subvolume = self.__filesystem.get_subvolume_by_name(target)
 | |
|             target_id = target_subvolume.id if target_subvolume is not None else self.__UNKNOWN_SUBVOLUME_ID
 | |
|         return "Updated default subvolume to '%s' (%s)" % (target, target_id)
 | |
| 
 | |
| 
 | |
| def run_module():
 | |
|     module_args = dict(
 | |
|         automount=dict(type='bool', required=False, default=False),
 | |
|         default=dict(type='bool', required=False, default=False),
 | |
|         filesystem_device=dict(type='path', required=False),
 | |
|         filesystem_label=dict(type='str', required=False),
 | |
|         filesystem_uuid=dict(type='str', required=False),
 | |
|         name=dict(type='str', required=True),
 | |
|         recursive=dict(type='bool', default=False),
 | |
|         state=dict(type='str', required=False, default='present', choices=['present', 'absent']),
 | |
|         snapshot_source=dict(type='str', required=False),
 | |
|         snapshot_conflict=dict(type='str', required=False, default='skip', choices=['skip', 'clobber', 'error'])
 | |
|     )
 | |
| 
 | |
|     module = AnsibleModule(
 | |
|         argument_spec=module_args,
 | |
|         supports_check_mode=True
 | |
|     )
 | |
| 
 | |
|     subvolume = BtrfsSubvolumeModule(module)
 | |
|     error, result = subvolume.run()
 | |
|     if error is not None:
 | |
|         module.fail_json(str(error), **result)
 | |
|     else:
 | |
|         module.exit_json(**result)
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     run_module()
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     main()
 |