From 5d0e45fa991287cc7b5958ec268df6a39f96f258 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Fri, 20 Dec 2019 14:41:25 -0500
Subject: [PATCH] Implement the monkey-patch-based solution to get the service
 started

---
 src/_zkapauthorizer/_plugin.py           |  94 ++++++++++++++++++++-
 src/_zkapauthorizer/tests/test_plugin.py | 101 ++++++++++++++++++++++-
 2 files changed, 190 insertions(+), 5 deletions(-)

diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py
index 0711f2c..81d7479 100644
--- a/src/_zkapauthorizer/_plugin.py
+++ b/src/_zkapauthorizer/_plugin.py
@@ -17,19 +17,23 @@ The Twisted plugin that glues the Zero-Knowledge Access Pass system into
 Tahoe-LAFS.
 """
 
+import random
 from weakref import (
     WeakValueDictionary,
 )
 from datetime import (
     datetime,
+    timedelta,
 )
-
 import attr
 
 from zope.interface import (
     implementer,
 )
 
+from twisted.logger import (
+    Logger,
+)
 from twisted.python.filepath import (
     FilePath,
 )
@@ -41,6 +45,9 @@ from allmydata.interfaces import (
     IFoolscapStoragePlugin,
     IAnnounceableStorageServer,
 )
+from allmydata.client import (
+    _Client,
+)
 from privacypass import (
     SigningKey,
 )
@@ -61,6 +68,13 @@ from .resource import (
 from .controller import (
     get_redeemer,
 )
+from .lease_maintenance import (
+    SERVICE_NAME,
+    lease_maintenance_service,
+    maintain_leases_from_root,
+)
+
+_log = Logger()
 
 @implementer(IAnnounceableStorageServer)
 @attr.s
@@ -69,8 +83,8 @@ class AnnounceableStorageServer(object):
     storage_server = attr.ib()
 
 
-@attr.s
 @implementer(IFoolscapStoragePlugin)
+@attr.s
 class ZKAPAuthorizer(object):
     """
     A storage plugin which provides a token-based access control mechanism on
@@ -153,7 +167,6 @@ class ZKAPAuthorizer(object):
             get_passes,
         )
 
-
     def get_client_resource(self, node_config):
         from twisted.internet import reactor
         return resource_from_configuration(
@@ -161,3 +174,78 @@ class ZKAPAuthorizer(object):
             store=self._get_store(node_config),
             redeemer=self._get_redeemer(node_config, None, reactor),
         )
+
+
+_init_storage = _Client.__dict__["init_storage"]
+def maintenance_init_storage(self, announceable_storage_servers):
+    """
+    A monkey-patched version of ``_Client.init_storage`` which also
+    initializes the lease maintenance service.
+    """
+    from twisted.internet import reactor
+    try:
+        return _init_storage(self, announceable_storage_servers)
+    finally:
+        _maybe_attach_maintenance_service(reactor, self)
+_Client.init_storage = maintenance_init_storage
+
+
+def _maybe_attach_maintenance_service(reactor, client_node):
+    """
+    Check for an existing lease maintenance service and if one is not found,
+    create one.
+
+    :param allmydata.client._Client client_node: The client node to check and,
+        possibly, modify.  A lease maintenance service is added to it if and
+        only if one is not already present.
+    """
+    try:
+        # If there is already one we don't need another.
+        client_node.getServiceNamed(SERVICE_NAME)
+    except KeyError:
+        # There isn't one so make it and add it.
+        _log.info("Creating new lease maintenance service")
+        _create_maintenance_service(
+            reactor,
+            client_node.config,
+            client_node,
+        ).setServiceParent(client_node)
+    except Exception as e:
+        _log.failure("Attaching maintenance service to client node")
+    else:
+        _log.info("Found existing lease maintenance service")
+
+
+def _create_maintenance_service(reactor, node_config, client_node):
+    """
+    Create a lease maintenance service to be attached to the given client
+    node.
+
+    :param allmydata.node._Config node_config: The configuration for the node
+        the lease maintenance service will be attached to.
+
+    :param allmydata.client._Client client_node: The client node the lease
+        maintenance service will be attached to.
+    """
+    def get_now():
+        return datetime.utcfromtimestamp(reactor.seconds())
+
+    # 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(u"rootcap"),
+        ),
+        client_node.get_storage_broker(),
+        client_node._secret_holder,
+        # Make this configuration
+        timedelta(days=3),
+        get_now,
+    )
+    # Create the service to periodically run the lease maintenance operation.
+    return lease_maintenance_service(
+        maintain_leases,
+        reactor,
+        node_config.get_private_config(u"last-lease-maintenance-run", None),
+        random,
+    )
diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py
index 9c97ea7..750f0b9 100644
--- a/src/_zkapauthorizer/tests/test_plugin.py
+++ b/src/_zkapauthorizer/tests/test_plugin.py
@@ -23,7 +23,10 @@ from __future__ import (
 from io import (
     BytesIO,
 )
-
+from os import (
+    makedirs,
+)
+import tempfile
 from zope.interface import (
     implementer,
 )
@@ -53,6 +56,7 @@ from hypothesis import (
 from hypothesis.strategies import (
     just,
     datetimes,
+    sampled_from,
 )
 from foolscap.broker import (
     Broker,
@@ -71,6 +75,9 @@ from allmydata.interfaces import (
     IStorageServer,
     RIStorageServer,
 )
+from allmydata.client import (
+    create_client_from_config,
+)
 
 from twisted.python.filepath import (
     FilePath,
@@ -94,6 +101,9 @@ from ..model import (
 from ..controller import (
     IssuerConfigurationMismatch,
 )
+from ..lease_maintenance import (
+    SERVICE_NAME,
+)
 
 from .strategies import (
     minimal_tahoe_configs,
@@ -114,7 +124,6 @@ from .matchers import (
     Provides,
 )
 
-
 SIGNING_KEY_PATH = FilePath(__file__).sibling(u"testing-signing.key")
 
 
@@ -391,3 +400,91 @@ class ClientResourceTests(TestCase):
             storage_server.get_client_resource(config),
             Provides([IResource]),
         )
+
+
+SERVERS_YAML = b"""
+storage:
+  v0-aaaaaaaa:
+    ann:
+      anonymous-storage-FURL: pb://@tcp:/
+      nickname: 10.0.0.2
+      storage-options:
+      - name: privatestorageio-zkapauthz-v1
+        ristretto-issuer-root-url: https://payments.example.com/
+        storage-server-FURL: pb://bbbbbbbb@tcp:10.0.0.2:1234/cccccccc
+"""
+
+TWO_SERVERS_YAML = b"""
+storage:
+  v0-aaaaaaaa:
+    ann:
+      anonymous-storage-FURL: pb://@tcp:/
+      nickname: 10.0.0.2
+      storage-options:
+      - name: privatestorageio-zkapauthz-v1
+        ristretto-issuer-root-url: https://payments.example.com/
+        storage-server-FURL: pb://bbbbbbbb@tcp:10.0.0.2:1234/cccccccc
+  v0-dddddddd:
+    ann:
+      anonymous-storage-FURL: pb://@tcp:/
+      nickname: 10.0.0.3
+      storage-options:
+      - name: privatestorageio-zkapauthz-v1
+        ristretto-issuer-root-url: https://payments.example.com/
+        storage-server-FURL: pb://eeeeeeee@tcp:10.0.0.3:1234/ffffffff
+"""
+
+
+class LeaseMaintenanceServiceTests(TestCase):
+    """
+    Tests for the plugin's initialization of the lease maintenance service.
+    """
+    def _created_test(self, get_config, servers_yaml):
+        original_tempdir = tempfile.tempdir
+
+        tempdir = self.useFixture(TempDir())
+        nodedir = tempdir.join(b"node")
+        privatedir = tempdir.join(b"node", b"private")
+        makedirs(privatedir)
+        config = get_config(nodedir, b"tub.port")
+
+        # Provide it a statically configured server to connect to.
+        config.write_private_config(
+            b"servers.yaml",
+            servers_yaml,
+        )
+        config.write_private_config(
+            b"rootcap",
+            b"dddddddd",
+        )
+
+        try:
+            d = create_client_from_config(config)
+            self.assertThat(
+                d,
+                succeeded(
+                    AfterPreprocessing(
+                        lambda client: client.getServiceNamed(SERVICE_NAME),
+                        Always(),
+                    ),
+                ),
+            )
+        finally:
+            # create_client_from_config (indirectly) rewrites tempfile.tempdir
+            # in a destructive manner that fails most of the rest of the test
+            # suite if we don't clean it up.  We can do this with a tearDown
+            # or a fixture or an addCleanup because hypothesis doesn't run any
+            # of those at the right time. :/
+           tempfile.tempdir = original_tempdir
+
+    @given(
+        tahoe_configs_with_dummy_redeemer,
+        sampled_from([SERVERS_YAML, TWO_SERVERS_YAML]),
+    )
+    def test_created(self, get_config, servers_yaml):
+        """
+        A client created from a configuration with the plugin enabled has a lease
+        maintenance service after it has at least one storage server to
+        connect to.
+        """
+        return self._created_test(get_config, servers_yaml)
-- 
GitLab