1 | """ |
---|
2 | This module contains classes and functions to implement and manage |
---|
3 | a node for Tahoe-LAFS. |
---|
4 | |
---|
5 | Ported to Python 3. |
---|
6 | """ |
---|
7 | from __future__ import annotations |
---|
8 | |
---|
9 | from six import ensure_str, ensure_text |
---|
10 | |
---|
11 | import json |
---|
12 | import datetime |
---|
13 | import os.path |
---|
14 | import re |
---|
15 | import types |
---|
16 | import errno |
---|
17 | from base64 import b32decode, b32encode |
---|
18 | from errno import ENOENT, EPERM |
---|
19 | from warnings import warn |
---|
20 | from typing import Union, Iterable |
---|
21 | |
---|
22 | import attr |
---|
23 | |
---|
24 | # On Python 2 this will be the backported package. |
---|
25 | import configparser |
---|
26 | |
---|
27 | from twisted.python.filepath import ( |
---|
28 | FilePath, |
---|
29 | ) |
---|
30 | from twisted.python import log as twlog |
---|
31 | from twisted.application import service |
---|
32 | from twisted.python.failure import Failure |
---|
33 | from foolscap.api import Tub |
---|
34 | |
---|
35 | import foolscap.logging.log |
---|
36 | |
---|
37 | from allmydata.util import log |
---|
38 | from allmydata.util import fileutil, iputil |
---|
39 | from allmydata.util.fileutil import abspath_expanduser_unicode |
---|
40 | from allmydata.util.encodingutil import get_filesystem_encoding, quote_output |
---|
41 | from allmydata.util import configutil |
---|
42 | from allmydata.util.yamlutil import ( |
---|
43 | safe_load, |
---|
44 | ) |
---|
45 | |
---|
46 | from . import ( |
---|
47 | __full_version__, |
---|
48 | ) |
---|
49 | from .protocol_switch import create_tub_with_https_support |
---|
50 | |
---|
51 | |
---|
52 | def _common_valid_config(): |
---|
53 | return configutil.ValidConfiguration({ |
---|
54 | "connections": ( |
---|
55 | "tcp", |
---|
56 | ), |
---|
57 | "node": ( |
---|
58 | "log_gatherer.furl", |
---|
59 | "nickname", |
---|
60 | "reveal-ip-address", |
---|
61 | "tempdir", |
---|
62 | "timeout.disconnect", |
---|
63 | "timeout.keepalive", |
---|
64 | "tub.location", |
---|
65 | "tub.port", |
---|
66 | "web.port", |
---|
67 | "web.static", |
---|
68 | ), |
---|
69 | "i2p": ( |
---|
70 | "enabled", |
---|
71 | "i2p.configdir", |
---|
72 | "i2p.executable", |
---|
73 | "launch", |
---|
74 | "sam.port", |
---|
75 | "dest", |
---|
76 | "dest.port", |
---|
77 | "dest.private_key_file", |
---|
78 | ), |
---|
79 | "tor": ( |
---|
80 | "control.port", |
---|
81 | "enabled", |
---|
82 | "launch", |
---|
83 | "socks.port", |
---|
84 | "tor.executable", |
---|
85 | "onion", |
---|
86 | "onion.local_port", |
---|
87 | "onion.external_port", |
---|
88 | "onion.private_key_file", |
---|
89 | ), |
---|
90 | }) |
---|
91 | |
---|
92 | # group 1 will be addr (dotted quad string), group 3 if any will be portnum (string) |
---|
93 | ADDR_RE = re.compile("^([1-9][0-9]*\.[1-9][0-9]*\.[1-9][0-9]*\.[1-9][0-9]*)(:([1-9][0-9]*))?$") |
---|
94 | |
---|
95 | # this is put into README in new node-directories (for client and introducers) |
---|
96 | PRIV_README = """ |
---|
97 | This directory contains files which contain private data for the Tahoe node, |
---|
98 | such as private keys. On Unix-like systems, the permissions on this directory |
---|
99 | are set to disallow users other than its owner from reading the contents of |
---|
100 | the files. See the 'configuration.rst' documentation file for details. |
---|
101 | """ |
---|
102 | |
---|
103 | |
---|
104 | def formatTimeTahoeStyle(self, when): |
---|
105 | """ |
---|
106 | Format the given (UTC) timestamp in the way Tahoe-LAFS expects it, |
---|
107 | for example: 2007-10-12 00:26:28.566Z |
---|
108 | |
---|
109 | :param when: UTC POSIX timestamp |
---|
110 | :type when: float |
---|
111 | :returns: datetime.datetime |
---|
112 | """ |
---|
113 | d = datetime.datetime.utcfromtimestamp(when) |
---|
114 | if d.microsecond: |
---|
115 | return d.isoformat(" ")[:-3]+"Z" |
---|
116 | return d.isoformat(" ") + ".000Z" |
---|
117 | |
---|
118 | PRIV_README = """ |
---|
119 | This directory contains files which contain private data for the Tahoe node, |
---|
120 | such as private keys. On Unix-like systems, the permissions on this directory |
---|
121 | are set to disallow users other than its owner from reading the contents of |
---|
122 | the files. See the 'configuration.rst' documentation file for details.""" |
---|
123 | |
---|
124 | class _None(object): |
---|
125 | """ |
---|
126 | This class is to be used as a marker in get_config() |
---|
127 | """ |
---|
128 | pass |
---|
129 | |
---|
130 | class MissingConfigEntry(Exception): |
---|
131 | """ A required config entry was not found. """ |
---|
132 | |
---|
133 | class OldConfigError(Exception): |
---|
134 | """ An obsolete config file was found. See |
---|
135 | docs/historical/configuration.rst. """ |
---|
136 | def __str__(self): |
---|
137 | return ("Found pre-Tahoe-LAFS-v1.3 configuration file(s):\n" |
---|
138 | "%s\n" |
---|
139 | "See docs/historical/configuration.rst." |
---|
140 | % "\n".join([quote_output(fname) for fname in self.args[0]])) |
---|
141 | |
---|
142 | class OldConfigOptionError(Exception): |
---|
143 | """Indicate that outdated configuration options are being used.""" |
---|
144 | pass |
---|
145 | |
---|
146 | class UnescapedHashError(Exception): |
---|
147 | """Indicate that a configuration entry contains an unescaped '#' character.""" |
---|
148 | def __str__(self): |
---|
149 | return ("The configuration entry %s contained an unescaped '#' character." |
---|
150 | % quote_output("[%s]%s = %s" % self.args)) |
---|
151 | |
---|
152 | class PrivacyError(Exception): |
---|
153 | """reveal-IP-address = false, but the node is configured in such a way |
---|
154 | that the IP address could be revealed""" |
---|
155 | |
---|
156 | |
---|
157 | def create_node_dir(basedir, readme_text): |
---|
158 | """ |
---|
159 | Create new new 'node directory' at 'basedir'. This includes a |
---|
160 | 'private' subdirectory. If basedir (and privdir) already exists, |
---|
161 | nothing is done. |
---|
162 | |
---|
163 | :param readme_text: text to put in <basedir>/private/README |
---|
164 | """ |
---|
165 | if not os.path.exists(basedir): |
---|
166 | fileutil.make_dirs(basedir) |
---|
167 | privdir = os.path.join(basedir, "private") |
---|
168 | if not os.path.exists(privdir): |
---|
169 | fileutil.make_dirs(privdir, 0o700) |
---|
170 | readme_text = ensure_text(readme_text) |
---|
171 | with open(os.path.join(privdir, 'README'), 'w') as f: |
---|
172 | f.write(readme_text) |
---|
173 | |
---|
174 | |
---|
175 | def read_config(basedir, portnumfile, generated_files: Iterable = (), _valid_config=None): |
---|
176 | """ |
---|
177 | Read and validate configuration. |
---|
178 | |
---|
179 | :param unicode basedir: directory where configuration data begins |
---|
180 | |
---|
181 | :param unicode portnumfile: filename fragment for "port number" files |
---|
182 | |
---|
183 | :param list generated_files: a list of automatically-generated |
---|
184 | configuration files. |
---|
185 | |
---|
186 | :param ValidConfiguration _valid_config: (internal use, optional) a |
---|
187 | structure defining valid configuration sections and keys |
---|
188 | |
---|
189 | :returns: :class:`allmydata.node._Config` instance |
---|
190 | """ |
---|
191 | basedir = abspath_expanduser_unicode(ensure_text(basedir)) |
---|
192 | if _valid_config is None: |
---|
193 | _valid_config = _common_valid_config() |
---|
194 | |
---|
195 | # complain if there's bad stuff in the config dir |
---|
196 | _error_about_old_config_files(basedir, generated_files) |
---|
197 | |
---|
198 | # canonicalize the portnum file |
---|
199 | portnumfile = os.path.join(basedir, portnumfile) |
---|
200 | |
---|
201 | config_path = FilePath(basedir).child("tahoe.cfg") |
---|
202 | try: |
---|
203 | config_bytes = config_path.getContent() |
---|
204 | except EnvironmentError as e: |
---|
205 | if e.errno != errno.ENOENT: |
---|
206 | raise |
---|
207 | # The file is missing, just create empty ConfigParser. |
---|
208 | config_str = u"" |
---|
209 | else: |
---|
210 | config_str = config_bytes.decode("utf-8-sig") |
---|
211 | |
---|
212 | return config_from_string( |
---|
213 | basedir, |
---|
214 | portnumfile, |
---|
215 | config_str, |
---|
216 | _valid_config, |
---|
217 | config_path, |
---|
218 | ) |
---|
219 | |
---|
220 | |
---|
221 | def config_from_string(basedir, portnumfile, config_str, _valid_config=None, fpath=None): |
---|
222 | """ |
---|
223 | load and validate configuration from in-memory string |
---|
224 | """ |
---|
225 | if _valid_config is None: |
---|
226 | _valid_config = _common_valid_config() |
---|
227 | |
---|
228 | if isinstance(config_str, bytes): |
---|
229 | config_str = config_str.decode("utf-8") |
---|
230 | |
---|
231 | # load configuration from in-memory string |
---|
232 | parser = configutil.get_config_from_string(config_str) |
---|
233 | |
---|
234 | configutil.validate_config( |
---|
235 | "<string>" if fpath is None else fpath.path, |
---|
236 | parser, |
---|
237 | _valid_config, |
---|
238 | ) |
---|
239 | |
---|
240 | return _Config( |
---|
241 | parser, |
---|
242 | portnumfile, |
---|
243 | basedir, |
---|
244 | fpath, |
---|
245 | _valid_config, |
---|
246 | ) |
---|
247 | |
---|
248 | |
---|
249 | def _error_about_old_config_files(basedir, generated_files): |
---|
250 | """ |
---|
251 | If any old configuration files are detected, raise |
---|
252 | OldConfigError. |
---|
253 | """ |
---|
254 | oldfnames = set() |
---|
255 | old_names = [ |
---|
256 | 'nickname', 'webport', 'keepalive_timeout', 'log_gatherer.furl', |
---|
257 | 'disconnect_timeout', 'advertised_ip_addresses', 'introducer.furl', |
---|
258 | 'helper.furl', 'key_generator.furl', 'stats_gatherer.furl', |
---|
259 | 'no_storage', 'readonly_storage', 'sizelimit', |
---|
260 | 'debug_discard_storage', 'run_helper' |
---|
261 | ] |
---|
262 | for fn in generated_files: |
---|
263 | old_names.remove(fn) |
---|
264 | for name in old_names: |
---|
265 | fullfname = os.path.join(basedir, name) |
---|
266 | if os.path.exists(fullfname): |
---|
267 | oldfnames.add(fullfname) |
---|
268 | if oldfnames: |
---|
269 | e = OldConfigError(oldfnames) |
---|
270 | twlog.msg(e) |
---|
271 | raise e |
---|
272 | |
---|
273 | |
---|
274 | def ensure_text_and_abspath_expanduser_unicode(basedir: Union[bytes, str]) -> str: |
---|
275 | return abspath_expanduser_unicode(ensure_text(basedir)) |
---|
276 | |
---|
277 | |
---|
278 | @attr.s |
---|
279 | class _Config(object): |
---|
280 | """ |
---|
281 | Manages configuration of a Tahoe 'node directory'. |
---|
282 | |
---|
283 | Note: all this code and functionality was formerly in the Node |
---|
284 | class; names and funtionality have been kept the same while moving |
---|
285 | the code. It probably makes sense for several of these APIs to |
---|
286 | have better names. |
---|
287 | |
---|
288 | :ivar ConfigParser config: The actual configuration values. |
---|
289 | |
---|
290 | :ivar str portnum_fname: filename to use for the port-number file (a |
---|
291 | relative path inside basedir). |
---|
292 | |
---|
293 | :ivar str _basedir: path to our "node directory", inside which all |
---|
294 | configuration is managed. |
---|
295 | |
---|
296 | :ivar (FilePath|NoneType) config_path: The path actually used to create |
---|
297 | the configparser (might be ``None`` if using in-memory data). |
---|
298 | |
---|
299 | :ivar ValidConfiguration valid_config_sections: The validator for the |
---|
300 | values in this configuration. |
---|
301 | """ |
---|
302 | config = attr.ib(validator=attr.validators.instance_of(configparser.ConfigParser)) |
---|
303 | portnum_fname = attr.ib() |
---|
304 | _basedir = attr.ib( |
---|
305 | converter=ensure_text_and_abspath_expanduser_unicode, |
---|
306 | ) # type: str |
---|
307 | config_path = attr.ib( |
---|
308 | validator=attr.validators.optional( |
---|
309 | attr.validators.instance_of(FilePath), |
---|
310 | ), |
---|
311 | ) |
---|
312 | valid_config_sections = attr.ib( |
---|
313 | default=configutil.ValidConfiguration.everything(), |
---|
314 | validator=attr.validators.instance_of(configutil.ValidConfiguration), |
---|
315 | ) |
---|
316 | |
---|
317 | @property |
---|
318 | def nickname(self): |
---|
319 | nickname = self.get_config("node", "nickname", u"<unspecified>") |
---|
320 | assert isinstance(nickname, str) |
---|
321 | return nickname |
---|
322 | |
---|
323 | @property |
---|
324 | def _config_fname(self): |
---|
325 | if self.config_path is None: |
---|
326 | return "<string>" |
---|
327 | return self.config_path.path |
---|
328 | |
---|
329 | def write_config_file(self, name, value, mode="w"): |
---|
330 | """ |
---|
331 | writes the given 'value' into a file called 'name' in the config |
---|
332 | directory |
---|
333 | """ |
---|
334 | fn = os.path.join(self._basedir, name) |
---|
335 | try: |
---|
336 | fileutil.write(fn, value, mode) |
---|
337 | except EnvironmentError: |
---|
338 | log.err( |
---|
339 | Failure(), |
---|
340 | "Unable to write config file '{}'".format(fn), |
---|
341 | ) |
---|
342 | |
---|
343 | def enumerate_section(self, section): |
---|
344 | """ |
---|
345 | returns a dict containing all items in a configuration section. an |
---|
346 | empty dict is returned if the section doesn't exist. |
---|
347 | """ |
---|
348 | answer = dict() |
---|
349 | try: |
---|
350 | for k in self.config.options(section): |
---|
351 | answer[k] = self.config.get(section, k) |
---|
352 | except configparser.NoSectionError: |
---|
353 | pass |
---|
354 | return answer |
---|
355 | |
---|
356 | def items(self, section, default=_None): |
---|
357 | try: |
---|
358 | return self.config.items(section) |
---|
359 | except configparser.NoSectionError: |
---|
360 | if default is _None: |
---|
361 | raise |
---|
362 | return default |
---|
363 | |
---|
364 | def get_config(self, section, option, default=_None, boolean=False): |
---|
365 | try: |
---|
366 | if boolean: |
---|
367 | return self.config.getboolean(section, option) |
---|
368 | |
---|
369 | item = self.config.get(section, option) |
---|
370 | if option.endswith(".furl") and '#' in item: |
---|
371 | raise UnescapedHashError(section, option, item) |
---|
372 | |
---|
373 | return item |
---|
374 | except (configparser.NoOptionError, configparser.NoSectionError): |
---|
375 | if default is _None: |
---|
376 | raise MissingConfigEntry( |
---|
377 | "{} is missing the [{}]{} entry".format( |
---|
378 | quote_output(self._config_fname), |
---|
379 | section, |
---|
380 | option, |
---|
381 | ) |
---|
382 | ) |
---|
383 | return default |
---|
384 | |
---|
385 | def set_config(self, section, option, value): |
---|
386 | """ |
---|
387 | Set a config option in a section and re-write the tahoe.cfg file |
---|
388 | |
---|
389 | :param str section: The name of the section in which to set the |
---|
390 | option. |
---|
391 | |
---|
392 | :param str option: The name of the option to set. |
---|
393 | |
---|
394 | :param str value: The value of the option. |
---|
395 | |
---|
396 | :raise UnescapedHashError: If the option holds a fURL and there is a |
---|
397 | ``#`` in the value. |
---|
398 | """ |
---|
399 | if option.endswith(".furl") and "#" in value: |
---|
400 | raise UnescapedHashError(section, option, value) |
---|
401 | |
---|
402 | copied_config = configutil.copy_config(self.config) |
---|
403 | configutil.set_config(copied_config, section, option, value) |
---|
404 | configutil.validate_config( |
---|
405 | self._config_fname, |
---|
406 | copied_config, |
---|
407 | self.valid_config_sections, |
---|
408 | ) |
---|
409 | if self.config_path is not None: |
---|
410 | configutil.write_config(self.config_path, copied_config) |
---|
411 | self.config = copied_config |
---|
412 | |
---|
413 | def get_config_from_file(self, name, required=False): |
---|
414 | """Get the (string) contents of a config file, or None if the file |
---|
415 | did not exist. If required=True, raise an exception rather than |
---|
416 | returning None. Any leading or trailing whitespace will be stripped |
---|
417 | from the data.""" |
---|
418 | fn = os.path.join(self._basedir, name) |
---|
419 | try: |
---|
420 | return fileutil.read(fn).strip() |
---|
421 | except EnvironmentError as e: |
---|
422 | if e.errno != errno.ENOENT: |
---|
423 | raise # we only care about "file doesn't exist" |
---|
424 | if not required: |
---|
425 | return None |
---|
426 | raise |
---|
427 | |
---|
428 | def get_or_create_private_config(self, name, default=_None): |
---|
429 | """Try to get the (string) contents of a private config file (which |
---|
430 | is a config file that resides within the subdirectory named |
---|
431 | 'private'), and return it. Any leading or trailing whitespace will be |
---|
432 | stripped from the data. |
---|
433 | |
---|
434 | If the file does not exist, and default is not given, report an error. |
---|
435 | If the file does not exist and a default is specified, try to create |
---|
436 | it using that default, and then return the value that was written. |
---|
437 | If 'default' is a string, use it as a default value. If not, treat it |
---|
438 | as a zero-argument callable that is expected to return a string. |
---|
439 | """ |
---|
440 | privname = os.path.join(self._basedir, "private", name) |
---|
441 | try: |
---|
442 | value = fileutil.read(privname, mode="r") |
---|
443 | except EnvironmentError as e: |
---|
444 | if e.errno != errno.ENOENT: |
---|
445 | raise # we only care about "file doesn't exist" |
---|
446 | if default is _None: |
---|
447 | raise MissingConfigEntry("The required configuration file %s is missing." |
---|
448 | % (quote_output(privname),)) |
---|
449 | if isinstance(default, bytes): |
---|
450 | default = str(default, "utf-8") |
---|
451 | if isinstance(default, str): |
---|
452 | value = default |
---|
453 | else: |
---|
454 | value = default() |
---|
455 | fileutil.write(privname, value) |
---|
456 | return value.strip() |
---|
457 | |
---|
458 | def write_private_config(self, name, value): |
---|
459 | """Write the (string) contents of a private config file (which is a |
---|
460 | config file that resides within the subdirectory named 'private'), and |
---|
461 | return it. |
---|
462 | """ |
---|
463 | if isinstance(value, str): |
---|
464 | value = value.encode("utf-8") |
---|
465 | privname = os.path.join(self._basedir, "private", name) |
---|
466 | with open(privname, "wb") as f: |
---|
467 | f.write(value) |
---|
468 | |
---|
469 | def get_private_config(self, name, default=_None): |
---|
470 | """Read the (native string) contents of a private config file (a |
---|
471 | config file that resides within the subdirectory named 'private'), |
---|
472 | and return it. Return a default, or raise an error if one was not |
---|
473 | given. |
---|
474 | """ |
---|
475 | privname = os.path.join(self._basedir, "private", name) |
---|
476 | try: |
---|
477 | return fileutil.read(privname, mode="r").strip() |
---|
478 | except EnvironmentError as e: |
---|
479 | if e.errno != errno.ENOENT: |
---|
480 | raise # we only care about "file doesn't exist" |
---|
481 | if default is _None: |
---|
482 | raise MissingConfigEntry("The required configuration file %s is missing." |
---|
483 | % (quote_output(privname),)) |
---|
484 | return default |
---|
485 | |
---|
486 | def get_private_path(self, *args): |
---|
487 | """ |
---|
488 | returns an absolute path inside the 'private' directory with any |
---|
489 | extra args join()-ed |
---|
490 | |
---|
491 | This exists for historical reasons. New code should ideally |
---|
492 | not call this because it makes it harder for e.g. a SQL-based |
---|
493 | _Config object to exist. Code that needs to call this method |
---|
494 | should probably be a _Config method itself. See |
---|
495 | e.g. get_grid_manager_certificates() |
---|
496 | """ |
---|
497 | return os.path.join(self._basedir, "private", *args) |
---|
498 | |
---|
499 | def get_config_path(self, *args): |
---|
500 | """ |
---|
501 | returns an absolute path inside the config directory with any |
---|
502 | extra args join()-ed |
---|
503 | |
---|
504 | This exists for historical reasons. New code should ideally |
---|
505 | not call this because it makes it harder for e.g. a SQL-based |
---|
506 | _Config object to exist. Code that needs to call this method |
---|
507 | should probably be a _Config method itself. See |
---|
508 | e.g. get_grid_manager_certificates() |
---|
509 | """ |
---|
510 | # note: we re-expand here (_basedir already went through this |
---|
511 | # expanduser function) in case the path we're being asked for |
---|
512 | # has embedded ".."'s in it |
---|
513 | return abspath_expanduser_unicode( |
---|
514 | os.path.join(self._basedir, *args) |
---|
515 | ) |
---|
516 | |
---|
517 | def get_grid_manager_certificates(self): |
---|
518 | """ |
---|
519 | Load all Grid Manager certificates in the config. |
---|
520 | |
---|
521 | :returns: A list of all certificates. An empty list is |
---|
522 | returned if there are none. |
---|
523 | """ |
---|
524 | grid_manager_certificates = [] |
---|
525 | |
---|
526 | cert_fnames = list(self.enumerate_section("grid_manager_certificates").values()) |
---|
527 | for fname in cert_fnames: |
---|
528 | fname = self.get_config_path(fname) |
---|
529 | if not os.path.exists(fname): |
---|
530 | raise ValueError( |
---|
531 | "Grid Manager certificate file '{}' doesn't exist".format( |
---|
532 | fname |
---|
533 | ) |
---|
534 | ) |
---|
535 | with open(fname, 'r') as f: |
---|
536 | cert = json.load(f) |
---|
537 | if set(cert.keys()) != {"certificate", "signature"}: |
---|
538 | raise ValueError( |
---|
539 | "Unknown key in Grid Manager certificate '{}'".format( |
---|
540 | fname |
---|
541 | ) |
---|
542 | ) |
---|
543 | grid_manager_certificates.append(cert) |
---|
544 | return grid_manager_certificates |
---|
545 | |
---|
546 | def get_introducer_configuration(self): |
---|
547 | """ |
---|
548 | Get configuration for introducers. |
---|
549 | |
---|
550 | :return {unicode: (unicode, FilePath)}: A mapping from introducer |
---|
551 | petname to a tuple of the introducer's fURL and local cache path. |
---|
552 | """ |
---|
553 | introducers_yaml_filename = self.get_private_path("introducers.yaml") |
---|
554 | introducers_filepath = FilePath(introducers_yaml_filename) |
---|
555 | |
---|
556 | def get_cache_filepath(petname): |
---|
557 | return FilePath( |
---|
558 | self.get_private_path("introducer_{}_cache.yaml".format(petname)), |
---|
559 | ) |
---|
560 | |
---|
561 | try: |
---|
562 | with introducers_filepath.open() as f: |
---|
563 | introducers_yaml = safe_load(f) |
---|
564 | if introducers_yaml is None: |
---|
565 | raise EnvironmentError( |
---|
566 | EPERM, |
---|
567 | "Can't read '{}'".format(introducers_yaml_filename), |
---|
568 | introducers_yaml_filename, |
---|
569 | ) |
---|
570 | introducers = { |
---|
571 | petname: config["furl"] |
---|
572 | for petname, config |
---|
573 | in introducers_yaml.get("introducers", {}).items() |
---|
574 | } |
---|
575 | non_strs = list( |
---|
576 | k |
---|
577 | for k |
---|
578 | in introducers.keys() |
---|
579 | if not isinstance(k, str) |
---|
580 | ) |
---|
581 | if non_strs: |
---|
582 | raise TypeError( |
---|
583 | "Introducer petnames {!r} should have been str".format( |
---|
584 | non_strs, |
---|
585 | ), |
---|
586 | ) |
---|
587 | non_strs = list( |
---|
588 | v |
---|
589 | for v |
---|
590 | in introducers.values() |
---|
591 | if not isinstance(v, str) |
---|
592 | ) |
---|
593 | if non_strs: |
---|
594 | raise TypeError( |
---|
595 | "Introducer fURLs {!r} should have been str".format( |
---|
596 | non_strs, |
---|
597 | ), |
---|
598 | ) |
---|
599 | log.msg( |
---|
600 | "found {} introducers in {!r}".format( |
---|
601 | len(introducers), |
---|
602 | introducers_yaml_filename, |
---|
603 | ) |
---|
604 | ) |
---|
605 | except EnvironmentError as e: |
---|
606 | if e.errno != ENOENT: |
---|
607 | raise |
---|
608 | introducers = {} |
---|
609 | |
---|
610 | # supported the deprecated [client]introducer.furl item in tahoe.cfg |
---|
611 | tahoe_cfg_introducer_furl = self.get_config("client", "introducer.furl", None) |
---|
612 | if tahoe_cfg_introducer_furl == "None": |
---|
613 | raise ValueError( |
---|
614 | "tahoe.cfg has invalid 'introducer.furl = None':" |
---|
615 | " to disable it omit the key entirely" |
---|
616 | ) |
---|
617 | if tahoe_cfg_introducer_furl: |
---|
618 | warn( |
---|
619 | "tahoe.cfg [client]introducer.furl is deprecated; " |
---|
620 | "use private/introducers.yaml instead.", |
---|
621 | category=DeprecationWarning, |
---|
622 | stacklevel=-1, |
---|
623 | ) |
---|
624 | if "default" in introducers: |
---|
625 | raise ValueError( |
---|
626 | "'default' introducer furl cannot be specified in tahoe.cfg and introducers.yaml;" |
---|
627 | " please fix impossible configuration." |
---|
628 | ) |
---|
629 | introducers['default'] = tahoe_cfg_introducer_furl |
---|
630 | |
---|
631 | return { |
---|
632 | petname: (furl, get_cache_filepath(petname)) |
---|
633 | for (petname, furl) |
---|
634 | in introducers.items() |
---|
635 | } |
---|
636 | |
---|
637 | |
---|
638 | def create_tub_options(config): |
---|
639 | """ |
---|
640 | :param config: a _Config instance |
---|
641 | |
---|
642 | :returns: dict containing all Foolscap Tub-related options, |
---|
643 | overriding defaults with appropriate config from `config` |
---|
644 | instance. |
---|
645 | """ |
---|
646 | # We can't unify the camelCase vs. dashed-name divide here, |
---|
647 | # because these are options for Foolscap |
---|
648 | tub_options = { |
---|
649 | "logLocalFailures": True, |
---|
650 | "logRemoteFailures": True, |
---|
651 | "expose-remote-exception-types": False, |
---|
652 | "accept-gifts": False, |
---|
653 | } |
---|
654 | |
---|
655 | # see #521 for a discussion of how to pick these timeout values. |
---|
656 | keepalive_timeout_s = config.get_config("node", "timeout.keepalive", "") |
---|
657 | if keepalive_timeout_s: |
---|
658 | tub_options["keepaliveTimeout"] = int(keepalive_timeout_s) |
---|
659 | disconnect_timeout_s = config.get_config("node", "timeout.disconnect", "") |
---|
660 | if disconnect_timeout_s: |
---|
661 | # N.B.: this is in seconds, so use "1800" to get 30min |
---|
662 | tub_options["disconnectTimeout"] = int(disconnect_timeout_s) |
---|
663 | return tub_options |
---|
664 | |
---|
665 | |
---|
666 | def _make_tcp_handler(): |
---|
667 | """ |
---|
668 | :returns: a Foolscap default TCP handler |
---|
669 | """ |
---|
670 | # this is always available |
---|
671 | from foolscap.connections.tcp import default |
---|
672 | return default() |
---|
673 | |
---|
674 | |
---|
675 | def create_default_connection_handlers(config, handlers): |
---|
676 | """ |
---|
677 | :return: A dictionary giving the default connection handlers. The keys |
---|
678 | are strings like "tcp" and the values are strings like "tor" or |
---|
679 | ``None``. |
---|
680 | """ |
---|
681 | reveal_ip = config.get_config("node", "reveal-IP-address", True, boolean=True) |
---|
682 | |
---|
683 | # Remember the default mappings from tahoe.cfg |
---|
684 | default_connection_handlers = { |
---|
685 | name: name |
---|
686 | for name |
---|
687 | in handlers |
---|
688 | } |
---|
689 | tcp_handler_name = config.get_config("connections", "tcp", "tcp").lower() |
---|
690 | if tcp_handler_name == "disabled": |
---|
691 | default_connection_handlers["tcp"] = None |
---|
692 | else: |
---|
693 | if tcp_handler_name not in handlers: |
---|
694 | raise ValueError( |
---|
695 | "'tahoe.cfg [connections] tcp=' uses " |
---|
696 | "unknown handler type '{}'".format( |
---|
697 | tcp_handler_name |
---|
698 | ) |
---|
699 | ) |
---|
700 | if not handlers[tcp_handler_name]: |
---|
701 | raise ValueError( |
---|
702 | "'tahoe.cfg [connections] tcp=' uses " |
---|
703 | "unavailable/unimportable handler type '{}'. " |
---|
704 | "Please pip install tahoe-lafs[{}] to fix.".format( |
---|
705 | tcp_handler_name, |
---|
706 | tcp_handler_name, |
---|
707 | ) |
---|
708 | ) |
---|
709 | default_connection_handlers["tcp"] = tcp_handler_name |
---|
710 | |
---|
711 | if not reveal_ip: |
---|
712 | if default_connection_handlers.get("tcp") == "tcp": |
---|
713 | raise PrivacyError( |
---|
714 | "Privacy requested with `reveal-IP-address = false` " |
---|
715 | "but `tcp = tcp` conflicts with this.", |
---|
716 | ) |
---|
717 | return default_connection_handlers |
---|
718 | |
---|
719 | |
---|
720 | def create_connection_handlers(config, i2p_provider, tor_provider): |
---|
721 | """ |
---|
722 | :returns: 2-tuple of default_connection_handlers, foolscap_connection_handlers |
---|
723 | """ |
---|
724 | # We store handlers for everything. None means we were unable to |
---|
725 | # create that handler, so hints which want it will be ignored. |
---|
726 | handlers = { |
---|
727 | "tcp": _make_tcp_handler(), |
---|
728 | "tor": tor_provider.get_client_endpoint(), |
---|
729 | "i2p": i2p_provider.get_client_endpoint(), |
---|
730 | } |
---|
731 | log.msg( |
---|
732 | format="built Foolscap connection handlers for: %(known_handlers)s", |
---|
733 | known_handlers=sorted([k for k,v in handlers.items() if v]), |
---|
734 | facility="tahoe.node", |
---|
735 | umid="PuLh8g", |
---|
736 | ) |
---|
737 | return create_default_connection_handlers( |
---|
738 | config, |
---|
739 | handlers, |
---|
740 | ), handlers |
---|
741 | |
---|
742 | |
---|
743 | def create_tub(tub_options, default_connection_handlers, foolscap_connection_handlers, |
---|
744 | handler_overrides=None, force_foolscap=False, **kwargs): |
---|
745 | """ |
---|
746 | Create a Tub with the right options and handlers. It will be |
---|
747 | ephemeral unless the caller provides certFile= in kwargs |
---|
748 | |
---|
749 | :param handler_overrides: anything in this will override anything |
---|
750 | in `default_connection_handlers` for just this call. |
---|
751 | |
---|
752 | :param dict tub_options: every key-value pair in here will be set in |
---|
753 | the new Tub via `Tub.setOption` |
---|
754 | |
---|
755 | :param bool force_foolscap: If True, only allow Foolscap, not just HTTPS |
---|
756 | storage protocol. |
---|
757 | """ |
---|
758 | if handler_overrides is None: |
---|
759 | handler_overrides = {} |
---|
760 | # We listen simultaneously for both Foolscap and HTTPS on the same port, |
---|
761 | # so we have to create a special Foolscap Tub for that to work: |
---|
762 | if force_foolscap: |
---|
763 | tub = Tub(**kwargs) |
---|
764 | else: |
---|
765 | tub = create_tub_with_https_support(**kwargs) |
---|
766 | |
---|
767 | for (name, value) in list(tub_options.items()): |
---|
768 | tub.setOption(name, value) |
---|
769 | handlers = default_connection_handlers.copy() |
---|
770 | handlers.update(handler_overrides) |
---|
771 | tub.removeAllConnectionHintHandlers() |
---|
772 | for hint_type, handler_name in list(handlers.items()): |
---|
773 | handler = foolscap_connection_handlers.get(handler_name) |
---|
774 | if handler: |
---|
775 | tub.addConnectionHintHandler(hint_type, handler) |
---|
776 | return tub |
---|
777 | |
---|
778 | |
---|
779 | def _convert_tub_port(s): |
---|
780 | """ |
---|
781 | :returns: a proper Twisted endpoint string like (`tcp:X`) is `s` |
---|
782 | is a bare number, or returns `s` as-is |
---|
783 | """ |
---|
784 | us = s |
---|
785 | if isinstance(s, bytes): |
---|
786 | us = s.decode("utf-8") |
---|
787 | if re.search(r'^\d+$', us): |
---|
788 | return "tcp:{}".format(int(us)) |
---|
789 | return us |
---|
790 | |
---|
791 | |
---|
792 | class PortAssignmentRequired(Exception): |
---|
793 | """ |
---|
794 | A Tub port number was configured to be 0 where this is not allowed. |
---|
795 | """ |
---|
796 | |
---|
797 | |
---|
798 | def _tub_portlocation(config, get_local_addresses_sync, allocate_tcp_port): |
---|
799 | """ |
---|
800 | Figure out the network location of the main tub for some configuration. |
---|
801 | |
---|
802 | :param get_local_addresses_sync: A function like |
---|
803 | ``iputil.get_local_addresses_sync``. |
---|
804 | |
---|
805 | :param allocate_tcp_port: A function like ``iputil.allocate_tcp_port``. |
---|
806 | |
---|
807 | :returns: None or tuple of (port, location) for the main tub based |
---|
808 | on the given configuration. May raise ValueError or PrivacyError |
---|
809 | if there are problems with the config |
---|
810 | """ |
---|
811 | cfg_tubport = config.get_config("node", "tub.port", None) |
---|
812 | cfg_location = config.get_config("node", "tub.location", None) |
---|
813 | reveal_ip = config.get_config("node", "reveal-IP-address", True, boolean=True) |
---|
814 | tubport_disabled = False |
---|
815 | |
---|
816 | if cfg_tubport is not None: |
---|
817 | cfg_tubport = cfg_tubport.strip() |
---|
818 | if cfg_tubport == "": |
---|
819 | raise ValueError("tub.port must not be empty") |
---|
820 | if cfg_tubport == "disabled": |
---|
821 | tubport_disabled = True |
---|
822 | |
---|
823 | location_disabled = False |
---|
824 | if cfg_location is not None: |
---|
825 | cfg_location = cfg_location.strip() |
---|
826 | if cfg_location == "": |
---|
827 | raise ValueError("tub.location must not be empty") |
---|
828 | if cfg_location == "disabled": |
---|
829 | location_disabled = True |
---|
830 | |
---|
831 | if tubport_disabled and location_disabled: |
---|
832 | return None |
---|
833 | if tubport_disabled and not location_disabled: |
---|
834 | raise ValueError("tub.port is disabled, but not tub.location") |
---|
835 | if location_disabled and not tubport_disabled: |
---|
836 | raise ValueError("tub.location is disabled, but not tub.port") |
---|
837 | |
---|
838 | if cfg_tubport is None: |
---|
839 | # For 'tub.port', tahoe.cfg overrides the individual file on |
---|
840 | # disk. So only read config.portnum_fname if tahoe.cfg doesn't |
---|
841 | # provide a value. |
---|
842 | if os.path.exists(config.portnum_fname): |
---|
843 | file_tubport = fileutil.read(config.portnum_fname).strip() |
---|
844 | tubport = _convert_tub_port(file_tubport) |
---|
845 | else: |
---|
846 | tubport = "tcp:%d" % (allocate_tcp_port(),) |
---|
847 | fileutil.write_atomically(config.portnum_fname, tubport + "\n", |
---|
848 | mode="") |
---|
849 | else: |
---|
850 | tubport = _convert_tub_port(cfg_tubport) |
---|
851 | |
---|
852 | for port in tubport.split(","): |
---|
853 | if port in ("0", "tcp:0", "tcp:port=0", "tcp:0:interface=127.0.0.1"): |
---|
854 | raise PortAssignmentRequired() |
---|
855 | |
---|
856 | if cfg_location is None: |
---|
857 | cfg_location = "AUTO" |
---|
858 | |
---|
859 | local_portnum = None # needed to hush lgtm.com static analyzer |
---|
860 | # Replace the location "AUTO", if present, with the detected local |
---|
861 | # addresses. Don't probe for local addresses unless necessary. |
---|
862 | split_location = cfg_location.split(",") |
---|
863 | if "AUTO" in split_location: |
---|
864 | if not reveal_ip: |
---|
865 | raise PrivacyError("tub.location uses AUTO") |
---|
866 | local_addresses = get_local_addresses_sync() |
---|
867 | # tubport must be like "tcp:12345" or "tcp:12345:morestuff" |
---|
868 | local_portnum = int(tubport.split(":")[1]) |
---|
869 | new_locations = [] |
---|
870 | for loc in split_location: |
---|
871 | if loc == "AUTO": |
---|
872 | new_locations.extend(["tcp:%s:%d" % (ip, local_portnum) |
---|
873 | for ip in local_addresses]) |
---|
874 | else: |
---|
875 | if not reveal_ip: |
---|
876 | # Legacy hints are "host:port". We use Foolscap's utility |
---|
877 | # function to convert all hints into the modern format |
---|
878 | # ("tcp:host:port") because that's what the receiving |
---|
879 | # client will probably do. We test the converted hint for |
---|
880 | # TCP-ness, but publish the original hint because that |
---|
881 | # was the user's intent. |
---|
882 | from foolscap.connections.tcp import convert_legacy_hint |
---|
883 | converted_hint = convert_legacy_hint(loc) |
---|
884 | hint_type = converted_hint.split(":")[0] |
---|
885 | if hint_type == "tcp": |
---|
886 | raise PrivacyError("tub.location includes tcp: hint") |
---|
887 | new_locations.append(loc) |
---|
888 | location = ",".join(new_locations) |
---|
889 | |
---|
890 | # Lacking this, Python 2 blows up in Foolscap when it is confused by a |
---|
891 | # Unicode FURL. |
---|
892 | location = location.encode("utf-8") |
---|
893 | |
---|
894 | return tubport, location |
---|
895 | |
---|
896 | |
---|
897 | def tub_listen_on(i2p_provider, tor_provider, tub, tubport, location): |
---|
898 | """ |
---|
899 | Assign a Tub its listener locations. |
---|
900 | |
---|
901 | :param i2p_provider: See ``allmydata.util.i2p_provider.create``. |
---|
902 | :param tor_provider: See ``allmydata.util.tor_provider.create``. |
---|
903 | """ |
---|
904 | for port in tubport.split(","): |
---|
905 | if port == "listen:i2p": |
---|
906 | # the I2P provider will read its section of tahoe.cfg and |
---|
907 | # return either a fully-formed Endpoint, or a descriptor |
---|
908 | # that will create one, so we don't have to stuff all the |
---|
909 | # options into the tub.port string (which would need a lot |
---|
910 | # of escaping) |
---|
911 | port_or_endpoint = i2p_provider.get_listener() |
---|
912 | elif port == "listen:tor": |
---|
913 | port_or_endpoint = tor_provider.get_listener() |
---|
914 | else: |
---|
915 | port_or_endpoint = port |
---|
916 | # Foolscap requires native strings: |
---|
917 | if isinstance(port_or_endpoint, (bytes, str)): |
---|
918 | port_or_endpoint = ensure_str(port_or_endpoint) |
---|
919 | tub.listenOn(port_or_endpoint) |
---|
920 | # This last step makes the Tub is ready for tub.registerReference() |
---|
921 | tub.setLocation(location) |
---|
922 | |
---|
923 | |
---|
924 | def create_main_tub(config, tub_options, |
---|
925 | default_connection_handlers, foolscap_connection_handlers, |
---|
926 | i2p_provider, tor_provider, |
---|
927 | handler_overrides=None, cert_filename="node.pem"): |
---|
928 | """ |
---|
929 | Creates a 'main' Foolscap Tub, typically for use as the top-level |
---|
930 | access point for a running Node. |
---|
931 | |
---|
932 | :param Config: a `_Config` instance |
---|
933 | |
---|
934 | :param dict tub_options: any options to change in the tub |
---|
935 | |
---|
936 | :param default_connection_handlers: default Foolscap connection |
---|
937 | handlers |
---|
938 | |
---|
939 | :param foolscap_connection_handlers: Foolscap connection |
---|
940 | handlers for this tub |
---|
941 | |
---|
942 | :param i2p_provider: None, or a _Provider instance if I2P is |
---|
943 | installed. |
---|
944 | |
---|
945 | :param tor_provider: None, or a _Provider instance if txtorcon + |
---|
946 | Tor are installed. |
---|
947 | """ |
---|
948 | if handler_overrides is None: |
---|
949 | handler_overrides = {} |
---|
950 | portlocation = _tub_portlocation( |
---|
951 | config, |
---|
952 | iputil.get_local_addresses_sync, |
---|
953 | iputil.allocate_tcp_port, |
---|
954 | ) |
---|
955 | |
---|
956 | # FIXME? "node.pem" was the CERTFILE option/thing |
---|
957 | certfile = config.get_private_path("node.pem") |
---|
958 | tub = create_tub( |
---|
959 | tub_options, |
---|
960 | default_connection_handlers, |
---|
961 | foolscap_connection_handlers, |
---|
962 | force_foolscap=config.get_config( |
---|
963 | "storage", "force_foolscap", default=False, boolean=True |
---|
964 | ), |
---|
965 | handler_overrides=handler_overrides, |
---|
966 | certFile=certfile, |
---|
967 | ) |
---|
968 | |
---|
969 | if portlocation is None: |
---|
970 | log.msg("Tub is not listening") |
---|
971 | else: |
---|
972 | tubport, location = portlocation |
---|
973 | tub_listen_on( |
---|
974 | i2p_provider, |
---|
975 | tor_provider, |
---|
976 | tub, |
---|
977 | tubport, |
---|
978 | location, |
---|
979 | ) |
---|
980 | log.msg("Tub location set to %r" % (location,)) |
---|
981 | return tub |
---|
982 | |
---|
983 | |
---|
984 | class Node(service.MultiService): |
---|
985 | """ |
---|
986 | This class implements common functionality of both Client nodes and Introducer nodes. |
---|
987 | """ |
---|
988 | NODETYPE = "unknown NODETYPE" |
---|
989 | CERTFILE = "node.pem" |
---|
990 | |
---|
991 | def __init__(self, config, main_tub, i2p_provider, tor_provider): |
---|
992 | """ |
---|
993 | Initialize the node with the given configuration. Its base directory |
---|
994 | is the current directory by default. |
---|
995 | """ |
---|
996 | service.MultiService.__init__(self) |
---|
997 | |
---|
998 | self.config = config |
---|
999 | self.get_config = config.get_config # XXX stopgap |
---|
1000 | self.nickname = config.nickname # XXX stopgap |
---|
1001 | |
---|
1002 | # this can go away once Client.init_client_storage_broker is moved into create_client() |
---|
1003 | # (tests sometimes have None here) |
---|
1004 | self._i2p_provider = i2p_provider |
---|
1005 | self._tor_provider = tor_provider |
---|
1006 | |
---|
1007 | self.create_log_tub() |
---|
1008 | self.logSource = "Node" |
---|
1009 | self.setup_logging() |
---|
1010 | |
---|
1011 | self.tub = main_tub |
---|
1012 | if self.tub is not None: |
---|
1013 | self.nodeid = b32decode(self.tub.tubID.upper()) # binary format |
---|
1014 | self.short_nodeid = b32encode(self.nodeid).lower()[:8] # for printing |
---|
1015 | self.config.write_config_file("my_nodeid", b32encode(self.nodeid).lower() + b"\n", mode="wb") |
---|
1016 | self.tub.setServiceParent(self) |
---|
1017 | else: |
---|
1018 | self.nodeid = self.short_nodeid = None |
---|
1019 | |
---|
1020 | self.log("Node constructed. " + __full_version__) |
---|
1021 | iputil.increase_rlimits() |
---|
1022 | |
---|
1023 | def _is_tub_listening(self): |
---|
1024 | """ |
---|
1025 | :returns: True if the main tub is listening |
---|
1026 | """ |
---|
1027 | return len(self.tub.getListeners()) > 0 |
---|
1028 | |
---|
1029 | # pull this outside of Node's __init__ too, see: |
---|
1030 | # https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2948 |
---|
1031 | def create_log_tub(self): |
---|
1032 | # The logport uses a localhost-only ephemeral Tub, with no control |
---|
1033 | # over the listening port or location. This might change if we |
---|
1034 | # discover a compelling reason for it in the future (e.g. being able |
---|
1035 | # to use "flogtool tail" against a remote server), but for now I |
---|
1036 | # think we can live without it. |
---|
1037 | self.log_tub = Tub() |
---|
1038 | portnum = iputil.listenOnUnused(self.log_tub) |
---|
1039 | self.log("Log Tub location set to 127.0.0.1:%s" % (portnum,)) |
---|
1040 | self.log_tub.setServiceParent(self) |
---|
1041 | |
---|
1042 | def startService(self): |
---|
1043 | # Note: this class can be started and stopped at most once. |
---|
1044 | self.log("Node.startService") |
---|
1045 | # Record the process id in the twisted log, after startService() |
---|
1046 | # (__init__ is called before fork(), but startService is called |
---|
1047 | # after). Note that Foolscap logs handle pid-logging by itself, no |
---|
1048 | # need to send a pid to the foolscap log here. |
---|
1049 | twlog.msg("My pid: %s" % os.getpid()) |
---|
1050 | try: |
---|
1051 | os.chmod("twistd.pid", 0o644) |
---|
1052 | except EnvironmentError: |
---|
1053 | pass |
---|
1054 | |
---|
1055 | service.MultiService.startService(self) |
---|
1056 | self.log("%s running" % self.NODETYPE) |
---|
1057 | twlog.msg("%s running" % self.NODETYPE) |
---|
1058 | |
---|
1059 | def stopService(self): |
---|
1060 | self.log("Node.stopService") |
---|
1061 | return service.MultiService.stopService(self) |
---|
1062 | |
---|
1063 | def shutdown(self): |
---|
1064 | """Shut down the node. Returns a Deferred that fires (with None) when |
---|
1065 | it finally stops kicking.""" |
---|
1066 | self.log("Node.shutdown") |
---|
1067 | return self.stopService() |
---|
1068 | |
---|
1069 | def setup_logging(self): |
---|
1070 | # we replace the formatTime() method of the log observer that |
---|
1071 | # twistd set up for us, with a method that uses our preferred |
---|
1072 | # timestamp format. |
---|
1073 | for o in twlog.theLogPublisher.observers: |
---|
1074 | # o might be a FileLogObserver's .emit method |
---|
1075 | if type(o) is type(self.setup_logging): # bound method |
---|
1076 | ob = o.__self__ |
---|
1077 | if isinstance(ob, twlog.FileLogObserver): |
---|
1078 | newmeth = types.MethodType(formatTimeTahoeStyle, ob) |
---|
1079 | ob.formatTime = newmeth |
---|
1080 | # TODO: twisted >2.5.0 offers maxRotatedFiles=50 |
---|
1081 | |
---|
1082 | lgfurl_file = self.config.get_private_path("logport.furl").encode(get_filesystem_encoding()) |
---|
1083 | if os.path.exists(lgfurl_file): |
---|
1084 | os.remove(lgfurl_file) |
---|
1085 | self.log_tub.setOption("logport-furlfile", lgfurl_file) |
---|
1086 | lgfurl = self.config.get_config("node", "log_gatherer.furl", "") |
---|
1087 | if lgfurl: |
---|
1088 | # this is in addition to the contents of log-gatherer-furlfile |
---|
1089 | lgfurl = lgfurl.encode("utf-8") |
---|
1090 | self.log_tub.setOption("log-gatherer-furl", lgfurl) |
---|
1091 | self.log_tub.setOption("log-gatherer-furlfile", |
---|
1092 | self.config.get_config_path("log_gatherer.furl")) |
---|
1093 | |
---|
1094 | incident_dir = self.config.get_config_path("logs", "incidents") |
---|
1095 | foolscap.logging.log.setLogDir(incident_dir) |
---|
1096 | twlog.msg("Foolscap logging initialized") |
---|
1097 | twlog.msg("Note to developers: twistd.log does not receive very much.") |
---|
1098 | twlog.msg("Use 'flogtool tail -c NODEDIR/private/logport.furl' instead") |
---|
1099 | twlog.msg("and read docs/logging.rst") |
---|
1100 | |
---|
1101 | def log(self, *args, **kwargs): |
---|
1102 | return log.msg(*args, **kwargs) |
---|