Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ALL target for snapshot, snaplist and snapremove #32

Merged
merged 2 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"])
Defenso-QTH marked this conversation as resolved.
Show resolved Hide resolved
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)
Defenso-QTH marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading