diff --git a/docs/configuration.rst b/docs/configuration.rst index b737a3d26..f3db29801 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -294,9 +294,9 @@ Client Configuration This FURL tells the client how to connect to the introducer. Each Tahoe-LAFS grid is defined by an introducer. The introducer's FURL is - created by the introducer node and written into its base directory when - it starts, whereupon it should be published to everyone who wishes to - attach a client to that grid + created by the introducer node and written into its private base + directory when it starts, whereupon it should be published to everyone + who wishes to attach a client to that grid ``helper.furl = (FURL string, optional)`` @@ -507,7 +507,7 @@ the others. The Introducer node maintains some different state than regular client nodes. -``BASEDIR/introducer.furl`` +``BASEDIR/private/introducer.furl`` This is generated the first time the introducer node is started, and used again on subsequent runs, to give the introduction service a persistent diff --git a/docs/frontends/CLI.rst b/docs/frontends/CLI.rst index 34e90c948..b28556715 100644 --- a/docs/frontends/CLI.rst +++ b/docs/frontends/CLI.rst @@ -111,8 +111,8 @@ That is, it behaves like "``tahoe create-node --no-storage [NODEDIR]``". "``tahoe create-introducer [NODEDIR]``" is used to create the Introducer node. This node provides introduction services and nothing else. When started, this -node will produce an ``introducer.furl`` file, which should be published to all -clients. +node will produce a ``private/introducer.furl`` file, which should be +published to all clients. "``tahoe create-key-generator [NODEDIR]``" is used to create a special "key-generation" service, which allows a client to offload their RSA key diff --git a/docs/running.rst b/docs/running.rst index de697515c..50ad2cb42 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -47,10 +47,11 @@ To construct an introducer, create a new base directory for it (the name of the directory is up to you), ``cd`` into it, and run "``tahoe create-introducer .``". Now run the introducer using "``tahoe start .``". After it starts, it will write a file named -``introducer.furl`` in that base directory. This file contains the URL -the other nodes must use in order to connect to this introducer. (Note -that "``tahoe run .``" doesn't work for introducers, this is a known -issue: `#937 `_.) +``introducer.furl`` into the ``private/`` subdirectory of that base +directory. This file contains the URL the other nodes must use in order +to connect to this introducer. (Note that "``tahoe run .``" doesn't +work for introducers, this is a known issue: `#937 +`_.) The "``tahoe run``" command above will run the node in the foreground. On Unix, you can run it in the background instead by using the diff --git a/src/allmydata/introducer/server.py b/src/allmydata/introducer/server.py index adc68af3a..43a026114 100644 --- a/src/allmydata/introducer/server.py +++ b/src/allmydata/introducer/server.py @@ -1,17 +1,21 @@ -import time, os.path +import time, os.path, textwrap from zope.interface import implements from twisted.application import service from foolscap.api import Referenceable import allmydata from allmydata import node from allmydata.util import log, rrefutil +from allmydata.util.encodingutil import get_filesystem_encoding from allmydata.introducer.interfaces import \ RIIntroducerPublisherAndSubscriberService_v2 from allmydata.introducer.common import convert_announcement_v1_to_v2, \ convert_announcement_v2_to_v1, unsign_from_foolscap, make_index, \ get_tubid_string_from_ann, SubscriberDescriptor, AnnouncementDescriptor +class FurlFileConflictError(Exception): + pass + class IntroducerNode(node.Node): PORTNUMFILE = "introducer.port" NODETYPE = "introducer" @@ -29,13 +33,27 @@ class IntroducerNode(node.Node): introducerservice = IntroducerService(self.basedir) self.add_service(introducerservice) + old_public_fn = os.path.join(self.basedir, "introducer.furl").encode(get_filesystem_encoding()) + private_fn = os.path.join(self.basedir, "private", "introducer.furl").encode(get_filesystem_encoding()) + + if os.path.exists(old_public_fn): + if os.path.exists(private_fn): + msg = """This directory (%s) contains both an old public + 'introducer.furl' file, and a new-style + 'private/introducer.furl', so I cannot safely remove the old + one. Please make sure your desired FURL is in + private/introducer.furl, and remove the public file. If this + causes your Introducer's FURL to change, you need to inform + all grid members so they can update their tahoe.cfg. + """ + raise FurlFileConflictError(textwrap.dedent(msg)) + os.rename(old_public_fn, private_fn) d = self.when_tub_ready() def _publish(res): - self.introducer_url = self.tub.registerReference(introducerservice, - "introducer") - self.log(" introducer is at %s" % self.introducer_url, - umid="qF2L9A") - self.write_config("introducer.furl", self.introducer_url + "\n") + furl = self.tub.registerReference(introducerservice, + furlFile=private_fn) + self.log(" introducer is at %s" % furl, umid="qF2L9A") + self.introducer_url = furl # for tests d.addCallback(_publish) d.addErrback(log.err, facility="tahoe.init", level=log.BAD, umid="UaNs9A") diff --git a/src/allmydata/test/test_introducer.py b/src/allmydata/test/test_introducer.py index 0127d282e..15ddfe495 100644 --- a/src/allmydata/test/test_introducer.py +++ b/src/allmydata/test/test_introducer.py @@ -12,7 +12,7 @@ from twisted.application import service from allmydata.interfaces import InsufficientVersionError from allmydata.introducer.client import IntroducerClient, \ WrapV2ClientInV1Interface -from allmydata.introducer.server import IntroducerService +from allmydata.introducer.server import IntroducerService, FurlFileConflictError from allmydata.introducer.common import get_tubid_string_from_ann, \ get_tubid_string, sign_to_foolscap, unsign_from_foolscap, \ UnknownKeyError @@ -29,15 +29,49 @@ class LoggingMultiService(service.MultiService): log.msg(msg, **kw) class Node(testutil.SignalMixin, unittest.TestCase): - def test_loadable(self): - basedir = "introducer.IntroducerNode.test_loadable" + def test_furl(self): + basedir = "introducer.IntroducerNode.test_furl" os.mkdir(basedir) - q = IntroducerNode(basedir) + public_fn = os.path.join(basedir, "introducer.furl") + private_fn = os.path.join(basedir, "private", "introducer.furl") + q1 = IntroducerNode(basedir) d = fireEventually(None) - d.addCallback(lambda res: q.startService()) - d.addCallback(lambda res: q.when_tub_ready()) - d.addCallback(lambda res: q.stopService()) + d.addCallback(lambda res: q1.startService()) + d.addCallback(lambda res: q1.when_tub_ready()) + d.addCallback(lambda res: q1.stopService()) d.addCallback(flushEventualQueue) + def _check_furl(res): + # new nodes create unguessable furls in private/introducer.furl + ifurl = fileutil.read(private_fn) + self.failUnless(ifurl) + ifurl = ifurl.strip() + self.failIf(ifurl.endswith("/introducer"), ifurl) + + # old nodes created guessable furls in BASEDIR/introducer.furl + guessable = ifurl[:ifurl.rfind("/")] + "/introducer" + fileutil.write(public_fn, guessable+"\n", mode="w") # text + + # if we see both files, throw an error + self.failUnlessRaises(FurlFileConflictError, + IntroducerNode, basedir) + + # when we see only the public one, move it to private/ and use + # the existing furl instead of creating a new one + os.unlink(private_fn) + q2 = IntroducerNode(basedir) + d2 = fireEventually(None) + d2.addCallback(lambda res: q2.startService()) + d2.addCallback(lambda res: q2.when_tub_ready()) + d2.addCallback(lambda res: q2.stopService()) + d2.addCallback(flushEventualQueue) + def _check_furl2(res): + self.failIf(os.path.exists(public_fn)) + ifurl2 = fileutil.read(private_fn) + self.failUnless(ifurl2) + self.failUnlessEqual(ifurl2.strip(), guessable) + d2.addCallback(_check_furl2) + return d2 + d.addCallback(_check_furl) return d class ServiceMixin: diff --git a/src/allmydata/test/test_runner.py b/src/allmydata/test/test_runner.py index a7be1357a..aed220e4b 100644 --- a/src/allmydata/test/test_runner.py +++ b/src/allmydata/test/test_runner.py @@ -355,7 +355,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin, c1 = os.path.join(basedir, "c1") HOTLINE_FILE = os.path.join(c1, "suicide_prevention_hotline") TWISTD_PID_FILE = os.path.join(c1, "twistd.pid") - INTRODUCER_FURL_FILE = os.path.join(c1, "introducer.furl") + INTRODUCER_FURL_FILE = os.path.join(c1, "private", "introducer.furl") PORTNUM_FILE = os.path.join(c1, "introducer.port") NODE_URL_FILE = os.path.join(c1, "node.url") CONFIG_FILE = os.path.join(c1, "tahoe.cfg") @@ -406,8 +406,8 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin, d.addCallback(lambda res: self.poll(_node_has_started)) def _started(res): - # read the introducer.furl and introducer.port files so we can check that their - # contents don't change on restart + # read the introducer.furl and introducer.port files so we can + # check that their contents don't change on restart self.furl = fileutil.read(INTRODUCER_FURL_FILE) self.failUnless(os.path.exists(PORTNUM_FILE)) self.portnum = fileutil.read(PORTNUM_FILE)