source: trunk/src/allmydata/test/cli/test_create.py

Last change on this file was 02734db2, checked in by meejah <meejah@…>, at 2024-09-26T02:36:40Z

basic test for --storage-dir

  • Property mode set to 100644
File size: 22.9 KB
Line 
1"""
2Ported to Python 3.
3"""
4from __future__ import annotations
5
6import os
7
8from typing import Any
9
10from twisted.trial import unittest
11from twisted.internet import defer, reactor
12from twisted.python import usage
13from allmydata.util import configutil
14from allmydata.util import tor_provider, i2p_provider
15from ..common_util import run_cli, parse_cli
16from ..common import (
17    disable_modules,
18)
19from ...scripts import create_node
20from ...listeners import ListenerConfig, StaticProvider
21from ... import client
22
23def read_config(basedir):
24    tahoe_cfg = os.path.join(basedir, "tahoe.cfg")
25    config = configutil.get_config(tahoe_cfg)
26    return config
27
28class MergeConfigTests(unittest.TestCase):
29    """
30    Tests for ``create_node.merge_config``.
31    """
32    def test_disable_left(self) -> None:
33        """
34        If the left argument to ``create_node.merge_config`` is ``None``
35        then the return value is ``None``.
36        """
37        conf = ListenerConfig([], [], {})
38        self.assertEqual(None, create_node.merge_config(None, conf))
39
40    def test_disable_right(self) -> None:
41        """
42        If the right argument to ``create_node.merge_config`` is ``None``
43        then the return value is ``None``.
44        """
45        conf = ListenerConfig([], [], {})
46        self.assertEqual(None, create_node.merge_config(conf, None))
47
48    def test_disable_both(self) -> None:
49        """
50        If both arguments to ``create_node.merge_config`` are ``None``
51        then the return value is ``None``.
52        """
53        self.assertEqual(None, create_node.merge_config(None, None))
54
55    def test_overlapping_keys(self) -> None:
56        """
57        If there are any keys in the ``node_config`` of the left and right
58        parameters that are shared then ``ValueError`` is raised.
59        """
60        left = ListenerConfig([], [], {"foo": [("b", "ar")]})
61        right = ListenerConfig([], [], {"foo": [("ba", "z")]})
62        self.assertRaises(ValueError, lambda: create_node.merge_config(left, right))
63
64    def test_merge(self) -> None:
65        """
66        ``create_node.merge_config`` returns a ``ListenerConfig`` that has
67        all of the ports, locations, and node config from each of the two
68        ``ListenerConfig`` values given.
69        """
70        left = ListenerConfig(
71            ["left-port"],
72            ["left-location"],
73            {"left": [("f", "oo")]},
74        )
75        right = ListenerConfig(
76            ["right-port"],
77            ["right-location"],
78            {"right": [("ba", "r")]},
79        )
80        result = create_node.merge_config(left, right)
81        self.assertEqual(
82            ListenerConfig(
83                ["left-port", "right-port"],
84                ["left-location", "right-location"],
85                {"left": [("f", "oo")], "right": [("ba", "r")]},
86            ),
87            result,
88        )
89
90class Config(unittest.TestCase):
91    def test_client_unrecognized_options(self):
92        tests = [
93            ("--listen", "create-client", "--listen=tcp"),
94            ("--hostname", "create-client", "--hostname=computer"),
95            ("--port",
96             "create-client", "--port=unix:/var/tahoe/socket",
97             "--location=tor:myservice.onion:12345"),
98            ("--port", "create-client", "--port=unix:/var/tahoe/socket"),
99            ("--location",
100             "create-client", "--location=tor:myservice.onion:12345"),
101            ("--listen", "create-client", "--listen=tor"),
102            ("--listen", "create-client", "--listen=i2p"),
103                ]
104        for test in tests:
105            option = test[0]
106            verb = test[1]
107            args = test[2:]
108            e = self.assertRaises(usage.UsageError, parse_cli, verb, *args)
109            self.assertIn("option %s not recognized" % (option,), str(e))
110
111    async def test_create_client_config(self):
112        """
113        ``create_node.write_client_config`` writes a configuration file
114        that can be parsed.
115
116        TODO Maybe we should test that we can recover the given configuration
117        from the parse, too.
118        """
119        d = self.mktemp()
120        os.mkdir(d)
121        fname = os.path.join(d, 'tahoe.cfg')
122
123        with open(fname, 'w') as f:
124            opts = {"nickname": "nick",
125                    "webport": "tcp:3456",
126                    "hide-ip": False,
127                    "listen": "none",
128                    "shares-needed": "1",
129                    "shares-happy": "1",
130                    "shares-total": "1",
131                    }
132            await create_node.write_node_config(f, opts)
133            create_node.write_client_config(f, opts)
134
135        # should succeed, no exceptions
136        client.read_config(d, "")
137
138    @defer.inlineCallbacks
139    def test_client(self):
140        basedir = self.mktemp()
141        rc, out, err = yield run_cli("create-client", basedir)
142        cfg = read_config(basedir)
143        self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), True)
144        self.assertEqual(cfg.get("node", "tub.port"), "disabled")
145        self.assertEqual(cfg.get("node", "tub.location"), "disabled")
146        self.assertFalse(cfg.has_section("connections"))
147
148    @defer.inlineCallbacks
149    def test_non_default_storage_args(self):
150        basedir = self.mktemp()
151        rc, out, err = yield run_cli(
152            "create-client",
153            '--shares-total', '19',
154            '--shares-needed', '2',
155            '--shares-happy', '11',
156            basedir,
157        )
158        cfg = read_config(basedir)
159        self.assertEqual(2, cfg.getint("client", "shares.needed"))
160        self.assertEqual(11, cfg.getint("client", "shares.happy"))
161        self.assertEqual(19, cfg.getint("client", "shares.total"))
162
163    @defer.inlineCallbacks
164    def test_illegal_shares_total(self):
165        basedir = self.mktemp()
166        rc, out, err = yield run_cli(
167            "create-client",
168            '--shares-total', 'funballs',
169            basedir,
170        )
171        self.assertNotEqual(0, rc)
172        self.assertTrue('--shares-total must be an integer' in err + out)
173
174    @defer.inlineCallbacks
175    def test_client_hide_ip_no_i2p_txtorcon(self):
176        """
177        The ``create-client`` sub-command tells the user to install the necessary
178        dependencies if they have neither tor nor i2p support installed and
179        they request network location privacy with the ``--hide-ip`` flag.
180        """
181        with disable_modules("txi2p", "txtorcon"):
182            basedir = self.mktemp()
183            rc, out, err = yield run_cli("create-client", "--hide-ip", basedir)
184            self.assertTrue(rc != 0, out)
185            self.assertTrue('pip install tahoe-lafs[i2p]' in out)
186            self.assertTrue('pip install tahoe-lafs[tor]' in out)
187
188    @defer.inlineCallbacks
189    def test_client_i2p_option_no_txi2p(self):
190        with disable_modules("txi2p"):
191            basedir = self.mktemp()
192            rc, out, err = yield run_cli("create-node", "--listen=i2p", "--i2p-launch", basedir)
193            self.assertTrue(rc != 0)
194            self.assertTrue("Specifying any I2P options requires the 'txi2p' module" in out)
195
196    @defer.inlineCallbacks
197    def test_client_tor_option_no_txtorcon(self):
198        with disable_modules("txtorcon"):
199            basedir = self.mktemp()
200            rc, out, err = yield run_cli("create-node", "--listen=tor", "--tor-launch", basedir)
201            self.assertTrue(rc != 0)
202            self.assertTrue("Specifying any Tor options requires the 'txtorcon' module" in out)
203
204    @defer.inlineCallbacks
205    def test_client_hide_ip(self):
206        basedir = self.mktemp()
207        rc, out, err = yield run_cli("create-client", "--hide-ip", basedir)
208        self.assertEqual(0, rc)
209        cfg = read_config(basedir)
210        self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), False)
211        self.assertEqual(cfg.get("connections", "tcp"), "tor")
212
213    @defer.inlineCallbacks
214    def test_client_hide_ip_no_txtorcon(self):
215        with disable_modules("txtorcon"):
216            basedir = self.mktemp()
217            rc, out, err = yield run_cli("create-client", "--hide-ip", basedir)
218            self.assertEqual(0, rc)
219            cfg = read_config(basedir)
220            self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), False)
221            self.assertEqual(cfg.get("connections", "tcp"), "disabled")
222
223    @defer.inlineCallbacks
224    def test_client_basedir_exists(self):
225        basedir = self.mktemp()
226        os.mkdir(basedir)
227        with open(os.path.join(basedir, "foo"), "w") as f:
228            f.write("blocker")
229        rc, out, err = yield run_cli("create-client", basedir)
230        self.assertEqual(rc, -1)
231        self.assertIn(basedir, err)
232        self.assertIn("is not empty", err)
233        self.assertIn("To avoid clobbering anything, I am going to quit now", err)
234
235    @defer.inlineCallbacks
236    def test_node(self):
237        basedir = self.mktemp()
238        rc, out, err = yield run_cli("create-node", "--hostname=foo", basedir)
239        cfg = read_config(basedir)
240        self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), True)
241        self.assertFalse(cfg.has_section("connections"))
242
243    @defer.inlineCallbacks
244    def test_storage_dir(self):
245        basedir = self.mktemp()
246        rc, out, err = yield run_cli("create-node", "--storage-dir", "/tmp/storage", "--hostname=foo", basedir)
247        cfg = read_config(basedir)
248        self.assertEqual(cfg.get("storage", "storage_dir"), "/tmp/storage")
249
250    @defer.inlineCallbacks
251    def test_node_hide_ip(self):
252        basedir = self.mktemp()
253        rc, out, err = yield run_cli("create-node", "--hide-ip",
254                                     "--hostname=foo", basedir)
255        cfg = read_config(basedir)
256        self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), False)
257        self.assertEqual(cfg.get("connections", "tcp"), "tor")
258
259    @defer.inlineCallbacks
260    def test_node_hostname(self):
261        basedir = self.mktemp()
262        rc, out, err = yield run_cli("create-node", "--hostname=computer", basedir)
263        cfg = read_config(basedir)
264        port = cfg.get("node", "tub.port")
265        location = cfg.get("node", "tub.location")
266        self.assertRegex(port, r'^tcp:\d+$')
267        self.assertRegex(location, r'^tcp:computer:\d+$')
268
269    @defer.inlineCallbacks
270    def test_node_port_location(self):
271        basedir = self.mktemp()
272        rc, out, err = yield run_cli("create-node",
273                                     "--port=unix:/var/tahoe/socket",
274                                     "--location=tor:myservice.onion:12345",
275                                     basedir)
276        cfg = read_config(basedir)
277        self.assertEqual(cfg.get("node", "tub.location"), "tor:myservice.onion:12345")
278        self.assertEqual(cfg.get("node", "tub.port"), "unix:/var/tahoe/socket")
279
280    def test_node_hostname_port_location(self):
281        basedir = self.mktemp()
282        e = self.assertRaises(usage.UsageError,
283                              parse_cli,
284                              "create-node", "--listen=tcp",
285                              "--hostname=foo", "--port=bar", "--location=baz",
286                              basedir)
287        self.assertEqual(str(e),
288                         "--hostname cannot be used with --location/--port")
289
290    def test_node_listen_tcp_no_hostname(self):
291        basedir = self.mktemp()
292        e = self.assertRaises(usage.UsageError,
293                              parse_cli,
294                              "create-node", "--listen=tcp", basedir)
295        self.assertIn("--listen=tcp requires --hostname=", str(e))
296
297    @defer.inlineCallbacks
298    def test_node_listen_none(self):
299        basedir = self.mktemp()
300        rc, out, err = yield run_cli("create-node", "--listen=none", basedir)
301        cfg = read_config(basedir)
302        self.assertEqual(cfg.get("node", "tub.port"), "disabled")
303        self.assertEqual(cfg.get("node", "tub.location"), "disabled")
304
305    def test_node_listen_none_errors(self):
306        basedir = self.mktemp()
307        e = self.assertRaises(usage.UsageError,
308                              parse_cli,
309                              "create-node", "--listen=none",
310                              "--hostname=foo",
311                              basedir)
312        self.assertEqual(str(e), "--hostname cannot be used when --listen=none")
313
314        e = self.assertRaises(usage.UsageError,
315                              parse_cli,
316                              "create-node", "--listen=none",
317                              "--port=foo", "--location=foo",
318                              basedir)
319        self.assertEqual(str(e), "--port/--location cannot be used when --listen=none")
320
321        e = self.assertRaises(usage.UsageError,
322                              parse_cli,
323                              "create-node", "--listen=tcp,none",
324                              basedir)
325        self.assertEqual(str(e), "--listen=tcp requires --hostname=")
326
327    def test_node_listen_bad(self):
328        basedir = self.mktemp()
329        e = self.assertRaises(usage.UsageError,
330                              parse_cli,
331                              "create-node", "--listen=XYZZY,tcp",
332                              basedir)
333        self.assertEqual(str(e), "--listen= must be one/some of: i2p, none, tcp, tor")
334
335    def test_node_listen_tor_hostname(self):
336        e = self.assertRaises(usage.UsageError,
337                              parse_cli,
338                              "create-node", "--listen=tor",
339                              "--hostname=foo")
340        self.assertEqual(str(e), "--listen= must be tcp to use --hostname")
341
342    def test_node_port_only(self):
343        e = self.assertRaises(usage.UsageError,
344                              parse_cli,
345                              "create-node", "--port=unix:/var/tahoe/socket")
346        self.assertEqual(str(e), "--port must be used with --location")
347
348    def test_node_location_only(self):
349        e = self.assertRaises(usage.UsageError,
350                              parse_cli,
351                              "create-node", "--location=tor:myservice.onion:12345")
352        self.assertEqual(str(e), "--location must be used with --port")
353
354    @defer.inlineCallbacks
355    def test_node_basedir_exists(self):
356        basedir = self.mktemp()
357        os.mkdir(basedir)
358        with open(os.path.join(basedir, "foo"), "w") as f:
359            f.write("blocker")
360        rc, out, err = yield run_cli("create-node", "--hostname=foo", basedir)
361        self.assertEqual(rc, -1)
362        self.assertIn(basedir, err)
363        self.assertIn("is not empty", err)
364        self.assertIn("To avoid clobbering anything, I am going to quit now", err)
365
366    @defer.inlineCallbacks
367    def test_node_slow(self):
368        """
369        A node can be created using a listener type that returns an
370        unfired Deferred from its ``create_config`` method.
371        """
372        d = defer.Deferred()
373        slow = StaticProvider(True, False, d, None)
374        create_node._LISTENERS["xxyzy"] = slow
375        self.addCleanup(lambda: create_node._LISTENERS.pop("xxyzy"))
376
377        basedir = self.mktemp()
378        d2 = run_cli("create-node", "--listen=xxyzy", basedir)
379        d.callback(None)
380        rc, out, err = yield d2
381        self.assertEqual(rc, 0)
382        self.assertIn("Node created", out)
383        self.assertEqual(err, "")
384
385    def test_introducer_no_hostname(self):
386        basedir = self.mktemp()
387        e = self.assertRaises(usage.UsageError, parse_cli,
388                              "create-introducer", basedir)
389        self.assertEqual(str(e), "--listen=tcp requires --hostname=")
390
391    @defer.inlineCallbacks
392    def test_introducer_hide_ip(self):
393        basedir = self.mktemp()
394        rc, out, err = yield run_cli("create-introducer", "--hide-ip",
395                                     "--hostname=foo", basedir)
396        cfg = read_config(basedir)
397        self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), False)
398
399    @defer.inlineCallbacks
400    def test_introducer_hostname(self):
401        basedir = self.mktemp()
402        rc, out, err = yield run_cli("create-introducer",
403                                     "--hostname=foo", basedir)
404        cfg = read_config(basedir)
405        self.assertTrue("foo" in cfg.get("node", "tub.location"))
406        self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), True)
407
408    @defer.inlineCallbacks
409    def test_introducer_basedir_exists(self):
410        basedir = self.mktemp()
411        os.mkdir(basedir)
412        with open(os.path.join(basedir, "foo"), "w") as f:
413            f.write("blocker")
414        rc, out, err = yield run_cli("create-introducer", "--hostname=foo",
415                                     basedir)
416        self.assertEqual(rc, -1)
417        self.assertIn(basedir, err)
418        self.assertIn("is not empty", err)
419        self.assertIn("To avoid clobbering anything, I am going to quit now", err)
420
421def fake_config(testcase: unittest.TestCase, module: Any, result: Any) -> list[tuple]:
422    """
423    Monkey-patch a fake configuration function into the given module.
424
425    :param testcase: The test case to use to do the monkey-patching.
426
427    :param module: The module into which to patch the fake function.
428
429    :param result: The return value for the fake function.
430
431    :return: A list of tuples of the arguments the fake function was called
432        with.
433    """
434    calls = []
435    def fake_config(reactor, cli_config):
436        calls.append((reactor, cli_config))
437        return result
438    testcase.patch(module, "create_config", fake_config)
439    return calls
440
441class Tor(unittest.TestCase):
442    def test_default(self):
443        basedir = self.mktemp()
444        tor_config = {"tor": [("abc", "def")]}
445        tor_port = "ghi"
446        tor_location = "jkl"
447        config_d = defer.succeed(
448            ListenerConfig([tor_port], [tor_location], tor_config)
449        )
450
451        calls = fake_config(self, tor_provider, config_d)
452        rc, out, err = self.successResultOf(
453            run_cli("create-node", "--listen=tor", basedir),
454        )
455
456        self.assertEqual(len(calls), 1)
457        args = calls[0]
458        self.assertIdentical(args[0], reactor)
459        self.assertIsInstance(args[1], create_node.CreateNodeOptions)
460        self.assertEqual(args[1]["listen"], "tor")
461        cfg = read_config(basedir)
462        self.assertEqual(cfg.get("tor", "abc"), "def")
463        self.assertEqual(cfg.get("node", "tub.port"), "ghi")
464        self.assertEqual(cfg.get("node", "tub.location"), "jkl")
465
466    def test_launch(self):
467        """
468        The ``--tor-launch`` command line option sets ``tor-launch`` to
469        ``True``.
470        """
471        basedir = self.mktemp()
472        config_d = defer.succeed(None)
473
474        calls = fake_config(self, tor_provider, config_d)
475        rc, out, err = self.successResultOf(
476            run_cli(
477                "create-node", "--listen=tor", "--tor-launch",
478                basedir,
479            ),
480        )
481        args = calls[0]
482        self.assertEqual(args[1]["listen"], "tor")
483        self.assertEqual(args[1]["tor-launch"], True)
484        self.assertEqual(args[1]["tor-control-port"], None)
485
486    def test_control_port(self):
487        """
488        The ``--tor-control-port`` command line parameter's value is
489        passed along as the ``tor-control-port`` value.
490        """
491        basedir = self.mktemp()
492        config_d = defer.succeed(None)
493
494        calls = fake_config(self, tor_provider, config_d)
495        rc, out, err = self.successResultOf(
496            run_cli(
497                "create-node", "--listen=tor", "--tor-control-port=mno",
498                basedir,
499            ),
500        )
501        args = calls[0]
502        self.assertEqual(args[1]["listen"], "tor")
503        self.assertEqual(args[1]["tor-launch"], False)
504        self.assertEqual(args[1]["tor-control-port"], "mno")
505
506    def test_not_both(self):
507        e = self.assertRaises(usage.UsageError,
508                              parse_cli,
509                              "create-node", "--listen=tor",
510                              "--tor-launch", "--tor-control-port=foo")
511        self.assertEqual(str(e), "use either --tor-launch or"
512                         " --tor-control-port=, not both")
513
514    def test_launch_without_listen(self):
515        e = self.assertRaises(usage.UsageError,
516                              parse_cli,
517                              "create-node", "--listen=none", "--tor-launch")
518        self.assertEqual(str(e), "--tor-launch requires --listen=tor")
519
520    def test_control_port_without_listen(self):
521        e = self.assertRaises(usage.UsageError,
522                              parse_cli,
523                              "create-node", "--listen=none",
524                              "--tor-control-port=foo")
525        self.assertEqual(str(e), "--tor-control-port= requires --listen=tor")
526
527class I2P(unittest.TestCase):
528    def test_default(self):
529        basedir = self.mktemp()
530        i2p_config = {"i2p": [("abc", "def")]}
531        i2p_port = "ghi"
532        i2p_location = "jkl"
533        dest_d = defer.succeed(ListenerConfig([i2p_port], [i2p_location], i2p_config))
534
535        calls = fake_config(self, i2p_provider, dest_d)
536        rc, out, err = self.successResultOf(
537            run_cli("create-node", "--listen=i2p", basedir),
538        )
539        self.assertEqual(len(calls), 1)
540        args = calls[0]
541        self.assertIdentical(args[0], reactor)
542        self.assertIsInstance(args[1], create_node.CreateNodeOptions)
543        self.assertEqual(args[1]["listen"], "i2p")
544        cfg = read_config(basedir)
545        self.assertEqual(cfg.get("i2p", "abc"), "def")
546        self.assertEqual(cfg.get("node", "tub.port"), "ghi")
547        self.assertEqual(cfg.get("node", "tub.location"), "jkl")
548
549    def test_launch(self):
550        e = self.assertRaises(usage.UsageError,
551                              parse_cli,
552                              "create-node", "--listen=i2p", "--i2p-launch")
553        self.assertEqual(str(e), "--i2p-launch is under development")
554
555
556    def test_sam_port(self):
557        basedir = self.mktemp()
558        dest_d = defer.succeed(None)
559
560        calls = fake_config(self, i2p_provider, dest_d)
561        rc, out, err = self.successResultOf(
562            run_cli(
563                "create-node", "--listen=i2p", "--i2p-sam-port=mno",
564                basedir,
565            ),
566        )
567        args = calls[0]
568        self.assertEqual(args[1]["listen"], "i2p")
569        self.assertEqual(args[1]["i2p-launch"], False)
570        self.assertEqual(args[1]["i2p-sam-port"], "mno")
571
572    def test_not_both(self):
573        e = self.assertRaises(usage.UsageError,
574                              parse_cli,
575                              "create-node", "--listen=i2p",
576                              "--i2p-launch", "--i2p-sam-port=foo")
577        self.assertEqual(str(e), "use either --i2p-launch or"
578                         " --i2p-sam-port=, not both")
579
580    def test_launch_without_listen(self):
581        e = self.assertRaises(usage.UsageError,
582                              parse_cli,
583                              "create-node", "--listen=none", "--i2p-launch")
584        self.assertEqual(str(e), "--i2p-launch requires --listen=i2p")
585
586    def test_sam_port_without_listen(self):
587        e = self.assertRaises(usage.UsageError,
588                              parse_cli,
589                              "create-node", "--listen=none",
590                              "--i2p-sam-port=foo")
591        self.assertEqual(str(e), "--i2p-sam-port= requires --listen=i2p")
Note: See TracBrowser for help on using the repository browser.