Skip to content

Commit

Permalink
Implement ALL target for snapshot, snaplist and snapremove (#32)
Browse files Browse the repository at this point in the history
* Implement ALL target for snapshot, snaplist and snapremove

* Apply suggestions from code review

Use isinstance() instead of type()

Co-authored-by: Vincent Barbaresi <[email protected]>
  • Loading branch information
Defenso-QTH and vbarbaresi authored Sep 28, 2024
1 parent fe000b2 commit 704ad7c
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 16 deletions.
14 changes: 14 additions & 0 deletions doc/source/advanced-use.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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]`
Expand All @@ -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:

Expand Down
9 changes: 7 additions & 2 deletions iocage_cli/snaplist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 12 additions & 2 deletions iocage_cli/snapremove.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,23 @@

import click

import iocage_lib.ioc_common as ioc_common
import iocage_lib.iocage as ioc


@click.command(name="snapremove", help="Remove specified snapshot of a jail.")
@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)
3 changes: 2 additions & 1 deletion iocage_cli/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
64 changes: 63 additions & 1 deletion iocage_lib/iocage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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__()

Expand Down Expand Up @@ -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')

Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 40 additions & 7 deletions tests/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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')
)
Expand Down Expand Up @@ -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
}
21 changes: 21 additions & 0 deletions tests/functional_tests/0018_snapshot_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@


SNAP_NAME = 'snaptest'
SNAPALL_NAME = 'snapalltest'


def common_function(invoke_cli, jails, skip_test):
Expand All @@ -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):
Expand All @@ -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)
46 changes: 43 additions & 3 deletions tests/functional_tests/0019_list_snapshot_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,28 @@ def common_function(
jails_as_rows, full=False
):
for flag in SORTING_FLAGS:
command = ['snaplist', jail.name, '-s', flag]
if isinstance(jail, list):
command = ['snaplist', 'ALL', '-s', flag]
else:
command = ['snaplist', jail.name, '-s', flag]
if full:
command.append('-l')

result = invoke_cli(
command
)

orig_list = parse_rows_output(result.output, 'snapshot')
verify_list = jails_as_rows(jail.recursive_snapshots, full=full)
if isinstance(jail, 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))

Expand Down Expand Up @@ -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
)
Loading

0 comments on commit 704ad7c

Please sign in to comment.