1 | |
---|
2 | from __future__ import annotations |
---|
3 | |
---|
4 | from typing import Optional |
---|
5 | |
---|
6 | import io |
---|
7 | import os |
---|
8 | |
---|
9 | from allmydata.scripts.types_ import ( |
---|
10 | SubCommands, |
---|
11 | Parameters, |
---|
12 | Flags, |
---|
13 | ) |
---|
14 | |
---|
15 | from twisted.internet import reactor, defer |
---|
16 | from twisted.python.usage import UsageError |
---|
17 | from twisted.python.filepath import ( |
---|
18 | FilePath, |
---|
19 | ) |
---|
20 | |
---|
21 | from allmydata.scripts.common import ( |
---|
22 | BasedirOptions, |
---|
23 | NoDefaultBasedirOptions, |
---|
24 | write_introducer, |
---|
25 | ) |
---|
26 | from allmydata.scripts.default_nodedir import _default_nodedir |
---|
27 | from allmydata.util import dictutil |
---|
28 | from allmydata.util.assertutil import precondition |
---|
29 | from allmydata.util.encodingutil import listdir_unicode, argv_to_unicode, quote_local_unicode_path, get_io_encoding |
---|
30 | |
---|
31 | i2p_provider: Listener |
---|
32 | tor_provider: Listener |
---|
33 | |
---|
34 | from allmydata.util import fileutil, i2p_provider, tor_provider, jsonbytes as json |
---|
35 | |
---|
36 | from ..listeners import ListenerConfig, Listener, TCPProvider, StaticProvider |
---|
37 | |
---|
38 | def _get_listeners() -> dict[str, Listener]: |
---|
39 | """ |
---|
40 | Get all of the kinds of listeners we might be able to use. |
---|
41 | """ |
---|
42 | return { |
---|
43 | "tor": tor_provider, |
---|
44 | "i2p": i2p_provider, |
---|
45 | "tcp": TCPProvider(), |
---|
46 | "none": StaticProvider( |
---|
47 | available=True, |
---|
48 | hide_ip=False, |
---|
49 | config=defer.succeed(None), |
---|
50 | # This is supposed to be an IAddressFamily but we have none for |
---|
51 | # this kind of provider. We could implement new client and server |
---|
52 | # endpoint types that always fail and pass an IAddressFamily here |
---|
53 | # that uses those. Nothing would ever even ask for them (at |
---|
54 | # least, yet), let alone try to use them, so that's a lot of extra |
---|
55 | # work for no practical result so I'm not doing it now. |
---|
56 | address=None, # type: ignore[arg-type] |
---|
57 | ), |
---|
58 | } |
---|
59 | |
---|
60 | _LISTENERS = _get_listeners() |
---|
61 | |
---|
62 | dummy_tac = """ |
---|
63 | import sys |
---|
64 | print("Nodes created by Tahoe-LAFS v1.11.0 or later cannot be run by") |
---|
65 | print("releases of Tahoe-LAFS before v1.10.0.") |
---|
66 | sys.exit(1) |
---|
67 | """ |
---|
68 | |
---|
69 | def write_tac(basedir, nodetype): |
---|
70 | fileutil.write(os.path.join(basedir, "tahoe-%s.tac" % (nodetype,)), dummy_tac) |
---|
71 | |
---|
72 | |
---|
73 | WHERE_OPTS : Parameters = [ |
---|
74 | ("location", None, None, |
---|
75 | "Server location to advertise (e.g. tcp:example.org:12345)"), |
---|
76 | ("port", None, None, |
---|
77 | "Server endpoint to listen on (e.g. tcp:12345, or tcp:12345:interface=127.0.0.1."), |
---|
78 | ("hostname", None, None, |
---|
79 | "Hostname to automatically set --location/--port when --listen=tcp"), |
---|
80 | ("listen", None, "tcp", |
---|
81 | "Comma-separated list of listener types (tcp,tor,i2p,none)."), |
---|
82 | ] |
---|
83 | |
---|
84 | TOR_OPTS : Parameters = [ |
---|
85 | ("tor-control-port", None, None, |
---|
86 | "Tor's control port endpoint descriptor string (e.g. tcp:127.0.0.1:9051 or unix:/var/run/tor/control)"), |
---|
87 | ("tor-executable", None, None, |
---|
88 | "The 'tor' executable to run (default is to search $PATH)."), |
---|
89 | ] |
---|
90 | |
---|
91 | TOR_FLAGS : Flags = [ |
---|
92 | ("tor-launch", None, "Launch a tor instead of connecting to a tor control port."), |
---|
93 | ] |
---|
94 | |
---|
95 | I2P_OPTS : Parameters = [ |
---|
96 | ("i2p-sam-port", None, None, |
---|
97 | "I2P's SAM API port endpoint descriptor string (e.g. tcp:127.0.0.1:7656)"), |
---|
98 | ("i2p-executable", None, None, |
---|
99 | "(future) The 'i2prouter' executable to run (default is to search $PATH)."), |
---|
100 | ] |
---|
101 | |
---|
102 | I2P_FLAGS : Flags = [ |
---|
103 | ("i2p-launch", None, "(future) Launch an I2P router instead of connecting to a SAM API port."), |
---|
104 | ] |
---|
105 | |
---|
106 | def validate_where_options(o): |
---|
107 | if o['listen'] == "none": |
---|
108 | # no other arguments are accepted |
---|
109 | if o['hostname']: |
---|
110 | raise UsageError("--hostname cannot be used when --listen=none") |
---|
111 | if o['port'] or o['location']: |
---|
112 | raise UsageError("--port/--location cannot be used when --listen=none") |
---|
113 | # --location and --port: overrides all others, rejects all others |
---|
114 | if o['location'] and not o['port']: |
---|
115 | raise UsageError("--location must be used with --port") |
---|
116 | if o['port'] and not o['location']: |
---|
117 | raise UsageError("--port must be used with --location") |
---|
118 | |
---|
119 | if o['location'] and o['port']: |
---|
120 | if o['hostname']: |
---|
121 | raise UsageError("--hostname cannot be used with --location/--port") |
---|
122 | # TODO: really, we should reject an explicit --listen= option (we |
---|
123 | # want them to omit it entirely, because --location/--port would |
---|
124 | # override anything --listen= might allocate). For now, just let it |
---|
125 | # pass, because that allows us to use --listen=tcp as the default in |
---|
126 | # optParameters, which (I think) gets included in the rendered --help |
---|
127 | # output, which is useful. In the future, let's reconsider the value |
---|
128 | # of that --help text (or achieve that documentation in some other |
---|
129 | # way), change the default to None, complain here if it's not None, |
---|
130 | # then change parseArgs() to transform the None into "tcp" |
---|
131 | else: |
---|
132 | # no --location and --port? expect --listen= (maybe the default), and |
---|
133 | # --listen=tcp requires --hostname. But --listen=none is special. |
---|
134 | if o['listen'] != "none" and o.get('join', None) is None: |
---|
135 | listeners = o['listen'].split(",") |
---|
136 | for l in listeners: |
---|
137 | if l not in _LISTENERS: |
---|
138 | raise UsageError( |
---|
139 | "--listen= must be one/some of: " |
---|
140 | f"{', '.join(sorted(_LISTENERS))}", |
---|
141 | ) |
---|
142 | if 'tcp' in listeners and not o['hostname']: |
---|
143 | raise UsageError("--listen=tcp requires --hostname=") |
---|
144 | if 'tcp' not in listeners and o['hostname']: |
---|
145 | raise UsageError("--listen= must be tcp to use --hostname") |
---|
146 | |
---|
147 | def validate_tor_options(o): |
---|
148 | use_tor = "tor" in o["listen"].split(",") |
---|
149 | if use_tor or any((o["tor-launch"], o["tor-control-port"])): |
---|
150 | if not _LISTENERS["tor"].is_available(): |
---|
151 | raise UsageError( |
---|
152 | "Specifying any Tor options requires the 'txtorcon' module" |
---|
153 | ) |
---|
154 | if not use_tor: |
---|
155 | if o["tor-launch"]: |
---|
156 | raise UsageError("--tor-launch requires --listen=tor") |
---|
157 | if o["tor-control-port"]: |
---|
158 | raise UsageError("--tor-control-port= requires --listen=tor") |
---|
159 | if o["tor-launch"] and o["tor-control-port"]: |
---|
160 | raise UsageError("use either --tor-launch or --tor-control-port=, not both") |
---|
161 | |
---|
162 | def validate_i2p_options(o): |
---|
163 | use_i2p = "i2p" in o["listen"].split(",") |
---|
164 | if use_i2p or any((o["i2p-launch"], o["i2p-sam-port"])): |
---|
165 | if not _LISTENERS["i2p"].is_available(): |
---|
166 | raise UsageError( |
---|
167 | "Specifying any I2P options requires the 'txi2p' module" |
---|
168 | ) |
---|
169 | if not use_i2p: |
---|
170 | if o["i2p-launch"]: |
---|
171 | raise UsageError("--i2p-launch requires --listen=i2p") |
---|
172 | if o["i2p-sam-port"]: |
---|
173 | raise UsageError("--i2p-sam-port= requires --listen=i2p") |
---|
174 | if o["i2p-launch"] and o["i2p-sam-port"]: |
---|
175 | raise UsageError("use either --i2p-launch or --i2p-sam-port=, not both") |
---|
176 | if o["i2p-launch"]: |
---|
177 | raise UsageError("--i2p-launch is under development") |
---|
178 | |
---|
179 | class _CreateBaseOptions(BasedirOptions): |
---|
180 | optFlags = [ |
---|
181 | ("hide-ip", None, "prohibit any configuration that would reveal the node's IP address"), |
---|
182 | ] |
---|
183 | |
---|
184 | def postOptions(self): |
---|
185 | super(_CreateBaseOptions, self).postOptions() |
---|
186 | if self['hide-ip']: |
---|
187 | ip_hiders = dictutil.filter(lambda v: v.can_hide_ip(), _LISTENERS) |
---|
188 | available = dictutil.filter(lambda v: v.is_available(), ip_hiders) |
---|
189 | if not available: |
---|
190 | raise UsageError( |
---|
191 | "--hide-ip was specified but no IP-hiding listener is installed.\n" |
---|
192 | "Try one of these:\n" + |
---|
193 | "".join([ |
---|
194 | f"\tpip install tahoe-lafs[{name}]\n" |
---|
195 | for name |
---|
196 | in ip_hiders |
---|
197 | ]) |
---|
198 | ) |
---|
199 | |
---|
200 | class CreateClientOptions(_CreateBaseOptions): |
---|
201 | synopsis = "[options] [NODEDIR]" |
---|
202 | description = "Create a client-only Tahoe-LAFS node (no storage server)." |
---|
203 | |
---|
204 | optParameters = [ |
---|
205 | # we provide 'create-node'-time options for the most common |
---|
206 | # configuration knobs. The rest can be controlled by editing |
---|
207 | # tahoe.cfg before node startup. |
---|
208 | |
---|
209 | ("nickname", "n", None, "Specify the nickname for this node."), |
---|
210 | ("introducer", "i", None, "Specify the introducer FURL to use."), |
---|
211 | ("webport", "p", "tcp:3456:interface=127.0.0.1", |
---|
212 | "Specify which TCP port to run the HTTP interface on. Use 'none' to disable."), |
---|
213 | ("basedir", "C", None, "Specify which Tahoe base directory should be used. This has the same effect as the global --node-directory option. [default: %s]" |
---|
214 | % quote_local_unicode_path(_default_nodedir)), |
---|
215 | ("shares-needed", None, 3, "Needed shares required for uploaded files."), |
---|
216 | ("shares-happy", None, 7, "How many servers new files must be placed on."), |
---|
217 | ("shares-total", None, 10, "Total shares required for uploaded files."), |
---|
218 | ("join", None, None, "Join a grid with the given Invite Code."), |
---|
219 | ] # type: Parameters |
---|
220 | |
---|
221 | # This is overridden in order to ensure we get a "Wrong number of |
---|
222 | # arguments." error when more than one argument is given. |
---|
223 | def parseArgs(self, basedir=None): |
---|
224 | BasedirOptions.parseArgs(self, basedir) |
---|
225 | for name in ["shares-needed", "shares-happy", "shares-total"]: |
---|
226 | try: |
---|
227 | int(self[name]) |
---|
228 | except ValueError: |
---|
229 | raise UsageError( |
---|
230 | "--{} must be an integer".format(name) |
---|
231 | ) |
---|
232 | |
---|
233 | |
---|
234 | class CreateNodeOptions(CreateClientOptions): |
---|
235 | optFlags = [ |
---|
236 | ("no-storage", None, "Do not offer storage service to other nodes."), |
---|
237 | ("helper", None, "Enable helper"), |
---|
238 | ] + TOR_FLAGS + I2P_FLAGS |
---|
239 | |
---|
240 | synopsis = "[options] [NODEDIR]" |
---|
241 | description = "Create a full Tahoe-LAFS node (client+server)." |
---|
242 | |
---|
243 | optParameters = [ |
---|
244 | ("storage-dir", None, None, "Path where the storage will be placed."), |
---|
245 | ] + CreateClientOptions.optParameters + WHERE_OPTS + TOR_OPTS + I2P_OPTS |
---|
246 | |
---|
247 | def parseArgs(self, basedir=None): |
---|
248 | CreateClientOptions.parseArgs(self, basedir) |
---|
249 | validate_where_options(self) |
---|
250 | validate_tor_options(self) |
---|
251 | validate_i2p_options(self) |
---|
252 | |
---|
253 | |
---|
254 | class CreateIntroducerOptions(NoDefaultBasedirOptions): |
---|
255 | subcommand_name = "create-introducer" |
---|
256 | description = "Create a Tahoe-LAFS introducer." |
---|
257 | optFlags = [ |
---|
258 | ("hide-ip", None, "prohibit any configuration that would reveal the node's IP address"), |
---|
259 | ] + TOR_FLAGS + I2P_FLAGS |
---|
260 | optParameters = NoDefaultBasedirOptions.optParameters + WHERE_OPTS + TOR_OPTS + I2P_OPTS |
---|
261 | def parseArgs(self, basedir=None): |
---|
262 | NoDefaultBasedirOptions.parseArgs(self, basedir) |
---|
263 | validate_where_options(self) |
---|
264 | validate_tor_options(self) |
---|
265 | validate_i2p_options(self) |
---|
266 | |
---|
267 | |
---|
268 | def merge_config( |
---|
269 | left: Optional[ListenerConfig], |
---|
270 | right: Optional[ListenerConfig], |
---|
271 | ) -> Optional[ListenerConfig]: |
---|
272 | """ |
---|
273 | Merge two listener configurations into one configuration representing |
---|
274 | both of them. |
---|
275 | |
---|
276 | If either is ``None`` then the result is ``None``. This supports the |
---|
277 | "disable listeners" functionality. |
---|
278 | |
---|
279 | :raise ValueError: If the keys in the node configs overlap. |
---|
280 | """ |
---|
281 | if left is None or right is None: |
---|
282 | return None |
---|
283 | |
---|
284 | overlap = set(left.node_config) & set(right.node_config) |
---|
285 | if overlap: |
---|
286 | raise ValueError(f"Node configs overlap: {overlap}") |
---|
287 | |
---|
288 | return ListenerConfig( |
---|
289 | list(left.tub_ports) + list(right.tub_ports), |
---|
290 | list(left.tub_locations) + list(right.tub_locations), |
---|
291 | dict(list(left.node_config.items()) + list(right.node_config.items())), |
---|
292 | ) |
---|
293 | |
---|
294 | |
---|
295 | async def write_node_config(c, config): |
---|
296 | # this is shared between clients and introducers |
---|
297 | c.write("# -*- mode: conf; coding: {c.encoding} -*-\n".format(c=c)) |
---|
298 | c.write("\n") |
---|
299 | c.write("# This file controls the configuration of the Tahoe node that\n") |
---|
300 | c.write("# lives in this directory. It is only read at node startup.\n") |
---|
301 | c.write("# For details about the keys that can be set here, please\n") |
---|
302 | c.write("# read the 'docs/configuration.rst' file that came with your\n") |
---|
303 | c.write("# Tahoe installation.\n") |
---|
304 | c.write("\n\n") |
---|
305 | |
---|
306 | if config["hide-ip"]: |
---|
307 | c.write("[connections]\n") |
---|
308 | if _LISTENERS["tor"].is_available(): |
---|
309 | c.write("tcp = tor\n") |
---|
310 | else: |
---|
311 | # XXX What about i2p? |
---|
312 | c.write("tcp = disabled\n") |
---|
313 | c.write("\n") |
---|
314 | |
---|
315 | c.write("[node]\n") |
---|
316 | nickname = argv_to_unicode(config.get("nickname") or "") |
---|
317 | c.write("nickname = %s\n" % (nickname,)) |
---|
318 | if config["hide-ip"]: |
---|
319 | c.write("reveal-IP-address = false\n") |
---|
320 | else: |
---|
321 | c.write("reveal-IP-address = true\n") |
---|
322 | |
---|
323 | # TODO: validate webport |
---|
324 | webport = argv_to_unicode(config.get("webport") or "none") |
---|
325 | if webport.lower() == "none": |
---|
326 | webport = "" |
---|
327 | c.write("web.port = %s\n" % (webport,)) |
---|
328 | c.write("web.static = public_html\n") |
---|
329 | |
---|
330 | listener_config = ListenerConfig([], [], {}) |
---|
331 | for listener_name in config['listen'].split(","): |
---|
332 | listener = _LISTENERS[listener_name] |
---|
333 | listener_config = merge_config( |
---|
334 | (await listener.create_config(reactor, config)), |
---|
335 | listener_config, |
---|
336 | ) |
---|
337 | |
---|
338 | if listener_config is None: |
---|
339 | tub_ports = ["disabled"] |
---|
340 | tub_locations = ["disabled"] |
---|
341 | else: |
---|
342 | tub_ports = listener_config.tub_ports |
---|
343 | tub_locations = listener_config.tub_locations |
---|
344 | |
---|
345 | c.write("tub.port = %s\n" % ",".join(tub_ports)) |
---|
346 | c.write("tub.location = %s\n" % ",".join(tub_locations)) |
---|
347 | c.write("\n") |
---|
348 | |
---|
349 | c.write("#log_gatherer.furl =\n") |
---|
350 | c.write("#timeout.keepalive =\n") |
---|
351 | c.write("#timeout.disconnect =\n") |
---|
352 | c.write("#ssh.port = 8022\n") |
---|
353 | c.write("#ssh.authorized_keys_file = ~/.ssh/authorized_keys\n") |
---|
354 | c.write("\n") |
---|
355 | |
---|
356 | if listener_config is not None: |
---|
357 | for section, items in listener_config.node_config.items(): |
---|
358 | c.write(f"[{section}]\n") |
---|
359 | for k, v in items: |
---|
360 | c.write(f"{k} = {v}\n") |
---|
361 | c.write("\n") |
---|
362 | |
---|
363 | |
---|
364 | def write_client_config(c, config): |
---|
365 | introducer = config.get("introducer", None) |
---|
366 | if introducer is not None: |
---|
367 | write_introducer( |
---|
368 | FilePath(config["basedir"]), |
---|
369 | "default", |
---|
370 | introducer, |
---|
371 | ) |
---|
372 | |
---|
373 | c.write("[client]\n") |
---|
374 | c.write("helper.furl =\n") |
---|
375 | c.write("\n") |
---|
376 | c.write("# Encoding parameters this client will use for newly-uploaded files\n") |
---|
377 | c.write("# This can be changed at any time: the encoding is saved in\n") |
---|
378 | c.write("# each filecap, and we can download old files with any encoding\n") |
---|
379 | c.write("# settings\n") |
---|
380 | c.write("shares.needed = {}\n".format(config['shares-needed'])) |
---|
381 | c.write("shares.happy = {}\n".format(config['shares-happy'])) |
---|
382 | c.write("shares.total = {}\n".format(config['shares-total'])) |
---|
383 | c.write("\n") |
---|
384 | |
---|
385 | boolstr = {True:"true", False:"false"} |
---|
386 | c.write("[storage]\n") |
---|
387 | c.write("# Shall this node provide storage service?\n") |
---|
388 | storage_enabled = not config.get("no-storage", None) |
---|
389 | c.write("enabled = %s\n" % boolstr[storage_enabled]) |
---|
390 | c.write("#readonly =\n") |
---|
391 | c.write("reserved_space = 1G\n") |
---|
392 | storage_dir = config.get("storage-dir") |
---|
393 | if storage_dir: |
---|
394 | c.write("storage_dir = %s\n" % (storage_dir,)) |
---|
395 | else: |
---|
396 | c.write("#storage_dir =\n") |
---|
397 | c.write("#expire.enabled =\n") |
---|
398 | c.write("#expire.mode =\n") |
---|
399 | c.write("\n") |
---|
400 | |
---|
401 | c.write("[helper]\n") |
---|
402 | c.write("# Shall this node run a helper service that clients can use?\n") |
---|
403 | if config.get("helper"): |
---|
404 | c.write("enabled = true\n") |
---|
405 | else: |
---|
406 | c.write("enabled = false\n") |
---|
407 | c.write("\n") |
---|
408 | |
---|
409 | |
---|
410 | @defer.inlineCallbacks |
---|
411 | def _get_config_via_wormhole(config): |
---|
412 | out = config.stdout |
---|
413 | print("Opening wormhole with code '{}'".format(config['join']), file=out) |
---|
414 | relay_url = config.parent['wormhole-server'] |
---|
415 | print("Connecting to '{}'".format(relay_url), file=out) |
---|
416 | |
---|
417 | wh = config.parent.wormhole.create( |
---|
418 | appid=config.parent['wormhole-invite-appid'], |
---|
419 | relay_url=relay_url, |
---|
420 | reactor=reactor, |
---|
421 | ) |
---|
422 | code = str(config['join']) |
---|
423 | wh.set_code(code) |
---|
424 | yield wh.get_welcome() |
---|
425 | print("Connected to wormhole server", file=out) |
---|
426 | |
---|
427 | intro = { |
---|
428 | u"abilities": { |
---|
429 | "client-v1": {}, |
---|
430 | } |
---|
431 | } |
---|
432 | wh.send_message(json.dumps_bytes(intro)) |
---|
433 | |
---|
434 | server_intro = yield wh.get_message() |
---|
435 | server_intro = json.loads(server_intro) |
---|
436 | |
---|
437 | print(" received server introduction", file=out) |
---|
438 | if u'abilities' not in server_intro: |
---|
439 | raise RuntimeError(" Expected 'abilities' in server introduction") |
---|
440 | if u'server-v1' not in server_intro['abilities']: |
---|
441 | raise RuntimeError(" Expected 'server-v1' in server abilities") |
---|
442 | |
---|
443 | remote_data = yield wh.get_message() |
---|
444 | print(" received configuration", file=out) |
---|
445 | defer.returnValue(json.loads(remote_data)) |
---|
446 | |
---|
447 | |
---|
448 | @defer.inlineCallbacks |
---|
449 | def create_node(config): |
---|
450 | out = config.stdout |
---|
451 | err = config.stderr |
---|
452 | basedir = config['basedir'] |
---|
453 | # This should always be called with an absolute Unicode basedir. |
---|
454 | precondition(isinstance(basedir, str), basedir) |
---|
455 | |
---|
456 | if os.path.exists(basedir): |
---|
457 | if listdir_unicode(basedir): |
---|
458 | print("The base directory %s is not empty." % quote_local_unicode_path(basedir), file=err) |
---|
459 | print("To avoid clobbering anything, I am going to quit now.", file=err) |
---|
460 | print("Please use a different directory, or empty this one.", file=err) |
---|
461 | defer.returnValue(-1) |
---|
462 | # we're willing to use an empty directory |
---|
463 | else: |
---|
464 | os.mkdir(basedir) |
---|
465 | write_tac(basedir, "client") |
---|
466 | |
---|
467 | # if we're doing magic-wormhole stuff, do it now |
---|
468 | if config['join'] is not None: |
---|
469 | try: |
---|
470 | remote_config = yield _get_config_via_wormhole(config) |
---|
471 | except RuntimeError as e: |
---|
472 | print(str(e), file=err) |
---|
473 | defer.returnValue(1) |
---|
474 | |
---|
475 | # configuration we'll allow the inviter to set |
---|
476 | whitelist = [ |
---|
477 | 'shares-happy', 'shares-needed', 'shares-total', |
---|
478 | 'introducer', 'nickname', |
---|
479 | ] |
---|
480 | sensitive_keys = ['introducer'] |
---|
481 | |
---|
482 | print("Encoding: {shares-needed} of {shares-total} shares, on at least {shares-happy} servers".format(**remote_config), file=out) |
---|
483 | print("Overriding the following config:", file=out) |
---|
484 | |
---|
485 | for k in whitelist: |
---|
486 | v = remote_config.get(k, None) |
---|
487 | if v is not None: |
---|
488 | # we're faking usually argv-supplied options :/ |
---|
489 | v_orig = v |
---|
490 | if isinstance(v, str): |
---|
491 | v = v.encode(get_io_encoding()) |
---|
492 | config[k] = v |
---|
493 | if k not in sensitive_keys: |
---|
494 | if k not in ['shares-happy', 'shares-total', 'shares-needed']: |
---|
495 | print(" {}: {}".format(k, v_orig), file=out) |
---|
496 | else: |
---|
497 | print(" {}: [sensitive data; see tahoe.cfg]".format(k), file=out) |
---|
498 | |
---|
499 | fileutil.make_dirs(os.path.join(basedir, "private"), 0o700) |
---|
500 | cfg_name = os.path.join(basedir, "tahoe.cfg") |
---|
501 | with io.open(cfg_name, "w", encoding='utf-8') as c: |
---|
502 | yield defer.Deferred.fromCoroutine(write_node_config(c, config)) |
---|
503 | write_client_config(c, config) |
---|
504 | |
---|
505 | print("Node created in %s" % quote_local_unicode_path(basedir), file=out) |
---|
506 | tahoe_cfg = quote_local_unicode_path(os.path.join(basedir, "tahoe.cfg")) |
---|
507 | introducers_yaml = quote_local_unicode_path( |
---|
508 | os.path.join(basedir, "private", "introducers.yaml"), |
---|
509 | ) |
---|
510 | if not config.get("introducer", ""): |
---|
511 | print(" Please add introducers to %s!" % (introducers_yaml,), file=out) |
---|
512 | print(" The node cannot connect to a grid without it.", file=out) |
---|
513 | if not config.get("nickname", ""): |
---|
514 | print(" Please set [node]nickname= in %s" % tahoe_cfg, file=out) |
---|
515 | defer.returnValue(0) |
---|
516 | |
---|
517 | def create_client(config): |
---|
518 | config['no-storage'] = True |
---|
519 | config['listen'] = "none" |
---|
520 | return create_node(config) |
---|
521 | |
---|
522 | |
---|
523 | @defer.inlineCallbacks |
---|
524 | def create_introducer(config): |
---|
525 | out = config.stdout |
---|
526 | err = config.stderr |
---|
527 | basedir = config['basedir'] |
---|
528 | # This should always be called with an absolute Unicode basedir. |
---|
529 | precondition(isinstance(basedir, str), basedir) |
---|
530 | |
---|
531 | if os.path.exists(basedir): |
---|
532 | if listdir_unicode(basedir): |
---|
533 | print("The base directory %s is not empty." % quote_local_unicode_path(basedir), file=err) |
---|
534 | print("To avoid clobbering anything, I am going to quit now.", file=err) |
---|
535 | print("Please use a different directory, or empty this one.", file=err) |
---|
536 | defer.returnValue(-1) |
---|
537 | # we're willing to use an empty directory |
---|
538 | else: |
---|
539 | os.mkdir(basedir) |
---|
540 | write_tac(basedir, "introducer") |
---|
541 | |
---|
542 | fileutil.make_dirs(os.path.join(basedir, "private"), 0o700) |
---|
543 | cfg_name = os.path.join(basedir, "tahoe.cfg") |
---|
544 | with io.open(cfg_name, "w", encoding='utf-8') as c: |
---|
545 | yield defer.Deferred.fromCoroutine(write_node_config(c, config)) |
---|
546 | |
---|
547 | print("Introducer created in %s" % quote_local_unicode_path(basedir), file=out) |
---|
548 | defer.returnValue(0) |
---|
549 | |
---|
550 | |
---|
551 | subCommands : SubCommands = [ |
---|
552 | ("create-node", None, CreateNodeOptions, "Create a node that acts as a client, server or both."), |
---|
553 | ("create-client", None, CreateClientOptions, "Create a client node (with storage initially disabled)."), |
---|
554 | ("create-introducer", None, CreateIntroducerOptions, "Create an introducer node."), |
---|
555 | ] |
---|
556 | |
---|
557 | dispatch = { |
---|
558 | "create-node": create_node, |
---|
559 | "create-client": create_client, |
---|
560 | "create-introducer": create_introducer, |
---|
561 | } |
---|