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

Last change on this file was 2d688df, checked in by Jean-Paul Calderone <exarkun@…>, at 2023-07-21T12:19:27Z

get the node config types right

  • Property mode set to 100644
File size: 22.6 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_node_hide_ip(self):
245        basedir = self.mktemp()
246        rc, out, err = yield run_cli("create-node", "--hide-ip",
247                                     "--hostname=foo", basedir)
248        cfg = read_config(basedir)
249        self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), False)
250        self.assertEqual(cfg.get("connections", "tcp"), "tor")
251
252    @defer.inlineCallbacks
253    def test_node_hostname(self):
254        basedir = self.mktemp()
255        rc, out, err = yield run_cli("create-node", "--hostname=computer", basedir)
256        cfg = read_config(basedir)
257        port = cfg.get("node", "tub.port")
258        location = cfg.get("node", "tub.location")
259        self.assertRegex(port, r'^tcp:\d+$')
260        self.assertRegex(location, r'^tcp:computer:\d+$')
261
262    @defer.inlineCallbacks
263    def test_node_port_location(self):
264        basedir = self.mktemp()
265        rc, out, err = yield run_cli("create-node",
266                                     "--port=unix:/var/tahoe/socket",
267                                     "--location=tor:myservice.onion:12345",
268                                     basedir)
269        cfg = read_config(basedir)
270        self.assertEqual(cfg.get("node", "tub.location"), "tor:myservice.onion:12345")
271        self.assertEqual(cfg.get("node", "tub.port"), "unix:/var/tahoe/socket")
272
273    def test_node_hostname_port_location(self):
274        basedir = self.mktemp()
275        e = self.assertRaises(usage.UsageError,
276                              parse_cli,
277                              "create-node", "--listen=tcp",
278                              "--hostname=foo", "--port=bar", "--location=baz",
279                              basedir)
280        self.assertEqual(str(e),
281                         "--hostname cannot be used with --location/--port")
282
283    def test_node_listen_tcp_no_hostname(self):
284        basedir = self.mktemp()
285        e = self.assertRaises(usage.UsageError,
286                              parse_cli,
287                              "create-node", "--listen=tcp", basedir)
288        self.assertIn("--listen=tcp requires --hostname=", str(e))
289
290    @defer.inlineCallbacks
291    def test_node_listen_none(self):
292        basedir = self.mktemp()
293        rc, out, err = yield run_cli("create-node", "--listen=none", basedir)
294        cfg = read_config(basedir)
295        self.assertEqual(cfg.get("node", "tub.port"), "disabled")
296        self.assertEqual(cfg.get("node", "tub.location"), "disabled")
297
298    def test_node_listen_none_errors(self):
299        basedir = self.mktemp()
300        e = self.assertRaises(usage.UsageError,
301                              parse_cli,
302                              "create-node", "--listen=none",
303                              "--hostname=foo",
304                              basedir)
305        self.assertEqual(str(e), "--hostname cannot be used when --listen=none")
306
307        e = self.assertRaises(usage.UsageError,
308                              parse_cli,
309                              "create-node", "--listen=none",
310                              "--port=foo", "--location=foo",
311                              basedir)
312        self.assertEqual(str(e), "--port/--location cannot be used when --listen=none")
313
314        e = self.assertRaises(usage.UsageError,
315                              parse_cli,
316                              "create-node", "--listen=tcp,none",
317                              basedir)
318        self.assertEqual(str(e), "--listen=tcp requires --hostname=")
319
320    def test_node_listen_bad(self):
321        basedir = self.mktemp()
322        e = self.assertRaises(usage.UsageError,
323                              parse_cli,
324                              "create-node", "--listen=XYZZY,tcp",
325                              basedir)
326        self.assertEqual(str(e), "--listen= must be one/some of: i2p, none, tcp, tor")
327
328    def test_node_listen_tor_hostname(self):
329        e = self.assertRaises(usage.UsageError,
330                              parse_cli,
331                              "create-node", "--listen=tor",
332                              "--hostname=foo")
333        self.assertEqual(str(e), "--listen= must be tcp to use --hostname")
334
335    def test_node_port_only(self):
336        e = self.assertRaises(usage.UsageError,
337                              parse_cli,
338                              "create-node", "--port=unix:/var/tahoe/socket")
339        self.assertEqual(str(e), "--port must be used with --location")
340
341    def test_node_location_only(self):
342        e = self.assertRaises(usage.UsageError,
343                              parse_cli,
344                              "create-node", "--location=tor:myservice.onion:12345")
345        self.assertEqual(str(e), "--location must be used with --port")
346
347    @defer.inlineCallbacks
348    def test_node_basedir_exists(self):
349        basedir = self.mktemp()
350        os.mkdir(basedir)
351        with open(os.path.join(basedir, "foo"), "w") as f:
352            f.write("blocker")
353        rc, out, err = yield run_cli("create-node", "--hostname=foo", basedir)
354        self.assertEqual(rc, -1)
355        self.assertIn(basedir, err)
356        self.assertIn("is not empty", err)
357        self.assertIn("To avoid clobbering anything, I am going to quit now", err)
358
359    @defer.inlineCallbacks
360    def test_node_slow(self):
361        """
362        A node can be created using a listener type that returns an
363        unfired Deferred from its ``create_config`` method.
364        """
365        d = defer.Deferred()
366        slow = StaticProvider(True, False, d, None)
367        create_node._LISTENERS["xxyzy"] = slow
368        self.addCleanup(lambda: create_node._LISTENERS.pop("xxyzy"))
369
370        basedir = self.mktemp()
371        d2 = run_cli("create-node", "--listen=xxyzy", basedir)
372        d.callback(None)
373        rc, out, err = yield d2
374        self.assertEqual(rc, 0)
375        self.assertIn("Node created", out)
376        self.assertEqual(err, "")
377
378    def test_introducer_no_hostname(self):
379        basedir = self.mktemp()
380        e = self.assertRaises(usage.UsageError, parse_cli,
381                              "create-introducer", basedir)
382        self.assertEqual(str(e), "--listen=tcp requires --hostname=")
383
384    @defer.inlineCallbacks
385    def test_introducer_hide_ip(self):
386        basedir = self.mktemp()
387        rc, out, err = yield run_cli("create-introducer", "--hide-ip",
388                                     "--hostname=foo", basedir)
389        cfg = read_config(basedir)
390        self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), False)
391
392    @defer.inlineCallbacks
393    def test_introducer_hostname(self):
394        basedir = self.mktemp()
395        rc, out, err = yield run_cli("create-introducer",
396                                     "--hostname=foo", basedir)
397        cfg = read_config(basedir)
398        self.assertTrue("foo" in cfg.get("node", "tub.location"))
399        self.assertEqual(cfg.getboolean("node", "reveal-IP-address"), True)
400
401    @defer.inlineCallbacks
402    def test_introducer_basedir_exists(self):
403        basedir = self.mktemp()
404        os.mkdir(basedir)
405        with open(os.path.join(basedir, "foo"), "w") as f:
406            f.write("blocker")
407        rc, out, err = yield run_cli("create-introducer", "--hostname=foo",
408                                     basedir)
409        self.assertEqual(rc, -1)
410        self.assertIn(basedir, err)
411        self.assertIn("is not empty", err)
412        self.assertIn("To avoid clobbering anything, I am going to quit now", err)
413
414def fake_config(testcase: unittest.TestCase, module: Any, result: Any) -> list[tuple]:
415    """
416    Monkey-patch a fake configuration function into the given module.
417
418    :param testcase: The test case to use to do the monkey-patching.
419
420    :param module: The module into which to patch the fake function.
421
422    :param result: The return value for the fake function.
423
424    :return: A list of tuples of the arguments the fake function was called
425        with.
426    """
427    calls = []
428    def fake_config(reactor, cli_config):
429        calls.append((reactor, cli_config))
430        return result
431    testcase.patch(module, "create_config", fake_config)
432    return calls
433
434class Tor(unittest.TestCase):
435    def test_default(self):
436        basedir = self.mktemp()
437        tor_config = {"tor": [("abc", "def")]}
438        tor_port = "ghi"
439        tor_location = "jkl"
440        config_d = defer.succeed(
441            ListenerConfig([tor_port], [tor_location], tor_config)
442        )
443
444        calls = fake_config(self, tor_provider, config_d)
445        rc, out, err = self.successResultOf(
446            run_cli("create-node", "--listen=tor", basedir),
447        )
448
449        self.assertEqual(len(calls), 1)
450        args = calls[0]
451        self.assertIdentical(args[0], reactor)
452        self.assertIsInstance(args[1], create_node.CreateNodeOptions)
453        self.assertEqual(args[1]["listen"], "tor")
454        cfg = read_config(basedir)
455        self.assertEqual(cfg.get("tor", "abc"), "def")
456        self.assertEqual(cfg.get("node", "tub.port"), "ghi")
457        self.assertEqual(cfg.get("node", "tub.location"), "jkl")
458
459    def test_launch(self):
460        """
461        The ``--tor-launch`` command line option sets ``tor-launch`` to
462        ``True``.
463        """
464        basedir = self.mktemp()
465        config_d = defer.succeed(None)
466
467        calls = fake_config(self, tor_provider, config_d)
468        rc, out, err = self.successResultOf(
469            run_cli(
470                "create-node", "--listen=tor", "--tor-launch",
471                basedir,
472            ),
473        )
474        args = calls[0]
475        self.assertEqual(args[1]["listen"], "tor")
476        self.assertEqual(args[1]["tor-launch"], True)
477        self.assertEqual(args[1]["tor-control-port"], None)
478
479    def test_control_port(self):
480        """
481        The ``--tor-control-port`` command line parameter's value is
482        passed along as the ``tor-control-port`` value.
483        """
484        basedir = self.mktemp()
485        config_d = defer.succeed(None)
486
487        calls = fake_config(self, tor_provider, config_d)
488        rc, out, err = self.successResultOf(
489            run_cli(
490                "create-node", "--listen=tor", "--tor-control-port=mno",
491                basedir,
492            ),
493        )
494        args = calls[0]
495        self.assertEqual(args[1]["listen"], "tor")
496        self.assertEqual(args[1]["tor-launch"], False)
497        self.assertEqual(args[1]["tor-control-port"], "mno")
498
499    def test_not_both(self):
500        e = self.assertRaises(usage.UsageError,
501                              parse_cli,
502                              "create-node", "--listen=tor",
503                              "--tor-launch", "--tor-control-port=foo")
504        self.assertEqual(str(e), "use either --tor-launch or"
505                         " --tor-control-port=, not both")
506
507    def test_launch_without_listen(self):
508        e = self.assertRaises(usage.UsageError,
509                              parse_cli,
510                              "create-node", "--listen=none", "--tor-launch")
511        self.assertEqual(str(e), "--tor-launch requires --listen=tor")
512
513    def test_control_port_without_listen(self):
514        e = self.assertRaises(usage.UsageError,
515                              parse_cli,
516                              "create-node", "--listen=none",
517                              "--tor-control-port=foo")
518        self.assertEqual(str(e), "--tor-control-port= requires --listen=tor")
519
520class I2P(unittest.TestCase):
521    def test_default(self):
522        basedir = self.mktemp()
523        i2p_config = {"i2p": [("abc", "def")]}
524        i2p_port = "ghi"
525        i2p_location = "jkl"
526        dest_d = defer.succeed(ListenerConfig([i2p_port], [i2p_location], i2p_config))
527
528        calls = fake_config(self, i2p_provider, dest_d)
529        rc, out, err = self.successResultOf(
530            run_cli("create-node", "--listen=i2p", basedir),
531        )
532        self.assertEqual(len(calls), 1)
533        args = calls[0]
534        self.assertIdentical(args[0], reactor)
535        self.assertIsInstance(args[1], create_node.CreateNodeOptions)
536        self.assertEqual(args[1]["listen"], "i2p")
537        cfg = read_config(basedir)
538        self.assertEqual(cfg.get("i2p", "abc"), "def")
539        self.assertEqual(cfg.get("node", "tub.port"), "ghi")
540        self.assertEqual(cfg.get("node", "tub.location"), "jkl")
541
542    def test_launch(self):
543        e = self.assertRaises(usage.UsageError,
544                              parse_cli,
545                              "create-node", "--listen=i2p", "--i2p-launch")
546        self.assertEqual(str(e), "--i2p-launch is under development")
547
548
549    def test_sam_port(self):
550        basedir = self.mktemp()
551        dest_d = defer.succeed(None)
552
553        calls = fake_config(self, i2p_provider, dest_d)
554        rc, out, err = self.successResultOf(
555            run_cli(
556                "create-node", "--listen=i2p", "--i2p-sam-port=mno",
557                basedir,
558            ),
559        )
560        args = calls[0]
561        self.assertEqual(args[1]["listen"], "i2p")
562        self.assertEqual(args[1]["i2p-launch"], False)
563        self.assertEqual(args[1]["i2p-sam-port"], "mno")
564
565    def test_not_both(self):
566        e = self.assertRaises(usage.UsageError,
567                              parse_cli,
568                              "create-node", "--listen=i2p",
569                              "--i2p-launch", "--i2p-sam-port=foo")
570        self.assertEqual(str(e), "use either --i2p-launch or"
571                         " --i2p-sam-port=, not both")
572
573    def test_launch_without_listen(self):
574        e = self.assertRaises(usage.UsageError,
575                              parse_cli,
576                              "create-node", "--listen=none", "--i2p-launch")
577        self.assertEqual(str(e), "--i2p-launch requires --listen=i2p")
578
579    def test_sam_port_without_listen(self):
580        e = self.assertRaises(usage.UsageError,
581                              parse_cli,
582                              "create-node", "--listen=none",
583                              "--i2p-sam-port=foo")
584        self.assertEqual(str(e), "--i2p-sam-port= requires --listen=i2p")
Note: See TracBrowser for help on using the repository browser.