diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index 1ec0f75305d2307cafeedd7187f08ef3e1779fa0..4ba84ddd840e6a56e7cf54fd0ed6ae8bc3449f40 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -25,6 +25,10 @@ from datetime import ( datetime, timedelta, ) +from functools import ( + partial, +) + import attr from zope.interface import ( @@ -45,6 +49,9 @@ from allmydata.interfaces import ( IFoolscapStoragePlugin, IAnnounceableStorageServer, ) +from allmydata.node import ( + MissingConfigEntry, +) from allmydata.client import ( _Client, ) @@ -238,9 +245,7 @@ def _create_maintenance_service(reactor, node_config, client_node): # Create the operation which performs the lease maintenance job when # called. maintain_leases = maintain_leases_from_root( - client_node.create_node_from_uri( - node_config.get_private_config(b"rootcap"), - ), + partial(get_root_nodes, client_node, node_config), client_node.get_storage_broker(), client_node._secret_holder, # Make this configuration @@ -256,3 +261,12 @@ def _create_maintenance_service(reactor, node_config, client_node): last_run_path, random, ) + + +def get_root_nodes(client_node, node_config): + try: + rootcap = node_config.get_private_config(b"rootcap") + except MissingConfigEntry: + return [] + else: + return [client_node.create_node_from_uri(rootcap)] diff --git a/src/_zkapauthorizer/lease_maintenance.py b/src/_zkapauthorizer/lease_maintenance.py index f32de8dff441b6832a906f32681182c7a9233f45..8a3a0956660984a5eb1b5d05cf8fb04759f6870d 100644 --- a/src/_zkapauthorizer/lease_maintenance.py +++ b/src/_zkapauthorizer/lease_maintenance.py @@ -50,6 +50,7 @@ from twisted.python.log import ( from allmydata.interfaces import ( IDirectoryNode, + IFilesystemNode, ) from allmydata.util.hashutil import ( file_renewal_secret_hash, @@ -68,7 +69,7 @@ SERVICE_NAME = u"lease maintenance service" @inlineCallbacks -def visit_storage_indexes(root_node, visit): +def visit_storage_indexes(root_nodes, visit): """ Call a visitor with the storage index of ``root_node`` and that of all nodes reachable from it. @@ -81,7 +82,17 @@ def visit_storage_indexes(root_node, visit): :return Deferred: A Deferred which fires after all nodes have been visited. """ - stack = [root_node] + if not isinstance(root_nodes, list): + raise TypeError("root_nodes must be a list, not {!r}".format( + root_nodes, + )) + for node in root_nodes: + if not IFilesystemNode.providedBy(node): + raise TypeError("Root nodes must provide IFilesystemNode, {!r} does not".format( + node, + )) + + stack = root_nodes[:] while stack: elem = stack.pop() visit(elem.get_storage_index()) @@ -438,7 +449,7 @@ def read_time_from_path(path): return parse_datetime(when) -def visit_storage_indexes_from_root(visitor, root_node): +def visit_storage_indexes_from_root(visitor, get_root_nodes): """ An operation for ``lease_maintenance_service`` which applies the given visitor to ``root_node`` and all its children. @@ -446,14 +457,18 @@ def visit_storage_indexes_from_root(visitor, root_node): :param visitor: A one-argument callable which takes the traversal function and which should call it as desired. - :param IFilesystemNode root_node: The filesystem node at which traversal - will begin. + :param get_root_nodes: A no-argument callable which returns a list of + filesystem nodes (``IFilesystemNode``) at which traversal will begin. :return: A no-argument callable to perform the visits. """ - return partial( - visitor, - partial(visit_storage_indexes, root_node), + return lambda: visitor( + partial( + visit_storage_indexes, + # Make sure we call get_root_nodes each time to give us a chance + # to notice when it changes. + get_root_nodes(), + ), ) @@ -486,7 +501,7 @@ class MemoryMaintenanceObserver(object): def maintain_leases_from_root( - root_node, + get_root_nodes, storage_broker, secret_holder, min_lease_remaining, @@ -498,8 +513,9 @@ def maintain_leases_from_root( and all its children and renews their leases if they have ``min_lease_remaining`` or less on them. - :param IFilesystemNode root_node: A Tahoe-LAFS filesystem node to use as - the root of a node hierarchy to be maintained. + :param get_root_nodes: A no-argument callable which returns the list of + Tahoe-LAFS filesystem nodes (``IFilesystemNode``) to use as the roots + of the node hierarchies to be maintained. :param StorageFarmBroker storage_broker: The storage broker which can put us in touch with storage servers where shares of the nodes to maintain @@ -529,7 +545,7 @@ def maintain_leases_from_root( return visit_storage_indexes_from_root( visitor, - root_node, + get_root_nodes, ) diff --git a/src/_zkapauthorizer/tests/test_lease_maintenance.py b/src/_zkapauthorizer/tests/test_lease_maintenance.py index 5b90fe3654839390e7a551bfc80b1d0a2bfd5160..4e0143ce899ee19136c8ed309e902d013d989ea6 100644 --- a/src/_zkapauthorizer/tests/test_lease_maintenance.py +++ b/src/_zkapauthorizer/tests/test_lease_maintenance.py @@ -411,7 +411,7 @@ class VisitStorageIndexesFromRootTests(TestCase): operation = visit_storage_indexes_from_root( perform_visit, - root_node, + lambda: [root_node], ) self.assertThat( @@ -516,7 +516,7 @@ class MaintainLeasesFromRootTests(TestCase): ) operation = maintain_leases_from_root( - root_node, + lambda: [root_node], storage_broker, secret_holder, min_lease_remaining, @@ -569,7 +569,7 @@ class MaintainLeasesFromRootTests(TestCase): observers = [observer] progress = observers.pop operation = maintain_leases_from_root( - root_node, + lambda: [root_node], storage_broker, secret_holder, min_lease_remaining, diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index c6b4e89d0cff64d7870ae4588cdd2f1bc725bdd1..55c30a5d2205b5427300359c427f33c95482dca1 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -441,7 +441,7 @@ class LeaseMaintenanceServiceTests(TestCase): """ Tests for the plugin's initialization of the lease maintenance service. """ - def _created_test(self, get_config, servers_yaml): + def _created_test(self, get_config, servers_yaml, rootcap): original_tempdir = tempfile.tempdir tempdir = self.useFixture(TempDir()) @@ -455,10 +455,11 @@ class LeaseMaintenanceServiceTests(TestCase): b"servers.yaml", servers_yaml, ) - config.write_private_config( - b"rootcap", - b"dddddddd", - ) + if rootcap: + config.write_private_config( + b"rootcap", + b"dddddddd", + ) try: d = create_client_from_config(config) @@ -492,4 +493,19 @@ class LeaseMaintenanceServiceTests(TestCase): maintenance service after it has at least one storage server to connect to. """ - return self._created_test(get_config, servers_yaml) + return self._created_test(get_config, servers_yaml, rootcap=True) + + + @settings( + deadline=None, + ) + @given( + tahoe_configs_with_dummy_redeemer, + sampled_from([SERVERS_YAML, TWO_SERVERS_YAML]), + ) + def test_created_without_rootcap(self, get_config, servers_yaml): + """ + The lease maintenance service can be created even if no rootcap has yet + been written to the client's configuration directory. + """ + return self._created_test(get_config, servers_yaml, rootcap=False)