66ae6bcfbd
Update to 2016.11.2 version, bugfixes OBS-URL: https://build.opensuse.org/request/show/458508 OBS-URL: https://build.opensuse.org/package/show/systemsmanagement:saltstack/salt?expand=0&rev=85
276 lines
11 KiB
Diff
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
|
|
|
|
|