SHA256
1
0
forked from pool/salt
salt/snapper-module-improvements.patch

276 lines
11 KiB
Diff

From 678de7117211fc359c9aa7e29f6c2fecf0944b08 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
<psuarezhernandez@suse.com>
Date: Fri, 27 Jan 2017 17:07:25 +0000
Subject: [PATCH] Snapper module improvements
* Snapper: Adding support for deleting snapshots
* Snapper: Adding support for snapshot metadata modification
* Snapper: Adding support for creating configurations
* Adds 'snapper.delete_snapshots' unit tests
* Adds 'snapper.modify_snapshots' unit tests
* Adds 'snapper.create_config' unit tests
* Removing extra spaces
* pylint fixes
---
salt/modules/snapper.py | 159 +++++++++++++++++++++++++++++++++++--
tests/unit/modules/snapper_test.py | 50 ++++++++++++
2 files changed, 201 insertions(+), 8 deletions(-)
diff --git a/salt/modules/snapper.py b/salt/modules/snapper.py
index 318ce9b99d..d5f1181743 100644
--- a/salt/modules/snapper.py
+++ b/salt/modules/snapper.py
@@ -290,6 +290,60 @@ def get_config(name='root'):
)
+def create_config(name=None,
+ subvolume=None,
+ fstype=None,
+ template=None,
+ extra_opts=None):
+ '''
+ Creates a new Snapper configuration
+
+ name
+ Name of the new Snapper configuration.
+ subvolume
+ Path to the related subvolume.
+ fstype
+ Filesystem type of the subvolume.
+ template
+ Configuration template to use. (Default: default)
+ extra_opts
+ Extra Snapper configuration opts dictionary. It will override the values provided
+ by the given template (if any).
+
+ CLI example:
+
+ .. code-block:: bash
+
+ salt '*' snapper.create_config name=myconfig subvolume=/foo/bar/ fstype=btrfs
+ salt '*' snapper.create_config name=myconfig subvolume=/foo/bar/ fstype=btrfs template="default"
+ salt '*' snapper.create_config name=myconfig subvolume=/foo/bar/ fstype=btrfs extra_opts='{"NUMBER_CLEANUP": False}'
+ '''
+ def raise_arg_error(argname):
+ raise CommandExecutionError(
+ 'You must provide a "{0}" for the new configuration'.format(argname)
+ )
+
+ if not name:
+ raise_arg_error("name")
+ if not subvolume:
+ raise_arg_error("subvolume")
+ if not fstype:
+ raise_arg_error("fstype")
+ if not template:
+ template = ""
+
+ try:
+ snapper.CreateConfig(name, subvolume, fstype, template)
+ if extra_opts:
+ set_config(name, **extra_opts)
+ return get_config(name)
+ except dbus.DBusException as exc:
+ raise CommandExecutionError(
+ 'Error encountered while creating the new configuration: {0}'
+ .format(_dbus_exception_to_reason(exc, locals()))
+ )
+
+
def create_snapshot(config='root', snapshot_type='single', pre_number=None,
description=None, cleanup_algorithm='number', userdata=None,
**kwargs):
@@ -309,14 +363,14 @@ def create_snapshot(config='root', snapshot_type='single', pre_number=None,
cleanup_algorithm
Set the cleanup algorithm for the snapshot.
- number
- Deletes old snapshots when a certain number of snapshots
- is reached.
- timeline
- Deletes old snapshots but keeps a number of hourly,
- daily, weekly, monthly and yearly snapshots.
- empty-pre-post
- Deletes pre/post snapshot pairs with empty diffs.
+ number
+ Deletes old snapshots when a certain number of snapshots
+ is reached.
+ timeline
+ Deletes old snapshots but keeps a number of hourly,
+ daily, weekly, monthly and yearly snapshots.
+ empty-pre-post
+ Deletes pre/post snapshot pairs with empty diffs.
userdata
Set userdata for the snapshot (key-value pairs).
@@ -364,6 +418,95 @@ def create_snapshot(config='root', snapshot_type='single', pre_number=None,
return new_nr
+def delete_snapshot(snapshots_ids=None, config="root"):
+ '''
+ Deletes an snapshot
+
+ config
+ Configuration name. (Default: root)
+
+ snapshots_ids
+ List of the snapshots IDs to be deleted.
+
+ CLI example:
+
+ .. code-block:: bash
+
+ salt '*' snapper.delete_snapshot 54
+ salt '*' snapper.delete_snapshot config=root 54
+ salt '*' snapper.delete_snapshot config=root snapshots_ids=[54,55,56]
+ '''
+ if not snapshots_ids:
+ raise CommandExecutionError('Error: No snapshot ID has been provided')
+ try:
+ current_snapshots_ids = [x['id'] for x in list_snapshots(config)]
+ if not isinstance(snapshots_ids, list):
+ snapshots_ids = [snapshots_ids]
+ if not set(snapshots_ids).issubset(set(current_snapshots_ids)):
+ raise CommandExecutionError(
+ "Error: Snapshots '{0}' not found".format(", ".join(
+ [str(x) for x in set(snapshots_ids).difference(
+ set(current_snapshots_ids))]))
+ )
+ snapper.DeleteSnapshots(config, snapshots_ids)
+ return {config: {"ids": snapshots_ids, "status": "deleted"}}
+ except dbus.DBusException as exc:
+ raise CommandExecutionError(_dbus_exception_to_reason(exc, locals()))
+
+
+def modify_snapshot(snapshot_id=None,
+ description=None,
+ userdata=None,
+ cleanup=None,
+ config="root"):
+ '''
+ Modify attributes of an existing snapshot.
+
+ config
+ Configuration name. (Default: root)
+
+ snapshot_id
+ ID of the snapshot to be modified.
+
+ cleanup
+ Change the cleanup method of the snapshot. (str)
+
+ description
+ Change the description of the snapshot. (str)
+
+ userdata
+ Change the userdata dictionary of the snapshot. (dict)
+
+ CLI example:
+
+ .. code-block:: bash
+
+ salt '*' snapper.modify_snapshot 54 description="my snapshot description"
+ salt '*' snapper.modify_snapshot 54 description="my snapshot description"
+ salt '*' snapper.modify_snapshot 54 userdata='{"foo": "bar"}'
+ salt '*' snapper.modify_snapshot snapshot_id=54 cleanup="number"
+ '''
+ if not snapshot_id:
+ raise CommandExecutionError('Error: No snapshot ID has been provided')
+
+ snapshot = get_snapshot(config=config, number=snapshot_id)
+ try:
+ # Updating only the explicitely provided attributes by the user
+ updated_opts = {
+ 'description': description if description is not None else snapshot['description'],
+ 'cleanup': cleanup if cleanup is not None else snapshot['cleanup'],
+ 'userdata': userdata if userdata is not None else snapshot['userdata'],
+ }
+ snapper.SetSnapshot(config,
+ snapshot_id,
+ updated_opts['description'],
+ updated_opts['cleanup'],
+ updated_opts['userdata'])
+ return get_snapshot(config=config, number=snapshot_id)
+ except dbus.DBusException as exc:
+ raise CommandExecutionError(_dbus_exception_to_reason(exc, locals()))
+
+
def _get_num_interval(config, num_pre, num_post):
'''
Returns numerical interval based on optionals num_pre, num_post values
diff --git a/tests/unit/modules/snapper_test.py b/tests/unit/modules/snapper_test.py
index ca985cfd05..a5d9b7686e 100644
--- a/tests/unit/modules/snapper_test.py
+++ b/tests/unit/modules/snapper_test.py
@@ -202,6 +202,26 @@ class SnapperTestCase(TestCase):
self.assertEqual(snapper.status_to_string(128), ["extended attributes changed"])
self.assertEqual(snapper.status_to_string(256), ["ACL info changed"])
+ @patch('salt.modules.snapper.snapper.CreateConfig', MagicMock())
+ @patch('salt.modules.snapper.snapper.GetConfig', MagicMock(return_value=DBUS_RET['ListConfigs'][0]))
+ def test_create_config(self):
+ opts = {
+ 'name': 'testconfig',
+ 'subvolume': '/foo/bar/',
+ 'fstype': 'btrfs',
+ 'template': 'mytemplate',
+ 'extra_opts': {"NUMBER_CLEANUP": False},
+ }
+ with patch('salt.modules.snapper.set_config', MagicMock()) as set_config_mock:
+ self.assertEqual(snapper.create_config(**opts), DBUS_RET['ListConfigs'][0])
+ set_config_mock.assert_called_with("testconfig", **opts['extra_opts'])
+
+ with patch('salt.modules.snapper.set_config', MagicMock()) as set_config_mock:
+ del opts['extra_opts']
+ self.assertEqual(snapper.create_config(**opts), DBUS_RET['ListConfigs'][0])
+ assert not set_config_mock.called
+ self.assertRaises(CommandExecutionError, snapper.create_config)
+
@patch('salt.modules.snapper.snapper.CreateSingleSnapshot', MagicMock(return_value=1234))
@patch('salt.modules.snapper.snapper.CreatePreSnapshot', MagicMock(return_value=1234))
@patch('salt.modules.snapper.snapper.CreatePostSnapshot', MagicMock(return_value=1234))
@@ -216,6 +236,36 @@ class SnapperTestCase(TestCase):
}
self.assertEqual(snapper.create_snapshot(**opts), 1234)
+ @patch('salt.modules.snapper.snapper.DeleteSnapshots', MagicMock())
+ @patch('salt.modules.snapper.snapper.ListSnapshots', MagicMock(return_value=DBUS_RET['ListSnapshots']))
+ def test_delete_snapshot_id_success(self):
+ self.assertEqual(snapper.delete_snapshot(snapshots_ids=43), {"root": {"ids": [43], "status": "deleted"}})
+ self.assertEqual(snapper.delete_snapshot(snapshots_ids=[42, 43]), {"root": {"ids": [42, 43], "status": "deleted"}})
+
+ @patch('salt.modules.snapper.snapper.DeleteSnapshots', MagicMock())
+ @patch('salt.modules.snapper.snapper.ListSnapshots', MagicMock(return_value=DBUS_RET['ListSnapshots']))
+ def test_delete_snapshot_id_fail(self):
+ self.assertRaises(CommandExecutionError, snapper.delete_snapshot)
+ self.assertRaises(CommandExecutionError, snapper.delete_snapshot, snapshots_ids=1)
+ self.assertRaises(CommandExecutionError, snapper.delete_snapshot, snapshots_ids=[1, 2])
+
+ @patch('salt.modules.snapper.snapper.SetSnapshot', MagicMock())
+ def test_modify_snapshot(self):
+ _ret = {
+ 'userdata': {'userdata2': 'uservalue2'},
+ 'description': 'UPDATED DESCRIPTION', 'timestamp': 1457006571,
+ 'cleanup': 'number', 'user': 'root', 'type': 'pre', 'id': 42
+ }
+ _opts = {
+ 'config': 'root',
+ 'snapshot_id': 42,
+ 'cleanup': 'number',
+ 'description': 'UPDATED DESCRIPTION',
+ 'userdata': {'userdata2': 'uservalue2'},
+ }
+ with patch('salt.modules.snapper.get_snapshot', MagicMock(side_effect=[DBUS_RET['ListSnapshots'][0], _ret])):
+ self.assertDictEqual(snapper.modify_snapshot(**_opts), _ret)
+
@patch('salt.modules.snapper._get_last_snapshot', MagicMock(return_value={'id': 42}))
def test__get_num_interval(self):
self.assertEqual(snapper._get_num_interval(config=None, num_pre=None, num_post=None), (42, 0)) # pylint: disable=protected-access
--
2.11.0