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

Convey snapshot information #658

Merged
merged 8 commits into from
Sep 8, 2014
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"])