Skip to content

Commit

Permalink
Merge pull request #658 from ClusterHQ/use-snapshot-information-46
Browse files Browse the repository at this point in the history
Expand the remote volume manager interface so it can convey information about the snapshots that exist for the filesystems it manages.
  • Loading branch information
exarkun committed Sep 8, 2014
2 parents 4aff72a + d9381d6 commit e38b3d2
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 19 deletions.
38 changes: 38 additions & 0 deletions flocker/volume/_ipc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
23 changes: 23 additions & 0 deletions flocker/volume/functional/test_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
35 changes: 34 additions & 1 deletion flocker/volume/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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``."""

Expand Down Expand Up @@ -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,
Expand Down
57 changes: 39 additions & 18 deletions flocker/volume/test/test_ipc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -211,65 +212,85 @@ 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):
"""
``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):
"""
``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"])

0 comments on commit e38b3d2

Please sign in to comment.