mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
ae5090d90e
* Initial implementation for new modules btrfs_subvolume and btrfs_info * Improve/flesh out documentation. Add ability to target filesystem by uuid, label or device. Update tests to test targeting filesystem by each supported parameter and when only mountpoint. * Updates for btrfs modules. Add missing copyright notices. Switch options to contains in return documentation. Update btrfs_subvolume to always use closest parent mount. * Add maintainers for btrfs module(s) and remove unused class member cause lint failure. * Add changelog fragment. Attempt to only run against the VMs as part of CI. * Updates per code review. Remove changelog fragment. Switch use of map to list comprehension. Add trailing comma to last item in multi-line dicts. Clean up documentation with complete senstences for descriptions and correct/consistent use of macros. * Improved error handling in btrfs_subvolume module: add custom exception type, favor exceptions over immediate call to fail_json and add single top level return for failure scenarios. Normalize name and snapshot_source parameters early in module execution and remove unecessary duplicate normalization throughout processing. * Add azp/posix/3 to aliases per feedback * Clean up automatic mounting. Prevent automount when check_mode=True. Immediately fail if a mount is identified as required and automount=True. Identify the minimal subset of subvolumes that need to be mounted instead of just finding a single common root. * Skip btrfs_subvolume integration tests if btrfs-progs isn't successfully installed. * Bump version_added for btrfs modules to 6.6.0. Ensure consistent trailing punctuation for module descriptions and document check_mode behavior as attribute description rather than a module level note. * Remove unused imports from btrfs_subvolume module. * Fix import. * Docs improvements. --------- 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 I(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 I(state=present) and I(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.
|
|
- C(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.
|
|
- C(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.
|
|
- C(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 I(filesystem_device), I(filesystem_label) or I(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 I(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()
|