From 19976701c11bbee7a5f2aa47dd66b6d065eeaea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quentin=20Th=C3=A9bault?= Date: Mon, 26 Aug 2024 03:18:35 +0900 Subject: [PATCH 1/3] Implement ALL target for snapshot, snaplist and snapremove --- doc/source/advanced-use.rst | 14 ++ iocage_cli/snaplist.py | 9 +- iocage_cli/snapremove.py | 14 +- iocage_cli/snapshot.py | 3 +- iocage_lib/iocage.py | 64 ++++++++- tests/conftest.py | 6 + tests/data_classes.py | 47 ++++++- tests/functional_tests/0018_snapshot_test.py | 21 +++ .../0019_list_snapshot_test.py | 46 ++++++- .../0020_remove_snapshot_test.py | 127 ++++++++++++++++++ 10 files changed, 335 insertions(+), 16 deletions(-) diff --git a/doc/source/advanced-use.rst b/doc/source/advanced-use.rst index 7f59f9a1..c71c8427 100644 --- a/doc/source/advanced-use.rst +++ b/doc/source/advanced-use.rst @@ -158,6 +158,8 @@ Snapshots are point-in-time copies of data, a safety point to which a jail can be reverted at any time. Initially, snapshots take up almost no space, as only changing data is recorded. +You may use **ALL** as a target jail name for these commands if you want to target all jails at once. + List snapshots for a jail: :command:`iocage snaplist [UUID|NAME]` @@ -168,6 +170,18 @@ Create a new snapshot: This creates a snapshot based on the current time. +:command:`iocage snapshot [UUID|NAME] -n [SNAPSHOT NAME]` + +This creates a snapshot with the given name. + +Delete a snapshot: + +:command:`iocage snapremove [UUID|NAME] -n [SNAPSHOT NAME]` + +Delete all snapshots from a jail (requires `-f / --force`): + +:command:`iocage snapremove [UUID|NAME] -n ALL -f` + .. index:: Resource Limits .. _Resource Limits: diff --git a/iocage_cli/snaplist.py b/iocage_cli/snaplist.py index 971b116e..80503ed5 100644 --- a/iocage_cli/snaplist.py +++ b/iocage_cli/snaplist.py @@ -43,9 +43,14 @@ def cli(header, jail, _long, _sort): snap_list = ioc.IOCage(jail=jail).snap_list(_long, _sort) if header: - table.header(["NAME", "CREATED", "RSIZE", "USED"]) + if jail == 'ALL': + cols = ["JAIL"] + else: + cols = [] + cols.extend(["NAME", "CREATED", "RSIZE", "USED"]) + table.header(cols) # We get an infinite float otherwise. - table.set_cols_dtype(["t", "t", "t", "t"]) + table.set_cols_dtype(["t"]*len(cols)) table.add_rows(snap_list, header=False) ioc_common.logit({ "level" : "INFO", diff --git a/iocage_cli/snapremove.py b/iocage_cli/snapremove.py index 3a19185d..1ab2fc8c 100644 --- a/iocage_cli/snapremove.py +++ b/iocage_cli/snapremove.py @@ -25,6 +25,7 @@ import click +import iocage_lib.ioc_common as ioc_common import iocage_lib.iocage as ioc @@ -32,6 +33,15 @@ @click.argument("jail") @click.option("--name", "-n", help="The snapshot name. This will be what comes" " after @", required=True) -def cli(jail, name): +@click.option("--force", "-f", is_flag=True, default=False, + help="Force removal (required for -n ALL)") +def cli(jail, name, force): """Removes a snapshot from a user supplied jail.""" - ioc.IOCage(jail=jail).snap_remove(name) + if name == 'ALL' and not force: + ioc_common.logit({ + "level": "EXCEPTION", + "message": 'Usage: iocage snapremove [OPTIONS] JAILS...\n' + '\nError: Mass snapshot deletion requires "force" (-f).' + }) + skip_jails = jail != 'ALL' + ioc.IOCage(jail=jail, skip_jails=skip_jails).snap_remove(name) diff --git a/iocage_cli/snapshot.py b/iocage_cli/snapshot.py index 09245b25..37db1939 100644 --- a/iocage_cli/snapshot.py +++ b/iocage_cli/snapshot.py @@ -36,4 +36,5 @@ " after @", required=False) def cli(jail, name): """Snapshot a jail.""" - ioc.IOCage(jail=jail, skip_jails=True).snapshot(name) + skip_jails = jail != 'ALL' + ioc.IOCage(jail=jail, skip_jails=skip_jails).snapshot(name) diff --git a/iocage_lib/iocage.py b/iocage_lib/iocage.py index f52c7d7e..6e1fef84 100644 --- a/iocage_lib/iocage.py +++ b/iocage_lib/iocage.py @@ -1648,8 +1648,22 @@ def set(self, prop, plugin=False, rename=False): rtsold_enable = "YES" if "accept_rtadv" in value else "NO" ioc_common.set_rcconf(path, "rtsold_enable", rtsold_enable) + def snap_list_all(self, long, _sort): + self._all = False + snap_list = [] + for jail in self.jails: + self.jail = jail + snap_list.extend( + [[jail, *snap] for snap in self.snap_list(long, _sort)] + ) + sort = ioc_common.ioc_sort("snaplist", _sort, data=snap_list) + snap_list.sort(key=sort) + return snap_list + def snap_list(self, long=True, _sort="created"): """Gathers a list of snapshots and returns it""" + if self._all: + return self.snap_list_all(long=long, _sort=_sort) uuid, path = self.__check_jail_existence__() conf = ioc_json.IOCJson(path, silent=self.silent).json_get_value('all') snap_list = [] @@ -1709,8 +1723,20 @@ def snap_list(self, long=True, _sort="created"): return snap_list + def snapshot_all(self, name): + # We want a consistent name across a snapshot batch. + if not name: + name = datetime.datetime.utcnow().strftime("%F_%T") + self._all = False + for jail in self.jails: + self.jail = jail + self.snapshot(name) + def snapshot(self, name): """Will create a snapshot for the given jail""" + if self._all: + self.snapshot_all(name) + return date = datetime.datetime.utcnow().strftime("%F_%T") uuid, path = self.__check_jail_existence__() @@ -2160,8 +2186,44 @@ def debug(self, directory): ioc_debug.IOCDebug(directory).run_debug() - def snap_remove(self, snapshot): + def _get_cloned_datasets(self): + return { + d.properties.get('origin', "").replace('/root@', '@') + for d in Dataset( + os.path.join(self.pool, 'iocage') + ).get_dependents(depth=3) + } + + def snap_remove_all(self, snapshot): + self._all = False + cloned_datasets=self._get_cloned_datasets() + + for jail in self.jails: + self.jail = jail + self.snap_remove(snapshot, cloned_datasets=cloned_datasets) + + def snap_remove(self, snapshot, cloned_datasets=None): """Removes user supplied snapshot from jail""" + if self._all: + self.snap_remove_all(snapshot) + return + if snapshot == 'ALL': + if cloned_datasets is None: + cloned_datasets = self._get_cloned_datasets() + for snapshot, *_ in reversed(self.snap_list()): + if snapshot in cloned_datasets: + ioc_common.logit({ + 'level': 'WARNING', + 'message': f"Skipped snapshot {snapshot}: used by clones." + }) + elif snapshot.rsplit('@', 1)[0].endswith('/root'): + # Deleting here would result in trying to delete + # the jail dataset-level snapshot twice since we construct + # the target based on the uuid, not path, below. + continue + else: + self.snap_remove(snapshot.rsplit('@', 1)[-1]) + return uuid, path = self.__check_jail_existence__() conf = ioc_json.IOCJson(path, silent=self.silent).json_get_value('all') diff --git a/tests/conftest.py b/tests/conftest.py index fbb84389..be54a33f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -315,6 +315,12 @@ def jail(): return Jail +@pytest.fixture +def snapshot(): + from tests.data_classes import Snapshot + return Snapshot + + @pytest.fixture def resource_selector(): from tests.data_classes import ResourceSelector diff --git a/tests/data_classes.py b/tests/data_classes.py index db12eead..6285a400 100644 --- a/tests/data_classes.py +++ b/tests/data_classes.py @@ -27,7 +27,7 @@ def __init__(self, raw_data, r_type=None): for attr in [ 'name', 'jid', 'state', 'release', 'ip4', 'ip6', 'orig_release', 'boot', 'type', 'template', 'basejail', 'crt', 'res', 'qta', - 'use', 'ava', 'created', 'rsize', 'used', 'orig_name' + 'use', 'ava', 'created', 'rsize', 'used', 'orig_name', 'jail' ]: setattr(self, attr, None) @@ -268,6 +268,10 @@ def snapshot_parse(self): self.name, self.created, self.rsize, self.used = self.standard_parse() + def snapall_parse(self): + self.jail, self.name, self.created, self.rsize, self.used = self.standard_parse() + + class ZFS: # TODO: Improve how we manage zfs object here pool = None @@ -361,13 +365,25 @@ class Resource: DEFAULT_JSON_FILE = 'config.json' def __init__(self, name, zfs=None): - self.name = name - self.zfs = ZFS() if not zfs else zfs + super().__setattr__('name', name) + super().__setattr__('zfs', ZFS() if not zfs else zfs) assert isinstance(self.zfs, ZFS) is True + def __eq__(self, other): + return self.name == other.name + + def __hash__(self): + return hash(self.name) + def __repr__(self): return self.name + def __setattr__(self, name, attr_value): + raise AttributeError(f"Resources are immutable. Cannot set attribute '{name}'.") + + def __delattr__(self, name): + raise AttributeError(f"Resources are immutable. Cannot delete attribute '{name}'.") + def convert_to_row(self, **kwargs): raise NotImplemented @@ -376,12 +392,12 @@ class Snapshot(Resource): def __init__(self, name, parent_dataset, zfs=None): super().__init__(name, zfs) - self.parent = parent_dataset + object.__setattr__(self, 'parent', parent_dataset) if isinstance(self.parent, str): - self.parent = Jail(self.parent) + object.__setattr__(self, 'parent', Jail(self.parent)) if self.exists: for k, v in self.zfs.get_snapshot_safely(self.name).items(): - setattr(self, k, v) + object.__setattr__(self, k, v) @property def exists(self): @@ -625,7 +641,7 @@ def is_rcjail(self): @property def is_cloned(self): return bool( - self.jail_dataset[ + self.root_dataset[ 'properties' ].get('origin', {}).get('value') ) @@ -795,3 +811,20 @@ def jails_with_prop(self, key, value): return [ j for j in self.all_jails if j.config.get(key, None) == value ] + + @property + def cloned_snapshots_set(self): + cloned_jails = self.cloned_jails + origins = { + jail.root_dataset['properties']['origin']['value'] + for jail in cloned_jails + } + origins |= { + jail.jail_dataset['properties']['origin']['value'] + for jail in cloned_jails + } + origins -= { "" } + return { + Snapshot(origin, origin.rsplit('@', 1)[0]) + for origin in origins + } diff --git a/tests/functional_tests/0018_snapshot_test.py b/tests/functional_tests/0018_snapshot_test.py index c305afe8..67df2ac3 100644 --- a/tests/functional_tests/0018_snapshot_test.py +++ b/tests/functional_tests/0018_snapshot_test.py @@ -30,6 +30,7 @@ SNAP_NAME = 'snaptest' +SNAPALL_NAME = 'snapalltest' def common_function(invoke_cli, jails, skip_test): @@ -46,6 +47,20 @@ def common_function(invoke_cli, jails, skip_test): ].count(SNAP_NAME) >= 2 +def all_jails_function(invoke_cli, jails, skip_test): + skip_test(not jails) + + invoke_cli( + ['snapshot', 'ALL', '-n', SNAPALL_NAME] + ) + + for jail in jails: + # We use count because of template and cloned jails + assert [ + s.id.split('@')[1] for s in jail.recursive_snapshots + ].count(SNAPALL_NAME) >= 2 + + @require_root @require_zpool def test_01_snapshot_of_jail(invoke_cli, resource_selector, skip_test): @@ -66,3 +81,9 @@ def test_02_snapshot_of_template_jail(invoke_cli, resource_selector, skip_test): @require_zpool def test_03_snapshot_of_cloned_jail(invoke_cli, resource_selector, skip_test): common_function(invoke_cli, resource_selector.cloned_jails, skip_test) + + +@require_root +@require_zpool +def test_04_snapshot_of_all_jails(invoke_cli, resource_selector, skip_test): + all_jails_function(invoke_cli, resource_selector.all_jails, skip_test) diff --git a/tests/functional_tests/0019_list_snapshot_test.py b/tests/functional_tests/0019_list_snapshot_test.py index 371191d8..033b4ce9 100644 --- a/tests/functional_tests/0019_list_snapshot_test.py +++ b/tests/functional_tests/0019_list_snapshot_test.py @@ -37,7 +37,10 @@ def common_function( jails_as_rows, full=False ): for flag in SORTING_FLAGS: - command = ['snaplist', jail.name, '-s', flag] + if type(jail) is list: + command = ['snaplist', 'ALL', '-s', flag] + else: + command = ['snaplist', jail.name, '-s', flag] if full: command.append('-l') @@ -45,8 +48,17 @@ def common_function( command ) - orig_list = parse_rows_output(result.output, 'snapshot') - verify_list = jails_as_rows(jail.recursive_snapshots, full=full) + if type(jail) is list: + jails = jail + orig_list = parse_rows_output(result.output, 'snapall') + verify_list = [] + for jail in jails: + for row in jails_as_rows(jail.recursive_snapshots, full=full): + row.jail = jail.name + verify_list.append(row) + else: + orig_list = parse_rows_output(result.output, 'snapshot') + verify_list = jails_as_rows(jail.recursive_snapshots, full=full) verify_list.sort(key=lambda r: r.sort_flag(flag)) @@ -89,3 +101,31 @@ def test_03_list_snapshots_of_jail_with_long_flag( common_function( invoke_cli, jails[0], parse_rows_output, jails_as_rows, True ) + + +@require_root +@require_zpool +def test_04_list_snapshots_of_all_jails( + invoke_cli, resource_selector, skip_test, + parse_rows_output, jails_as_rows +): + jails = resource_selector.all_jails_having_snapshots + skip_test(not jails) + + common_function( + invoke_cli, jails, parse_rows_output, jails_as_rows + ) + + +@require_root +@require_zpool +def test_05_list_snapshots_of_all_jails_with_long_flag( + invoke_cli, resource_selector, skip_test, + parse_rows_output, jails_as_rows +): + jails = resource_selector.all_jails_having_snapshots + skip_test(not jails) + + common_function( + invoke_cli, jails, parse_rows_output, jails_as_rows, True + ) diff --git a/tests/functional_tests/0020_remove_snapshot_test.py b/tests/functional_tests/0020_remove_snapshot_test.py index 1b863e26..bf9bbbb3 100644 --- a/tests/functional_tests/0020_remove_snapshot_test.py +++ b/tests/functional_tests/0020_remove_snapshot_test.py @@ -29,6 +29,7 @@ require_zpool = pytest.mark.require_zpool SNAP_NAME = 'snaptest' +SNAPALL_NAME = 'snapalltest' @require_root @@ -60,3 +61,129 @@ def test_01_remove_snapshot(invoke_cli, resource_selector, skip_test): ) assert remove_snap.exists is False + + +@require_root +@require_zpool +def test_02_remove_snapshot_of_all_jails( + invoke_cli, resource_selector, skip_test): + jails = resource_selector.all_jails + skip_test(not jails) + + snap_jails = [] + for jail in jails: + if any( + SNAPALL_NAME in snap.id for snap in jail.recursive_snapshots + ): + snap_jails.append(jail) + + skip_test(not snap_jails) + + remove_snaps = [] + for snap_jail in snap_jails: + for snap in snap_jail.recursive_snapshots: + if SNAPALL_NAME in snap.id: + remove_snaps.append(snap) + + assert all(snap.exists is True for snap in remove_snaps) + + invoke_cli( + ['snapremove', '-n', SNAPALL_NAME, 'ALL'] + ) + + assert all(snap.exists is False for snap in remove_snaps) + +@require_root +@require_zpool +def test_03_remove_all_snapshots_fail(invoke_cli, resource_selector, skip_test): + jails = resource_selector.all_jails_having_snapshots + skip_test(not jails) + + snap_jail = None + for jail in jails: + if (not jail.is_template and not jail.is_cloned and + len(jail.recursive_snapshots)>0): + snap_jail = jail + break + + skip_test(not snap_jail) + + failremove_snap = snap_jail.recursive_snapshots[0] + + assert failremove_snap.exists is True + + # Voluntarily forgetting the -f force flag + invoke_cli( + ['snapremove', '-n', 'ALL', snap_jail.name], + assert_returncode=False + ) + + assert failremove_snap.exists is True + + +@require_root +@require_zpool +def test_04_remove_all_snapshots_success(invoke_cli, resource_selector, + snapshot, skip_test): + jails = resource_selector.all_jails_having_snapshots + skip_test(not jails) + + snap_jail = None + for jail in jails: + if (not jail.is_template and not jail.is_cloned and + len(jail.recursive_snapshots)>1): + snap_jail = jail + break + + skip_test(not snap_jail) + + remove_snaps = set(snap_jail.recursive_snapshots) + assert all(snap.exists is True for snap in remove_snaps) + + cloned_snaps = resource_selector.cloned_snapshots_set + assert all(snap.exists is True for snap in cloned_snaps) + + filtered_remove_snaps = remove_snaps - { + snapshot(s, s.rsplit('@', 1)[0]) + for snap in cloned_snaps + for s in (snap.name.replace('/root@', '@'), snap.name) + } + + result = invoke_cli( + ['snapremove', '-n', 'ALL', snap_jail.name, '--force'] + ) + + assert all(snap.exists is False for snap in filtered_remove_snaps) + assert all(snap.exists is True for snap in cloned_snaps) + + +@require_root +@require_zpool +def test_05_remove_all_snapshots_all_jails(invoke_cli, resource_selector, + snapshot, skip_test): + jails = resource_selector.all_jails_having_snapshots + skip_test(not jails) + + cloned_snaps = resource_selector.cloned_snapshots_set + assert all(snap.exists is True for snap in cloned_snaps) + + remove_snaps = { + snap for jail in jails for snap in jail.recursive_snapshots + } + assert all(snap.exists is True for snap in remove_snaps) + + # We want to keep cloned jail datasets, cloned root datasets, + # and non-cloned jail datasets that contain cloned root datasets. + # This last case happens when creating jails from templates. + filtered_remove_snaps = remove_snaps - { + snapshot(s, s.rsplit('@', 1)[0]) + for snap in cloned_snaps + for s in (snap.name.replace('/root@', '@'), snap.name) + } + + result = invoke_cli( + ['snapremove', '-n', 'ALL', 'ALL', '--force'] + ) + + assert all(snap.exists is False for snap in filtered_remove_snaps) + assert all(snap.exists is True for snap in cloned_snaps) From 700f5d69c6a3f8f57ddfc4e485d1ac68bed6e4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quentin=20Th=C3=A9bault?= Date: Sat, 21 Sep 2024 04:49:50 +0900 Subject: [PATCH 2/3] Apply suggestions from code review Use isinstance() instead of type() Co-authored-by: Vincent Barbaresi --- tests/functional_tests/0019_list_snapshot_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional_tests/0019_list_snapshot_test.py b/tests/functional_tests/0019_list_snapshot_test.py index 033b4ce9..dc88fb47 100644 --- a/tests/functional_tests/0019_list_snapshot_test.py +++ b/tests/functional_tests/0019_list_snapshot_test.py @@ -37,7 +37,7 @@ def common_function( jails_as_rows, full=False ): for flag in SORTING_FLAGS: - if type(jail) is list: + if isinstance(jail, list): command = ['snaplist', 'ALL', '-s', flag] else: command = ['snaplist', jail.name, '-s', flag] @@ -48,7 +48,7 @@ def common_function( command ) - if type(jail) is list: + if isinstance(jail, list): jails = jail orig_list = parse_rows_output(result.output, 'snapall') verify_list = [] From f45410026d08861e94f8bec41b2c2e5908905504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quentin=20Th=C3=A9bault?= Date: Fri, 23 Aug 2024 02:36:51 +0900 Subject: [PATCH 3/3] Implement prefix for ZFS dataset tree Instead of using pool/iocage, we can now use anything like pool/some/prefix/iocage, which enables iocage use within jails. To do so, use the -p / --prefix option with iocage activate. The prefix is then stored as a ZFS property of the pool and used for all commands. A fixture is also added to the tests to enable functional tests. --- .cirrus.yml | 6 +- iocage_cli/activate.py | 14 ++++- iocage_cli/migrate.py | 6 +- iocage_lib/cache.py | 12 ++-- iocage_lib/ioc_check.py | 15 ++++- iocage_lib/ioc_clean.py | 17 +++-- iocage_lib/ioc_create.py | 52 ++++++++++++---- iocage_lib/ioc_debug.py | 11 +++- iocage_lib/ioc_destroy.py | 10 +-- iocage_lib/ioc_fetch.py | 16 +++-- iocage_lib/ioc_image.py | 20 ++++-- iocage_lib/ioc_json.py | 65 +++++++++++++------- iocage_lib/ioc_list.py | 8 +-- iocage_lib/ioc_plugin.py | 4 +- iocage_lib/iocage.py | 61 +++++++++++------- iocage_lib/pools.py | 19 ++++-- iocage_lib/zfs.py | 9 +-- pytest.ini | 2 +- tests/conftest.py | 9 +++ tests/data_classes.py | 13 ++-- tests/functional_tests/0001_activate_test.py | 9 ++- 21 files changed, 264 insertions(+), 114 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 0301ecc0..5017389f 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -45,7 +45,11 @@ iocage_tests_task: - pkg install_iocage_script: - python3 setup.py install - test_script: pytest --zpool=pool tests/functional_tests --junit-xml=reports/pytest-report.xml -rA --image + matrix: + - name: No prefix + test_script: pytest -rA --zpool=pool tests/functional_tests --junit-xml=reports/pytest-report.xml --image + - name: Custom prefix + test_script: pytest -rA --zpool=pool --prefix=test-prefix tests/functional_tests --junit-xml=reports/pytest-report.xml --image always: pytest_results_artifacts: path: reports/*.xml diff --git a/iocage_cli/activate.py b/iocage_cli/activate.py index 48b904b8..5a1a8ba8 100644 --- a/iocage_cli/activate.py +++ b/iocage_cli/activate.py @@ -31,12 +31,22 @@ @click.command(name="activate", help="Set a zpool active for iocage usage.") +@click.option( + "--prefix", "-p", default='', + help="Provide a prefix for dataset path." +) @click.argument("zpool") -def cli(zpool): +def cli(zpool, prefix): """Calls ZFS set to change the property org.freebsd.ioc:active to yes.""" - ioc.IOCage(activate=True).activate(zpool) + ioc.IOCage(activate=True).activate(zpool, prefix) ioc_common.logit({ "level" : "INFO", "message": f"ZFS pool '{zpool}' successfully activated." }) + + if prefix: + ioc_common.logit({ + "level" : "INFO", + "message": f"Dataset prefix '{prefix}' set." + }) diff --git a/iocage_cli/migrate.py b/iocage_cli/migrate.py index 55ef96f1..3f725989 100644 --- a/iocage_cli/migrate.py +++ b/iocage_cli/migrate.py @@ -74,8 +74,8 @@ def cli(force, delete): for uuid, path in jails.items(): pool = ioc_json.IOCJson().json_get_value("pool") iocroot = ioc_json.IOCJson(pool).json_get_value("iocroot") - jail = f"{pool}/iocage/jails/{uuid}" - jail_old = f"{pool}/iocage/jails_old/{uuid}" + jail = f"{iocroot}/jails/{uuid}" + jail_old = f"{iocroot}/jails_old/{uuid}" conf = ioc_json.IOCJson(path).json_get_value('all') try: @@ -164,7 +164,7 @@ def cli(force, delete): try: su.check_call([ "zfs", "destroy", "-r", "-f", - f"{pool}/iocage/jails_old" + f"{iocroot}/jails_old" ]) except su.CalledProcessError: # We just want the top level dataset gone, no big deal. diff --git a/iocage_lib/cache.py b/iocage_lib/cache.py index 3dcfd742..0a498f27 100644 --- a/iocage_lib/cache.py +++ b/iocage_lib/cache.py @@ -4,7 +4,8 @@ import threading from iocage_lib.zfs import ( - all_properties, dataset_exists, get_all_dependents, get_dependents_with_depth, + all_properties, dataset_exists, get_all_dependents, get_dependents_with_depth, IOCAGE_POOL_PROP, + IOCAGE_PREFIX_PROP, ) @@ -43,10 +44,13 @@ def iocage_activated_pool_internal(self, lock=True): all_properties([p for p in pools], types=['filesystem']) ) for p in filter( - lambda p: self.dataset_data.get(p, {}).get('org.freebsd.ioc:active') == 'yes', + lambda p: self.dataset_data.get(p, {}).get(IOCAGE_POOL_PROP) == 'yes', pools ): self.ioc_pool = p + self.ioc_prefix = self.dataset_data.get( + self.ioc_pool, {} + ).get(IOCAGE_PREFIX_PROP, '') return self.ioc_pool finally: if lock: @@ -58,7 +62,7 @@ def iocage_activated_dataset(self): ioc_pool = self.iocage_activated_pool_internal(lock=False) if ioc_pool: dependents = self.dependents_internal(ioc_pool, 1, lock=False) - ioc_ds = os.path.join(ioc_pool, 'iocage') + ioc_ds = os.path.join(ioc_pool, self.ioc_prefix, 'iocage') if not self.ioc_dataset and ioc_pool and ioc_ds in dependents: self.ioc_dataset = ioc_ds return self.ioc_dataset @@ -70,7 +74,7 @@ def datasets(self): if not self.dataset_data or set(self.dataset_data) == set(self.pool_data): ds = '' if ioc_pool: - ds = os.path.join(ioc_pool, 'iocage') + ds = os.path.join(ioc_pool, self.ioc_prefix, 'iocage') self.dataset_data.update(all_properties( [ds] if ds and dataset_exists(ds) else [], recursive=True, types=['filesystem'] )) diff --git a/iocage_lib/ioc_check.py b/iocage_lib/ioc_check.py index 815bfcf4..bc08a9a8 100644 --- a/iocage_lib/ioc_check.py +++ b/iocage_lib/ioc_check.py @@ -32,7 +32,8 @@ from iocage_lib.cache import cache from iocage_lib.dataset import Dataset -from iocage_lib.zfs import ZFSException +from iocage_lib.pools import Pool +from iocage_lib.zfs import ZFSException, IOCAGE_PREFIX_PROP DATASET_CREATION_LOCK = threading.Lock() @@ -51,6 +52,7 @@ def __init__( silent=silent, checking_datasets=True ).json_get_value("pool") + self.zpool = Pool(self.pool) self.callback = callback self.silent = silent @@ -59,7 +61,8 @@ def __init__( self.pool_root_dataset = Dataset(self.pool, cache=reset_cache) self.iocage_dataset = Dataset( - os.path.join(self.pool, 'iocage'), cache=reset_cache + os.path.join(self.zpool.name, self.zpool.prefix, 'iocage'), + cache=reset_cache ) if migrate: @@ -87,6 +90,14 @@ def __check_datasets__(self): datasets = ("iocage", "iocage/download", "iocage/images", "iocage/jails", "iocage/log", "iocage/releases", "iocage/templates") + if self.zpool.prefix != '': + prefix_split = self.zpool.prefix.split('/') + datasets = *( + os.path.join(*prefix_split[:n+1]) + for n in range(len(prefix_split)) + ), *( + os.path.join(self.zpool.prefix, ioc_ds) for ioc_ds in datasets + ) for dataset in datasets: zfs_dataset_name = f"{self.pool}/{dataset}" diff --git a/iocage_lib/ioc_clean.py b/iocage_lib/ioc_clean.py index 42cb6906..abf8f034 100644 --- a/iocage_lib/ioc_clean.py +++ b/iocage_lib/ioc_clean.py @@ -27,8 +27,10 @@ import iocage_lib.ioc_destroy import iocage_lib.ioc_json import shutil +import os from iocage_lib.dataset import Dataset +from iocage_lib.pools import Pool class IOCClean: @@ -36,6 +38,7 @@ class IOCClean: def __init__(self, callback=None, silent=False): self.pool = iocage_lib.ioc_json.IOCJson().json_get_value('pool') + self.zpool = Pool(self.pool) self.iocroot = iocage_lib.ioc_json.IOCJson(self.pool).json_get_value( 'iocroot') @@ -52,7 +55,7 @@ def clean_jails(self): silent=self.silent) iocage_lib.ioc_destroy.IOCDestroy().destroy_jail( - f'{self.pool}/iocage/jails', + f'{self.iocroot}/jails', clean=True ) @@ -66,7 +69,7 @@ def clean_releases(self): silent=self.silent) iocage_lib.ioc_destroy.IOCDestroy().destroy_jail( - f'{self.pool}/iocage/download', + f'{self.iocroot}/download', clean=True ) @@ -78,7 +81,7 @@ def clean_releases(self): silent=self.silent) iocage_lib.ioc_destroy.IOCDestroy().destroy_jail( - f'{self.pool}/iocage/releases', + f'{self.iocroot}/releases', clean=True) def clean_all(self): @@ -96,7 +99,9 @@ def clean_all(self): silent=self.silent) iocage_lib.ioc_destroy.IOCDestroy().__destroy_parse_datasets__( - f'{self.pool}/{dataset}', clean=True) + os.path.join(self.zpool.name, self.zpool.prefix, dataset), + clean=True + ) def clean_templates(self): """Cleans all templates and their respective children.""" @@ -108,7 +113,7 @@ def clean_templates(self): silent=self.silent) iocage_lib.ioc_destroy.IOCDestroy().__destroy_parse_datasets__( - f"{self.pool}/iocage/templates", + f"{self.iocroot}/templates", clean=True) def clean_images(self): @@ -120,7 +125,7 @@ def clean_images(self): _callback=self.callback, silent=self.silent) - Dataset(f'{self.pool}/iocage/images').destroy(True, True) + Dataset(f'{self.iocroot}/images').destroy(True, True) def clean_debug(self): """Removes the debug directory""" diff --git a/iocage_lib/ioc_create.py b/iocage_lib/ioc_create.py index 0a312abf..7bd6e100 100644 --- a/iocage_lib/ioc_create.py +++ b/iocage_lib/ioc_create.py @@ -43,6 +43,7 @@ import shutil from iocage_lib.cache import cache +from iocage_lib.pools import Pool from iocage_lib.dataset import Dataset @@ -59,6 +60,7 @@ def __init__(self, release, props, num, pkglist=None, plugin=False, self.pool = iocage_lib.ioc_json.IOCJson().json_get_value("pool") self.iocroot = iocage_lib.ioc_json.IOCJson(self.pool).json_get_value( "iocroot") + self.zpool = Pool(self.pool) self.release = release self.props = props self.num = num @@ -218,10 +220,16 @@ def _create_jail(self, jail_uuid, location): clone_etc_hosts = \ f"{self.iocroot}/jails/{jail_uuid}/root/etc/hosts" - jail = f"{self.pool}/iocage/jails/{jail_uuid}/root" + jail = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'jails', jail_uuid, 'root' + ) if self.template: - source = f'{self.pool}/iocage/templates/{self.release}@{jail_uuid}' + source = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'templates', f'{self.release}@{jail_uuid}' + ) snap_cmd = ['zfs', 'snapshot', '-r', source] if self.thickjail: @@ -255,7 +263,10 @@ def _create_jail(self, jail_uuid, location): # Thick jails won't have this pass elif self.clone: - source = f'{self.pool}/iocage/jails/{self.release}@{jail_uuid}' + source = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'jails', f'{self.release}@{jail_uuid}' + ) snap_cmd = ['zfs', 'snapshot', '-r', source] if self.thickjail: @@ -319,13 +330,16 @@ def _create_jail(self, jail_uuid, location): config[k] = v else: if not self.empty: - dataset = f'{self.pool}/iocage/releases/{self.release}/' \ - f'root@{jail_uuid}' + dataset = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'releases', self.release, f'root@{jail_uuid}' + ) try: su.check_call(['zfs', 'snapshot', dataset], stderr=su.PIPE) except su.CalledProcessError: release = os.path.join( - self.pool, 'iocage/releases', self.release + self.zpool.name, self.zpool.prefix, 'iocage', + 'releases', self.release ) if not Dataset(release).exists: raise RuntimeError( @@ -366,7 +380,11 @@ def _create_jail(self, jail_uuid, location): if jail_uuid == "default" or jail_uuid == "help": iocage_lib.ioc_destroy.IOCDestroy( ).__destroy_parse_datasets__( - f"{self.pool}/iocage/jails/{jail_uuid}") + os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'jails', jail_uuid + ) + ) iocage_lib.ioc_common.logit({ "level": "EXCEPTION", "message": f"You cannot name a jail {jail_uuid}, " @@ -391,7 +409,10 @@ def _create_jail(self, jail_uuid, location): iocjson.json_set_value("type=template") iocjson.json_set_value("template=1") Dataset( - os.path.join(self.pool, 'iocage', 'templates', jail_uuid) + os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'templates', jail_uuid + ) ).set_property('readonly', 'off') # If you supply pkglist and templates without setting the @@ -532,7 +553,10 @@ def _create_jail(self, jail_uuid, location): # If jail is template, the dataset would be readonly at this point if is_template: Dataset( - os.path.join(self.pool, 'iocage/templates', jail_uuid) + os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'templates', jail_uuid + ) ).set_property('readonly', 'off') if self.empty: @@ -691,7 +715,10 @@ def _create_jail(self, jail_uuid, location): if is_template: # We have to set readonly back, since we're done with our tasks Dataset( - os.path.join(self.pool, 'iocage/templates', jail_uuid) + os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'templates', jail_uuid + ) ).set_property('readonly', 'on') return jail_uuid @@ -1047,7 +1074,10 @@ def create_rc(self, location, host_hostname, basejail=0): ['umount', '-F', f'{location}/fstab', '-a']).communicate() def create_thickjail(self, jail_uuid, source): - jail = f"{self.pool}/iocage/jails/{jail_uuid}" + jail = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'jails', jail_uuid + ) try: su.Popen(['zfs', 'create', '-p', jail], diff --git a/iocage_lib/ioc_debug.py b/iocage_lib/ioc_debug.py index e1f73905..bb477bc3 100644 --- a/iocage_lib/ioc_debug.py +++ b/iocage_lib/ioc_debug.py @@ -30,7 +30,7 @@ import iocage_lib.ioc_list as ioc_list from iocage_lib.dataset import Dataset -from iocage_lib.pools import PoolListableResource +from iocage_lib.pools import PoolListableResource, Pool class IOCDebug(object): @@ -55,6 +55,7 @@ class IOCDebug(object): def __init__(self, path, silent=False, callback=None): self.pool = ioc_json.IOCJson(' ').json_get_value('pool') + self.zpool = Pool(self.pool) self.path = path self.callback = callback self.silent = silent @@ -64,10 +65,14 @@ def run_debug(self): self.run_host_debug() jails = Dataset( - os.path.join(self.pool, 'iocage/jails') + os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', 'jails' + ) ).get_dependents() templates = Dataset( - os.path.join(self.pool, 'iocage/templates') + os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', 'templates' + ) ).get_dependents() for jail in jails: diff --git a/iocage_lib/ioc_destroy.py b/iocage_lib/ioc_destroy.py index bb914c1e..d7f6ab33 100644 --- a/iocage_lib/ioc_destroy.py +++ b/iocage_lib/ioc_destroy.py @@ -45,7 +45,7 @@ def __init__(self, callback=None): self.callback = callback self.iocroot_datasets = [ d.name for d in - Dataset(os.path.join(self.pool, 'iocage')).get_dependents() + Dataset(self.iocroot).get_dependents() ] self.path = None self.j_conf = None @@ -137,7 +137,7 @@ def __destroy_leftovers__(self, dataset, clean=False): release = self.j_conf['release'] release_snap = Snapshot( - f'{self.pool}/iocage/releases/{release}/root@{uuid}' + f'{self.iocroot}/releases/{release}/root@{uuid}' ) if release_snap.exists: @@ -146,7 +146,7 @@ def __destroy_leftovers__(self, dataset, clean=False): try: temp = self.j_conf['source_template'] temp_snap = Snapshot( - f'{self.pool}/iocage/templates/{temp}@{uuid}' + f'{self.iocroot}/templates/{temp}@{uuid}' ) if temp_snap.exists: @@ -182,7 +182,7 @@ def __destroy_dataset__(self, dataset): uuid = dataset.rsplit('/', 1)[1] jail_datasets = Dataset( - f'{self.pool}/iocage/jails' + f'{self.iocroot}/jails' ).get_dependents() for jail in jail_datasets: with iocage_lib.ioc_exceptions.ignore_exceptions( @@ -278,7 +278,7 @@ def destroy_jail(self, path, clean=False): try: self.__destroy_parse_datasets__( - f"{self.pool}/iocage/{dataset_type}/{uuid}") + f"{self.iocroot}/{dataset_type}/{uuid}") except SystemExit: # The dataset doesn't exist, we don't care :) pass diff --git a/iocage_lib/ioc_fetch.py b/iocage_lib/ioc_fetch.py index 585824fb..135830f2 100644 --- a/iocage_lib/ioc_fetch.py +++ b/iocage_lib/ioc_fetch.py @@ -238,7 +238,7 @@ def fetch_release(self, _list=False): silent=self.silent) dataset = f"{self.iocroot}/download/{self.release}" - pool_dataset = f"{self.pool}/iocage/download/{self.release}" + pool_dataset = f"{self.iocroot}/download/{self.release}" if os.path.isdir(dataset): pass @@ -652,12 +652,15 @@ def __fetch_check__(self, _list, _missing=False): def fetch_download(self, _list, missing=False): """Creates the download dataset and then downloads the RELEASE.""" - dataset = f"{self.iocroot}/download/{self.release}" + release_dir = f"{self.iocroot}/download/{self.release}" fresh = False - if not os.path.isdir(dataset): + if not os.path.isdir(release_dir): fresh = True - dataset = f"{self.pool}/iocage/download/{self.release}" + dataset = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'download', self.release + ) ds = Dataset(dataset) if not ds.exists: @@ -805,7 +808,10 @@ def fetch_extract(self, f): src = f"{self.iocroot}/download/{self.release}/{f}" dest = f"{self.iocroot}/releases/{self.release}/root" - dataset = f"{self.pool}/iocage/releases/{self.release}/root" + dataset = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'releases', self.release, 'root' + ) if not os.path.isdir(dest): self.zpool.create_dataset({ diff --git a/iocage_lib/ioc_image.py b/iocage_lib/ioc_image.py index 953d2082..354624e5 100644 --- a/iocage_lib/ioc_image.py +++ b/iocage_lib/ioc_image.py @@ -34,6 +34,7 @@ import iocage_lib.ioc_json from iocage_lib.cache import cache +from iocage_lib.pools import Pool class IOCImage(object): @@ -41,6 +42,7 @@ class IOCImage(object): def __init__(self, callback=None, silent=False): self.pool = iocage_lib.ioc_json.IOCJson().json_get_value("pool") + self.zpool = Pool(self.pool) self.iocroot = iocage_lib.ioc_json.IOCJson( self.pool).json_get_value("iocroot") self.date = datetime.datetime.utcnow().strftime("%F") @@ -53,7 +55,10 @@ def export_jail(self, uuid, path, compression_algo='zip'): name = f"{uuid}_{self.date}" image = f"{images}/{name}" export_type, jail_name = path.rsplit('/', 2)[-2:] - image_path = f"{self.pool}/iocage/{export_type}/{jail_name}" + image_path = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + export_type, jail_name + ) jail_list = [] extension = 'zip' if compression_algo == 'zip' else 'tar.xz' @@ -153,7 +158,10 @@ def export_jail(self, uuid, path, compression_algo='zip'): # Cleanup our mess. try: - target = f"{self.pool}/iocage/jails/{uuid}@ioc-export-{self.date}" + target = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'jails', f"{uuid}@ioc-export-{self.date}" + ) iocage_lib.ioc_common.checkoutput( ["zfs", "destroy", "-r", target], stderr=su.STDOUT) @@ -280,7 +288,8 @@ def import_jail(self, jail, compression_algo=None, path=None): recv = su.Popen( [ 'zfs', 'recv', '-F', os.path.join( - self.pool, 'iocage/jails', z_dataset_type + self.zpool.name, self.zpool.prefix, 'iocage', + 'jails', z_dataset_type ) ], stdin=su.PIPE ) @@ -297,7 +306,10 @@ def import_jail(self, jail, compression_algo=None, path=None): # Cleanup our mess. try: - target = f"{self.pool}/iocage/jails/{uuid}@ioc-export-{date}" + target = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'jails', f"{uuid}@ioc-export-{date}" + ) iocage_lib.ioc_common.checkoutput( ["zfs", "destroy", "-r", target], stderr=su.STDOUT) diff --git a/iocage_lib/ioc_json.py b/iocage_lib/ioc_json.py index 7f46c308..ef948afc 100644 --- a/iocage_lib/ioc_json.py +++ b/iocage_lib/ioc_json.py @@ -427,6 +427,7 @@ def __init__(self, location, checking_datasets, silent, callback): self.json_version = self.get_version() self.mac_prefix = self.get_mac_prefix() self.pool, self.iocroot = self.get_pool_and_iocroot() + self.zpool = Pool(self.pool) if not checking_datasets: self.default_config = self.check_default_config() @@ -461,7 +462,7 @@ def get_pool(): if old: matches[0].activate_pool() - return matches[0].name + return matches[0] elif len(matches) > 1: iocage_lib.ioc_common.logit( @@ -548,12 +549,12 @@ def get_pool(): zpool.activate_pool() - return zpool.name + return zpool pool = get_pool() def get_iocroot(): - loc = Dataset(os.path.join(pool, 'iocage')) + loc = Dataset(os.path.join(pool.name, pool.prefix, 'iocage')) if not loc.exists: # It's okay, ioc check would create datasets @@ -568,8 +569,7 @@ def get_iocroot(): }, _callback=self.callback, silent=self.silent) - - return pool, get_iocroot() + return pool.name, get_iocroot() @staticmethod def get_mac_prefix(): @@ -1445,7 +1445,7 @@ def json_convert_from_ucl(self): def json_convert_from_zfs(self, uuid, skip=False): """Convert to JSON. Accepts a jail UUID""" - dataset = f"{self.pool}/iocage/jails/{uuid}" + dataset = f"{self.iocroot}/jails/{uuid}" jail_zfs_prop = "org.freebsd.iocage:jail_zfs_dataset" if os.geteuid() != 0: @@ -1509,7 +1509,7 @@ def json_load(self): legacy_short = False jail_dataset = Dataset( - os.path.join(self.pool, 'iocage', jail_type, jail_uuid) + os.path.join(self.iocroot, jail_type, jail_uuid) ) if not jail_dataset.exists: if os.path.isfile(os.path.join(self.location, 'config')): @@ -1598,9 +1598,9 @@ def json_load(self): # Hack88 migration to a perm short UUID. short_uuid = full_uuid[:8] full_dataset = \ - f"{self.pool}/iocage/jails/{full_uuid}" + f"{self.iocroot}/jails/{full_uuid}" short_dataset = \ - f"{self.pool}/iocage/jails/{short_uuid}" + f"{self.iocroot}/jails/{short_uuid}" jail_hostname = Dataset( full_dataset @@ -1795,8 +1795,14 @@ def json_set_value(self, prop, _import=False, default=False): ] if key == "template": - old_location = f"{self.pool}/iocage/jails/{uuid}" - new_location = f"{self.pool}/iocage/templates/{uuid}" + old_location = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'jails', uuid + ) + new_location = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'templates', uuid + ) if status: iocage_lib.ioc_common.logit( @@ -1844,7 +1850,8 @@ def json_set_value(self, prop, _import=False, default=False): if iocage_lib.ioc_common.check_truthy(value): jail_zfs_dataset = os.path.join( - self.pool, conf['jail_zfs_dataset'] + self.zpool.name, self.zpool.prefix, + conf['jail_zfs_dataset'] ) jail_zfs_dataset_obj = Dataset(jail_zfs_dataset) if jail_zfs_dataset_obj.exists: @@ -1857,8 +1864,15 @@ def json_set_value(self, prop, _import=False, default=False): conf["type"] = "template" - self.location = new_location.lstrip(self.pool).replace( - "/iocage", self.iocroot) + if self.zpool.prefix == '': + self.location = new_location.removeprefix(self.pool) + else: + self.location = new_location.removeprefix( + os.path.join(self.zpool.name, self.zpool.prefix) + ) + self.location = self.location.replace( + "/iocage", self.iocroot + ) iocage_lib.ioc_common.logit( { @@ -1880,8 +1894,15 @@ def json_set_value(self, prop, _import=False, default=False): ds = Dataset(new_location) ds.rename(old_location, {'force_unmount': True}) conf["type"] = "jail" - self.location = old_location.lstrip(self.pool).replace( - "/iocage", self.iocroot) + if self.zpool.prefix == '': + self.location = old_location.removeprefix(self.pool) + else: + self.location = old_location.removeprefix( + os.path.join(self.zpool.name, self.zpool.prefix) + ) + self.location = self.location.replace( + "/iocage", self.iocroot + ) ds.set_property('readonly', 'off') self.json_check_prop(key, value, conf, default) @@ -2250,7 +2271,7 @@ def json_check_prop(self, key, value, conf, default=False): silent=self.silent) Dataset( - os.path.join(self.pool, 'iocage', _type, uuid) + os.path.join(self.iocroot, _type, uuid) ).set_property(key, value) return value, conf @@ -2559,7 +2580,7 @@ def json_plugin_get_value(self, prop): conf, write = self.json_load() uuid = conf["host_hostuuid"] - _path = Dataset(f"{self.pool}/iocage/jails/{uuid}").path + _path = Dataset(f"{self.iocroot}/jails/{uuid}").path # Plugin variables settings = self.json_plugin_load() @@ -2607,7 +2628,7 @@ def json_plugin_get_value(self, prop): def json_plugin_set_value(self, prop): conf, write = self.json_load() uuid = conf["host_hostuuid"] - _path = Dataset(f"{self.pool}/iocage/jails/{uuid}").path + _path = Dataset(f"{self.iocroot}/jails/{uuid}").path status, _ = iocage_lib.ioc_list.IOCList().list_get_jid(uuid) # Plugin variables @@ -2754,7 +2775,7 @@ def json_migrate_uuid_to_tag(self, uuid, tag, state, conf): # Can't rename when the child is # in a non-global zone - jail_parent_ds = f"{self.pool}/iocage/jails/{uuid}" + jail_parent_ds = f"{self.iocroot}/jails/{uuid}" jail_parent_data_obj = Dataset( os.path.join(jail_parent_ds, 'data') ) @@ -2789,7 +2810,7 @@ def json_migrate_uuid_to_tag(self, uuid, tag, state, conf): snap.clone(new_dataset) # Datasets are not mounted upon creation - new_jail_parent_ds = f"{self.pool}/iocage/jails/{tag}" + new_jail_parent_ds = f"{self.iocroot}/jails/{tag}" new_jail = Dataset(new_jail_parent_ds) if not new_jail.mounted: new_jail.mount() @@ -2802,7 +2823,7 @@ def json_migrate_uuid_to_tag(self, uuid, tag, state, conf): # Easier. su.check_call([ - "zfs", "rename", "-r", f"{self.pool}/iocage@{uuid}", + "zfs", "rename", "-r", f"{self.iocroot}@{uuid}", f"@{tag}" ]) diff --git a/iocage_lib/ioc_list.py b/iocage_lib/ioc_list.py index b56c3157..5205afc7 100644 --- a/iocage_lib/ioc_list.py +++ b/iocage_lib/ioc_list.py @@ -66,12 +66,12 @@ def __init__( def list_datasets(self): """Lists the datasets of given type.""" if self.list_type == "base": - ds = Dataset(f"{self.pool}/iocage/releases").get_dependents() + ds = Dataset(f"{self.iocroot}/releases").get_dependents() elif self.list_type == "template": ds = Dataset( - f"{self.pool}/iocage/templates").get_dependents() + f"{self.iocroot}/templates").get_dependents() else: - ds = Dataset(f"{self.pool}/iocage/jails").get_dependents() + ds = Dataset(f"{self.iocroot}/jails").get_dependents() ds = list(ds) @@ -101,7 +101,7 @@ def list_datasets(self): ) template_datasets = Dataset( - f'{self.pool}/iocage/templates').get_dependents() + f'{self.iocroot}/templates').get_dependents() for template in template_datasets: uuid = template.name.rsplit("/", 1)[-1] diff --git a/iocage_lib/ioc_plugin.py b/iocage_lib/ioc_plugin.py index 9ea6bad1..1c6af64a 100644 --- a/iocage_lib/ioc_plugin.py +++ b/iocage_lib/ioc_plugin.py @@ -575,7 +575,7 @@ def __fetch_plugin_create__(self, create_props): jaildir = f"{self.iocroot}/jails/{self.jail}" repo_dir = f"{jaildir}/root/usr/local/etc/pkg/repos" - path = f"{self.pool}/iocage/jails/{self.jail}" + path = f"{self.iocroot}/jails/{self.jail}" _conf = iocage_lib.ioc_json.IOCJson(jaildir).json_get_value('all') # We do these tests again as the user could supply a malformed IP to @@ -1464,7 +1464,7 @@ def __remove_snapshot__(self, name): names = [f'ioc_plugin_{name}_{self.date}', f'ioc_update_{release}'] for snap in Dataset( - f'{self.pool}/iocage/jails/{self.jail}' + f'{self.iocroot}/jails/{self.jail}' ).snapshots_recursive(): snap_name = snap.name diff --git a/iocage_lib/iocage.py b/iocage_lib/iocage.py index 6e1fef84..0d98b858 100644 --- a/iocage_lib/iocage.py +++ b/iocage_lib/iocage.py @@ -94,6 +94,7 @@ def __init__( if not activate: self.generic_iocjson = ioc_json.IOCJson() self.pool = self.generic_iocjson.pool + self.zpool = Pool(self.pool) self.iocroot = self.generic_iocjson.iocroot if not skip_jails: @@ -355,7 +356,7 @@ def __umount__(path, _type): return stderr - def activate(self, zpool): + def activate(self, zpool, prefix=''): """Activates the zpool for iocage usage""" zpool = Pool(zpool, cache=False) if not zpool.exists: @@ -372,8 +373,7 @@ def activate(self, zpool): locked_error = None if pool.root_dataset.locked: locked_error = f'ZFS pool "{zpool}" root dataset is locked' - - iocage_ds = Dataset(os.path.join(zpool.name, 'iocage')) + iocage_ds = Dataset(os.path.join(zpool.name, prefix, 'iocage')) if iocage_ds.exists and iocage_ds.locked: locked_error = f'ZFS dataset "{iocage_ds.name}" is locked' if locked_error: @@ -386,7 +386,7 @@ def activate(self, zpool): silent=self.silent, ) else: - pool.activate_pool() + pool.activate_pool(prefix) else: pool.deactivate_pool() @@ -633,7 +633,7 @@ def create(self, su.run( [ 'zfs', 'destroy', '-r', - f'{self.pool}/iocage/jails/{clone}@{_uuid}' + f'{self.iocroot}/jails/{clone}@{_uuid}' ] ) raise @@ -642,7 +642,7 @@ def create(self, def destroy_release(self, download=False): """Destroy supplied RELEASE and the download dataset if asked""" - path = f"{self.pool}/iocage/releases/{self.jail}" + path = f"{self.iocroot}/releases/{self.jail}" release = Release(self.jail) # Let's make sure the release exists before we try to destroy it @@ -663,7 +663,7 @@ def destroy_release(self, download=False): ioc_destroy.IOCDestroy().__destroy_parse_datasets__(path, stop=False) if download: - path = f"{self.pool}/iocage/download/{self.jail}" + path = f"{self.iocroot}/download/{self.jail}" ioc_common.logit( { "level": "INFO", @@ -688,7 +688,7 @@ def destroy_jail(self, force=False): if "Configuration is missing" in err: uuid = err.split()[5] - path = f"{self.pool}/iocage/jails/{uuid}" + path = f"{self.iocroot}/jails/{uuid}" if uuid == self.jail: ioc_destroy.IOCDestroy().__destroy_parse_datasets__( @@ -714,7 +714,7 @@ def destroy_jail(self, force=False): except FileNotFoundError as err: # Jail is lacking a configuration, time to nuke it from orbit. uuid = str(err).rsplit("/")[-2] - path = f"{self.pool}/iocage/jails/{uuid}" + path = f"{self.iocroot}/jails/{uuid}" if uuid == self.jail: ioc_destroy.IOCDestroy().__destroy_parse_datasets__(path) @@ -768,12 +768,12 @@ def df(self): for jail, path in self.jails.items(): conf = ioc_json.IOCJson(path).json_get_value('all') - mountpoint = f"{self.pool}/iocage/jails/{jail}" + mountpoint = f"{self.iocroot}/jails/{jail}" template = conf["type"] if template == "template": - mountpoint = f"{self.pool}/iocage/templates/{jail}" + mountpoint = f"{self.iocroot}/templates/{jail}" ds = Dataset(mountpoint) zconf = ds.properties @@ -1394,8 +1394,11 @@ def rename(self, new_name): _callback=self.callback, silent=self.silent) - path = f"{self.pool}/iocage/{_folders[0]}/{uuid}" - new_path = f"{self.pool}/iocage/{_folders[0]}/{new_name}" + path = f"{self.iocroot}/{_folders[0]}/{uuid}" + new_path = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + _folders[0], new_name + ) _silent = self.silent self.silent = True @@ -1522,9 +1525,9 @@ def rollback(self, name): silent=self.silent) if ioc_common.check_truthy(conf['template']): - target = f"{self.pool}/iocage/templates/{uuid}" + target = f"{self.iocroot}/templates/{uuid}" else: - target = f"{self.pool}/iocage/jails/{uuid}" + target = f"{self.iocroot}/jails/{uuid}" dataset = Dataset(target) if not dataset.exists: @@ -1671,9 +1674,9 @@ def snap_list(self, long=True, _sort="created"): snap_list_root = [] if ioc_common.check_truthy(conf['template']): - full_path = f"{self.pool}/iocage/templates/{uuid}" + full_path = f"{self.iocroot}/templates/{uuid}" else: - full_path = f"{self.pool}/iocage/jails/{uuid}" + full_path = f"{self.iocroot}/jails/{uuid}" dataset = Dataset(full_path) @@ -1749,9 +1752,15 @@ def snapshot(self, name): conf = ioc_json.IOCJson(path, silent=self.silent).json_get_value('all') if ioc_common.check_truthy(conf['template']): - target = f"{self.pool}/iocage/templates/{uuid}" + target = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'templates', uuid + ) else: - target = f"{self.pool}/iocage/jails/{uuid}" + target = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'jails', uuid + ) snap = Snapshot(f'{target}@{name}') if snap.exists: @@ -2190,7 +2199,9 @@ def _get_cloned_datasets(self): return { d.properties.get('origin', "").replace('/root@', '@') for d in Dataset( - os.path.join(self.pool, 'iocage') + os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage' + ) ).get_dependents(depth=3) } @@ -2228,9 +2239,15 @@ def snap_remove(self, snapshot, cloned_datasets=None): conf = ioc_json.IOCJson(path, silent=self.silent).json_get_value('all') if ioc_common.check_truthy(conf['template']): - target = f'{self.pool}/iocage/templates/{uuid}@{snapshot}' + target = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'templates', f"{uuid}@{snapshot}" + ) else: - target = f'{self.pool}/iocage/jails/{uuid}@{snapshot}' + target = os.path.join( + self.zpool.name, self.zpool.prefix, 'iocage', + 'jails', f"{uuid}@{snapshot}" + ) # Let's verify target exists and then destroy it, else log it snapshot = Snapshot(target) diff --git a/iocage_lib/pools.py b/iocage_lib/pools.py index 1fe480e7..826504cd 100644 --- a/iocage_lib/pools.py +++ b/iocage_lib/pools.py @@ -2,7 +2,8 @@ from iocage_lib.ioc_exceptions import PoolNotActivated from iocage_lib.resource import Resource, ListableResource from iocage_lib.zfs import ( - list_pools, IOCAGE_POOL_PROP, get_dependents + list_pools, IOCAGE_POOL_PROP, + IOCAGE_PREFIX_PROP, get_dependents ) import iocage_lib.dataset as dataset @@ -28,17 +29,25 @@ def active(self): IOCAGE_POOL_PROP ) == 'yes' + @property + def prefix(self): + return Dataset(self.name, cache=self.cache).properties.get( + IOCAGE_PREFIX_PROP, '' + ) + @property def health(self): return self.properties['health'] - def activate_pool(self): + def activate_pool(self, prefix=None): if self.health != 'ONLINE': raise PoolNotActivated( f'Please check pool status, it should be ONLINE' ) Dataset(self.name).set_property(IOCAGE_POOL_PROP, 'yes') + if prefix: + Dataset(self.name).set_property(IOCAGE_PREFIX_PROP, prefix) self.comment_check() def comment_check(self): @@ -50,9 +59,9 @@ def comment_check(self): ds.set_property('comment', '-') def deactivate_pool(self): - Dataset(self.name, cache=self.cache).set_property( - IOCAGE_POOL_PROP, 'no' - ) + ds = Dataset(self.name, cache=self.cache) + ds.set_property(IOCAGE_POOL_PROP, 'no') + ds.inherit_property(IOCAGE_PREFIX_PROP) self.comment_check() def __eq__(self, other): diff --git a/iocage_lib/zfs.py b/iocage_lib/zfs.py index d2aa843b..cb90d0c8 100644 --- a/iocage_lib/zfs.py +++ b/iocage_lib/zfs.py @@ -33,7 +33,7 @@ def __reduce__(self): IOCAGE_POOL_PROP = 'org.freebsd.ioc:active' - +IOCAGE_PREFIX_PROP = 'org.freebsd.ioc:prefix' def list_pools(): return list(filter( @@ -92,7 +92,7 @@ def pool_properties(pool): def iocage_activated_pool(): for pool in list_pools(): - if dataset_properties(pool).get('org.freebsd.ioc:active') == 'yes': + if dataset_properties(pool).get(IOCAGE_POOL_PROP) == 'yes': return pool else: return None @@ -101,8 +101,9 @@ def iocage_activated_pool(): def iocage_activated_dataset(): pool = iocage_activated_pool() if pool: - if os.path.join(pool, 'iocage') in get_dependents(pool, depth=1): - return os.path.join(pool, 'iocage') + prefix = dataset_properties(pool).get(IOCAGE_PREFIX_PROP, '') + if os.path.join(pool, prefix, 'iocage') in get_dependents(pool): + return os.path.join(pool, prefix, 'iocage') return None diff --git a/pytest.ini b/pytest.ini index 9919cb60..5eee95d0 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] -addopts = -vvv -rs --ignore=setup.py --pep8 --cov-report term-missing --cov=./iocage_lib --cov=./iocage_cli +addopts = -vvv --ignore=setup.py --pep8 --cov-report term-missing --cov=./iocage_lib --cov=./iocage_cli pep8maxlinelength = 80 pep8ignore = * ALL markers = diff --git a/tests/conftest.py b/tests/conftest.py index be54a33f..1f3e7e28 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -99,6 +99,9 @@ def pytest_addoption(parser): '--image', action='store_true', default=False, help='Run image operations (export/import)' ) + parser.addoption( + '--prefix', action='store', default='', + help='Prefix to use in ZFS dataset paths') def pytest_runtest_setup(item): @@ -148,6 +151,12 @@ def zpool(request): return request.config.getoption('--zpool') +@pytest.fixture +def prefix(request): + """Specify a prefix to use in ZFS dataset paths""" + return request.config.getoption('--prefix') + + @pytest.fixture def jail_ip(request): """Specify a jail ip to use.""" diff --git a/tests/data_classes.py b/tests/data_classes.py index 6285a400..2b0d5935 100644 --- a/tests/data_classes.py +++ b/tests/data_classes.py @@ -276,6 +276,7 @@ class ZFS: # TODO: Improve how we manage zfs object here pool = None pool_mountpoint = None + prefix = None def __init__(self): pass @@ -297,6 +298,7 @@ def set_pool(self): ZFS.pool = pools[0].name ZFS.pool_mountpoint = pools[0].root_dataset.mountpoint + ZFS.prefix = pools[0].root_dataset.__getstate__().get('properties').get('org.freebsd.ioc:prefix', {}).get('value', '') @staticmethod def get(identifier): @@ -350,15 +352,15 @@ def get_snapshot_safely(snap): @property def iocage_dataset(self): - return self.get_dataset(f'{self.pool}/iocage') + return self.get_dataset(os.path.join(self.pool, self.prefix, 'iocage')) @property def releases_dataset(self): - return self.get_dataset(f'{self.pool}/iocage/releases') + return self.get_dataset(os.path.join(self.pool, self.prefix, 'iocage/releases')) @property def images_dataset_path(self): - return os.path.join(ZFS.pool_mountpoint, 'iocage/images') + return os.path.join(ZFS.pool_mountpoint, ZFS.prefix, 'iocage/images') class Resource: @@ -507,13 +509,12 @@ def convert_to_row(self, **kwargs): @property def path(self): # Jail can be either under `jails` or `templates` datasets - if self.zfs.get_dataset( - f'{self.zfs.pool}/iocage/jails/{self.name}' + if self.zfs.get_dataset(os.path.join(self.zfs.pool, self.zfs.prefix, 'iocage/jails', self.name) ): dataset = 'jails' else: dataset = 'templates' - return f'{self.zfs.pool}/iocage/{dataset}/{self.name}' + return os.path.join(self.zfs.pool, self.zfs.prefix, 'iocage', dataset, self.name) @property def absolute_path(self): diff --git a/tests/functional_tests/0001_activate_test.py b/tests/functional_tests/0001_activate_test.py index 524835df..4cc8564f 100644 --- a/tests/functional_tests/0001_activate_test.py +++ b/tests/functional_tests/0001_activate_test.py @@ -31,11 +31,16 @@ @require_root @require_zpool -def test_activate(zpool, invoke_cli, zfs): +def test_activate(zpool, prefix, invoke_cli, zfs): + args = [zpool] + if prefix != '': + args.extend(['-p', prefix]) invoke_cli( - ['activate', zpool] + ['activate', *args] ) zfs.set_pool() assert zfs.pool == zpool, f'Failed to activate {zpool}' + if prefix != '': + assert zfs.prefix == prefix, f'Failed to set prefix {prefix}'