diff --git a/flocker/volume/_ipc.py b/flocker/volume/_ipc.py index ad6c96b0ab..25a3a3594d 100644 --- a/flocker/volume/_ipc.py +++ b/flocker/volume/_ipc.py @@ -17,13 +17,27 @@ from zope.interface import Interface, implementer +from twisted.internet.defer import succeed + from .service import DEFAULT_CONFIG_PATH +from .filesystems.zfs import Snapshot class IRemoteVolumeManager(Interface): """ A remote volume manager with which one can communicate somehow. """ + def snapshots(volume): + """ + Retrieve a list of the snapshots which exist for the given volume. + + :param Volume volume: The volume for which to retrieve snapshots. + + :return: A ``Deferred`` that fires with a ``list`` of ``Snapshot`` + instances giving the snapshot information. The snapshots are + ordered from oldest to newest. + """ + def receive(volume): """ Context manager that returns a file-like object to which a volume's @@ -63,6 +77,24 @@ def __init__(self, destination, config_path=DEFAULT_CONFIG_PATH): self._destination = destination self._config_path = config_path + def snapshots(self, volume): + """ + Run ``flocker-volume snapshots`` on the destination and parse the + output into a ``list`` of ``Snapshot`` instances. + """ + data = self._destination.get_output( + [b"flocker-volume", + b"--config", self._config_path.path, + b"snapshots", + volume.uuid.encode("ascii"), + volume.name.encode("ascii")] + ) + return succeed([ + Snapshot(name=name) + for name + in data.splitlines() + ]) + def receive(self, volume): return self._destination.run([b"flocker-volume", b"--config", self._config_path.path, @@ -91,6 +123,12 @@ def __init__(self, service): """ self._service = service + def snapshots(self, volume): + """ + Interrogate the volume's filesystem for its snapshots. + """ + return volume.get_filesystem().snapshots() + @contextmanager def receive(self, volume): input_file = BytesIO() diff --git a/flocker/volume/functional/test_script.py b/flocker/volume/functional/test_script.py index b94ce7456a..d925a01117 100644 --- a/flocker/volume/functional/test_script.py +++ b/flocker/volume/functional/test_script.py @@ -79,3 +79,26 @@ def test_no_permission(self): self.assertEqual(result, b"Writing config file %s failed: Permission denied\n" % (config.path,)) + + +class FlockerVolumeSnapshotsTests(TestCase): + """ + Tests for ``flocker-volume snapshots``. + """ + @_require_installed + def test_snapshots(self): + """ + The output of ``flocker-volume snapshots`` is the name of each snapshot + that exists for the identified filesystem, one per line. + """ + pool_name = create_zfs_pool(self) + dataset = pool_name + b"/myuuid.myfilesystem" + check_output([b"zfs", b"create", b"-p", dataset]) + check_output([b"zfs", b"snapshot", dataset + b"@somesnapshot"]) + check_output([b"zfs", b"snapshot", dataset + b"@lastsnapshot"]) + config_path = FilePath(self.mktemp()) + snapshots = run( + b"--config", config_path.path, + b"--pool", pool_name, + b"snapshots", b"myuuid", b"myfilesystem") + self.assertEqual(snapshots, b"somesnapshot\nlastsnapshot\n") diff --git a/flocker/volume/script.py b/flocker/volume/script.py index 7236d2819d..ae6bebf631 100644 --- a/flocker/volume/script.py +++ b/flocker/volume/script.py @@ -12,7 +12,7 @@ from .service import ( DEFAULT_CONFIG_PATH, FLOCKER_MOUNTPOINT, FLOCKER_POOL, - VolumeScript, ICommandLineVolumeScript + Volume, VolumeScript, ICommandLineVolumeScript ) from ..common.script import ( flocker_standard_options, FlockerScriptRunner @@ -60,6 +60,37 @@ def postOptions(self): return cls +class _SnapshotsSubcommandOptions(Options): + """ + Command line options for ``flocker-volume snapshots``. + """ + + longdesc = """List local snapshots of a particular volume. + + Parameters: + + * owner-uuid: The UUID of the volume manager that owns the volume. + + * name: The name of the volume. + """ + + def parseArgs(self, uuid, name): + self["uuid"] = uuid.decode("ascii") + self["name"] = name.decode("ascii") + + def run(self, service): + volume = Volume(uuid=self["uuid"], name=self["name"], service=service) + filesystem = volume.get_filesystem() + snapshots = filesystem.snapshots() + + def got_snapshots(snapshots): + for snapshot in snapshots: + sys.stdout.write(snapshot.name + b"\n") + + snapshots.addCallback(got_snapshots) + return snapshots + + class _ReceiveSubcommandOptions(Options): """Command line options for ``flocker-volume receive``.""" @@ -140,6 +171,8 @@ class VolumeOptions(Options): synopsis = "Usage: flocker-volume [OPTIONS]" subCommands = [ + ["snapshots", None, _SnapshotsSubcommandOptions, + "List snapshots for a volume."], ["receive", None, _ReceiveSubcommandOptions, "Receive a remotely pushed volume."], ["acquire", None, _AcquireSubcommandOptions, diff --git a/flocker/volume/test/test_ipc.py b/flocker/volume/test/test_ipc.py index 2cca44100f..3787fde185 100644 --- a/flocker/volume/test/test_ipc.py +++ b/flocker/volume/test/test_ipc.py @@ -13,6 +13,7 @@ from twisted.trial.unittest import TestCase from ..service import VolumeService, Volume, DEFAULT_CONFIG_PATH +from ..filesystems.zfs import Snapshot from ..filesystems.memory import FilesystemStoragePool from .._ipc import ( IRemoteVolumeManager, RemoteVolumeManager, LocalVolumeManager) @@ -211,28 +212,56 @@ class LocalVolumeManagerInterfaceTests( """ Tests for ``LocalVolumeManager`` as a ``IRemoteVolumeManager``. """ + def test_snapshots(self): + """ + ``LocalVolumeManager.snapshots`` returns a ``Deferred`` that fires with + ``[]`` because ``DirectoryFilesystem`` does not support snapshots. + """ + pair = create_local_servicepair(self) + volume = self.successResultOf(pair.from_service.create(u"myvolume")) + self.assertEqual( + [], self.successResultOf(pair.remote.snapshots(volume))) class RemoteVolumeManagerTests(TestCase): """ Tests for ``RemoteVolumeManager``. """ + def setUp(self): + self.pool = FilesystemStoragePool(FilePath(self.mktemp())) + self.service = VolumeService( + FilePath(self.mktemp()), self.pool, reactor=Clock()) + self.service.startService() + self.volume = self.successResultOf(self.service.create(u"myvolume")) + + def test_snapshots_destination_run(self): + """ + ``RemoteVolumeManager.snapshots`` calls ``flocker-volume`` remotely + with the ``snapshots`` sub-command. + """ + node = FakeNode([b"abc\ndef\n"]) + + remote = RemoteVolumeManager(node, FilePath(b"/path/to/json")) + snapshots = self.successResultOf(remote.snapshots(self.volume)) + self.assertEqual(node.remote_command, + [b"flocker-volume", b"--config", b"/path/to/json", + b"snapshots", self.volume.uuid.encode("ascii"), + b"myvolume"]) + self.assertEqual( + [Snapshot(name="abc"), Snapshot(name="def")], snapshots) + def test_receive_destination_run(self): """ Receiving calls ``flocker-volume`` remotely with ``receive`` command. """ - pool = FilesystemStoragePool(FilePath(self.mktemp())) - service = VolumeService(FilePath(self.mktemp()), pool, reactor=Clock()) - service.startService() - volume = self.successResultOf(service.create(u"myvolume")) node = FakeNode() remote = RemoteVolumeManager(node, FilePath(b"/path/to/json")) - with remote.receive(volume): + with remote.receive(self.volume): pass self.assertEqual(node.remote_command, [b"flocker-volume", b"--config", b"/path/to/json", - b"receive", volume.uuid.encode("ascii"), + b"receive", self.volume.uuid.encode("ascii"), b"myvolume"]) def test_receive_default_config(self): @@ -240,19 +269,15 @@ def test_receive_default_config(self): ``RemoteVolumeManager`` by default calls ``flocker-volume`` with default config path. """ - pool = FilesystemStoragePool(FilePath(self.mktemp())) - service = VolumeService(FilePath(self.mktemp()), pool, reactor=Clock()) - service.startService() - volume = self.successResultOf(service.create(u"myvolume")) node = FakeNode() remote = RemoteVolumeManager(node) - with remote.receive(volume): + with remote.receive(self.volume): pass self.assertEqual(node.remote_command, [b"flocker-volume", b"--config", DEFAULT_CONFIG_PATH.path, - b"receive", volume.uuid.encode("ascii"), + b"receive", self.volume.uuid.encode("ascii"), b"myvolume"]) def test_acquire_destination_run(self): @@ -260,16 +285,12 @@ def test_acquire_destination_run(self): ``RemoteVolumeManager.acquire()`` calls ``flocker-volume`` remotely with ``acquire`` command. """ - pool = FilesystemStoragePool(FilePath(self.mktemp())) - service = VolumeService(FilePath(self.mktemp()), pool, reactor=Clock()) - service.startService() - volume = self.successResultOf(service.create(u"myvolume")) node = FakeNode([b"remoteuuid"]) remote = RemoteVolumeManager(node, FilePath(b"/path/to/json")) - remote.acquire(volume) + remote.acquire(self.volume) self.assertEqual(node.remote_command, [b"flocker-volume", b"--config", b"/path/to/json", - b"acquire", volume.uuid.encode("ascii"), + b"acquire", self.volume.uuid.encode("ascii"), b"myvolume"])