From d531cc9d6c9f517a5267eac41b79702cde91ed5c Mon Sep 17 00:00:00 2001 From: jehad Date: Tue, 1 Dec 2020 12:38:16 +0300 Subject: [PATCH 01/51] =?UTF-8?q?new=20README=20file=20based=20on=20Victor?= =?UTF-8?q?iia=E2=80=99s=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.rst | 151 +++++++++++++++++++--------------- docs/_static/media/image1.png | Bin 0 -> 114034 bytes docs/_static/media/image2.png | Bin 0 -> 4431 bytes newsfragments/3545.other | 1 + 4 files changed, 87 insertions(+), 65 deletions(-) create mode 100644 docs/_static/media/image1.png create mode 100644 docs/_static/media/image2.png create mode 100644 newsfragments/3545.other diff --git a/README.rst b/README.rst index d3b089186..33bb16a52 100644 --- a/README.rst +++ b/README.rst @@ -1,97 +1,118 @@ -========== -Tahoe-LAFS -========== +**Free and Open decentralized data store** -Tahoe-LAFS is a Free and Open decentralized cloud storage system. It -distributes your data across multiple servers. Even if some of the servers -fail or are taken over by an attacker, the entire file store continues to -function correctly, preserving your privacy and security. +|image0| -For full documentation, please see -http://tahoe-lafs.readthedocs.io/en/latest/ . +`Tahoe-LAFS `__ (Tahoe Least-Authority File Store) is the first free software / open-source storage technology that distributes your data across multiple servers. Even if some servers fail or are taken over by an attacker, the entire file store continues to function correctly, preserving your privacy and security. |Contributor Covenant| |readthedocs| |travis| |circleci| |codecov| -INSTALLING -========== +Table of contents -There are three ways to install Tahoe-LAFS. +- `About Tahoe-LAFS <#about-tahoe-lafs>`__ -using OS packages -^^^^^^^^^^^^^^^^^ +- `Installation <#installation>`__ -Pre-packaged versions are available for several operating systems: +- `Contributing <#contributing>`__ -* Debian and Ubuntu users can ``apt-get install tahoe-lafs`` -* NixOS, NetBSD (pkgsrc), ArchLinux, Slackware, and Gentoo have packages - available, see `OSPackages`_ for details -* `Mac`_ and Windows installers are in development. +- `Issues <#issues>`__ -via pip -^^^^^^^ +- `Documentation <#documentation>`__ -If you don't use an OS package, you'll need Python 2.7 and `pip`_. You may -also need a C compiler, and the development headers for python, libffi, and -OpenSSL. On a Debian-like system, use ``apt-get install build-essential -python-dev libffi-dev libssl-dev python-virtualenv``. On Windows, see -``_. +- `Community <#community>`__ -Then, to install the most recent release, just run: +- `FAQ <#faq>`__ -* ``pip install tahoe-lafs`` +- `License <#license>`__ -from source -^^^^^^^^^^^ -To install from source (either so you can hack on it, or just to run -pre-release code), you should create a virtualenv and install into that: +πŸ’‘ About Tahoe-LAFS +------------------- -* ``git clone https://github.com/tahoe-lafs/tahoe-lafs.git`` -* ``cd tahoe-lafs`` -* ``virtualenv --python=python2.7 venv`` -* ``venv/bin/pip install --upgrade setuptools`` -* ``venv/bin/pip install --editable .`` -* ``venv/bin/tahoe --version`` +Tahoe-LAFS helps you to store files while granting confidentiality, integrity, and availability of your data. -To run the unit test suite: +How does it work? You run a client program on your computer, which talks to one or more storage servers on other computers. When you tell your client to store a file, it will encrypt that file, encode it into multiple pieces, then spread those pieces out among various servers. The pieces are all encrypted and protected against modifications. Later, when you ask your client to retrieve the file, it will find the necessary pieces, make sure they haven’t been corrupted, reassemble them, and decrypt the result. -* ``tox`` +| |image2| +| *The image is taken from meejah's* \ `blog `__ \ *post at Torproject.org.* -You can pass arguments to ``trial`` with an environment variable. For -example, you can run the test suite on multiple cores to speed it up: +| -* ``TAHOE_LAFS_TRIAL_ARGS="-j4" tox`` +The client creates pieces (β€œshares”) that have a configurable amount of redundancy, so even if some servers fail, you can still get your data back. Corrupt shares are detected and ignored so that the system can tolerate server-side hard-drive errors. All files are encrypted (with a unique key) before uploading, so even a malicious server operator cannot read your data. The only thing you ask of the servers is that they can (usually) provide the shares when you ask for them: you aren’t relying upon them for confidentiality, integrity, or absolute availability. -For more detailed instructions, read ``_ . +Tahoe-LAFS was first designed in 2007, following the "principle of least authority", a security best practice requiring system components to only have the privilege necessary to complete their intended function and not more. -Once ``tahoe --version`` works, see ``_ to learn how to set -up your first Tahoe-LAFS node. +Please read more about Tahoe-LAFS architecture `here `__. -LICENCE -======= +βœ… Installation +--------------- -Copyright 2006-2018 The Tahoe-LAFS Software Foundation +For more detailed instructions, read `docs/INSTALL.rst `__ . -You may use this package under the GNU General Public License, version 2 or, -at your option, any later version. You may use this package under the -Transitive Grace Period Public Licence, version 1.0, or at your option, any -later version. (You may choose to use this package under the terms of either -licence, at your option.) See the file `COPYING.GPL`_ for the terms of the -GNU General Public License, version 2. See the file `COPYING.TGPPL`_ for -the terms of the Transitive Grace Period Public Licence, version 1.0. +- Building Tahoe-LAFS on Windows: https://github.com/tahoe-lafs/tahoe-lafs/blob/master/docs/windows.rst -See `TGPPL.PDF`_ for why the TGPPL exists, graphically illustrated on three -slides. +- | OS-X Packaging: + | https://github.com/tahoe-lafs/tahoe-lafs/blob/master/docs/OS-X.rst -.. _OSPackages: https://tahoe-lafs.org/trac/tahoe-lafs/wiki/OSPackages -.. _Mac: docs/OS-X.rst -.. _pip: https://pip.pypa.io/en/stable/installing/ -.. _COPYING.GPL: https://github.com/tahoe-lafs/tahoe-lafs/blob/master/COPYING.GPL -.. _COPYING.TGPPL: https://github.com/tahoe-lafs/tahoe-lafs/blob/master/COPYING.TGPPL.rst -.. _TGPPL.PDF: https://tahoe-lafs.org/~zooko/tgppl.pdf +Once tahoe --version works, see `docs/running.rst `__ to learn how to set up your first Tahoe-LAFS node. ----- +πŸ€— Contributing +--------------- +As a community-driven open source project, Tahoe-LAFS welcomes contributions of any form: + +- `Code patches `__ + +- `Documentation improvements `__ + +- `Bug reports `__ + +- `Patch reviews `__ + +Before authoring or reviewing a patch, please familiarize yourself with the `Coding Standard `__ and the `Contributor Code of Conduct `__. + +πŸ€– Issues +--------- + +Tahoe-LAFS uses the Trac instance to track `issues `__. Please email jean-paul plus tahoe-lafs at leastauthority dot com for an account. + +πŸ“‘ Documentation +---------------- + +You can find the full Tahoe-LAFS documentation at our `documentation site `__. + +πŸ’¬ Community +------------ + +Get involved with the Tahoe-LAFS community: + +- Chat with Tahoe-LAFS developers at #tahoe-lafs chat on irc.freenode.net or `Slack `__. + +- Join our `weekly conference calls `__ with core developers and interested community members. + +- Subscribe to `the tahoe-dev mailing list `__, the community forum for discussion of Tahoe-LAFS design, implementation, and usage. + +- Familiarize yourself with our `Contributor Code of Conduct `__. + +❓ FAQ +------ + +Need more information? Please check our `FAQ page `__. + +πŸ“„ License +---------- + +Copyright 2006-2020 The Tahoe-LAFS Software Foundation + +You may use this package under the GNU General Public License, version 2 or, at your option, any later version. You may use this package under the Transitive Grace Period Public Licence, version 1.0, or at your choice, any later version. (You may choose to use this package under the terms of either license, at your option.) See the file `COPYING.GPL `__ for the terms of the GNU General Public License, version 2. See the file `COPYING.TGPPL `__ for the terms of the Transitive Grace Period Public Licence, version 1.0. + +See `TGPPL.PDF `__ for why the TGPPL exists, graphically illustrated on three slides. + +.. |image0| image:: docs/_static/media/image2.png + :width: 3in + :height: 0.91667in +.. |image2| image:: docs/_static//media/image1.png + :width: 6.9252in + :height: 2.73611in .. |readthedocs| image:: http://readthedocs.org/projects/tahoe-lafs/badge/?version=latest :alt: documentation status :target: http://tahoe-lafs.readthedocs.io/en/latest/?badge=latest diff --git a/docs/_static/media/image1.png b/docs/_static/media/image1.png new file mode 100644 index 0000000000000000000000000000000000000000..e25576f471da1b669012d7bfddbee5ede5a1ad9e GIT binary patch literal 114034 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 literal 0 HcmV?d00001 diff --git a/docs/_static/media/image2.png b/docs/_static/media/image2.png new file mode 100644 index 0000000000000000000000000000000000000000..d8704f35955f515c96923e715c7a627c5c206463 GIT binary patch literal 4431 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 Date: Sun, 6 Dec 2020 11:03:31 -0500 Subject: [PATCH 02/51] news fragment --- newsfragments/3522.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3522.minor diff --git a/newsfragments/3522.minor b/newsfragments/3522.minor new file mode 100644 index 000000000..e69de29bb From 238590d7fd7929e68b7d218d02dbf676b5b911cb Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 6 Dec 2020 11:03:44 -0500 Subject: [PATCH 03/51] Remove mock by removing a bunch of unicode shenanigans --- src/allmydata/scripts/tahoe_add_alias.py | 45 ++++++------- src/allmydata/test/cli/test_alias.py | 85 +++++------------------- 2 files changed, 34 insertions(+), 96 deletions(-) diff --git a/src/allmydata/scripts/tahoe_add_alias.py b/src/allmydata/scripts/tahoe_add_alias.py index ddef46db6..c7215d1db 100644 --- a/src/allmydata/scripts/tahoe_add_alias.py +++ b/src/allmydata/scripts/tahoe_add_alias.py @@ -112,33 +112,26 @@ def _get_alias_details(nodedir): def list_aliases(options): - nodedir = options['node-directory'] - stdout = options.stdout - stderr = options.stderr - - data = _get_alias_details(nodedir) - - max_width = max([len(quote_output(name)) for name in data.keys()] + [0]) - fmt = "%" + str(max_width) + "s: %s" - rc = 0 + data = _get_alias_details(options['node-directory']) if options['json']: - try: - # XXX why are we presuming utf-8 output? - print(json.dumps(data, indent=4).decode('utf-8'), file=stdout) - except (UnicodeEncodeError, UnicodeDecodeError): - print(json.dumps(data, indent=4), file=stderr) - rc = 1 + output = json.dumps(data, indent=4) else: - for name, details in data.items(): - dircap = details['readonly'] if options['readonly-uri'] else details['readwrite'] - try: - print(fmt % (unicode_to_output(name), unicode_to_output(dircap.decode('utf-8'))), file=stdout) - except (UnicodeEncodeError, UnicodeDecodeError): - print(fmt % (quote_output(name), quote_output(dircap)), file=stderr) - rc = 1 + def dircap(details): + return ( + details['readonly'] + if options['readonly-uri'] + else details['readwrite'] + ).decode("utf-8") - if rc == 1: - print("\nThis listing included aliases or caps that could not be converted to the terminal" \ - "\noutput encoding. These are shown using backslash escapes and in quotes.", file=stderr) - return rc + max_width = max([len(quote_output(name)) for name in data.keys()] + [0]) + fmt = "%" + str(max_width) + "s: %s" + output = u"\n".join(list( + fmt % (name, dircap(details)) + for name, details + in data.items() + )) + + print(output, file=options.stdout) + + return 0 diff --git a/src/allmydata/test/cli/test_alias.py b/src/allmydata/test/cli/test_alias.py index 6542d154f..6eaa82e3e 100644 --- a/src/allmydata/test/cli/test_alias.py +++ b/src/allmydata/test/cli/test_alias.py @@ -1,5 +1,4 @@ import json -from mock import patch from twisted.trial import unittest from twisted.internet.defer import inlineCallbacks @@ -15,91 +14,37 @@ from ..common_util import skip_if_cannot_represent_argv class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): @inlineCallbacks - def test_list(self): + def _test_list(self, alias): self.basedir = "cli/ListAlias/test_list" self.set_up_grid(oneshare=True) rc, stdout, stderr = yield self.do_cli( "create-alias", - unicode_to_argv(u"tahoe"), + unicode_to_argv(alias), ) - self.failUnless(unicode_to_argv(u"Alias 'tahoe' created") in stdout) - self.failIf(stderr) + self.assertIn( + unicode_to_argv(u"Alias '{}' created".format(alias)), + stdout, + ) + self.assertEqual("", stderr) aliases = get_aliases(self.get_clientdir()) - self.failUnless(u"tahoe" in aliases) - self.failUnless(aliases[u"tahoe"].startswith("URI:DIR2:")) + self.assertIn(alias, aliases) + self.assertTrue(aliases[alias].startswith("URI:DIR2:")) rc, stdout, stderr = yield self.do_cli("list-aliases", "--json") self.assertEqual(0, rc) data = json.loads(stdout) - self.assertIn(u"tahoe", data) - data = data[u"tahoe"] + self.assertIn(alias, data) + data = data[alias] self.assertIn("readwrite", data) self.assertIn("readonly", data) - @inlineCallbacks - def test_list_unicode_mismatch_json(self): - """ - pretty hack-y test, but we want to cover the 'except' on Unicode - errors paths and I can't come up with a nicer way to trigger - this - """ - self.basedir = "cli/ListAlias/test_list_unicode_mismatch_json" - skip_if_cannot_represent_argv(u"tahoe\u263A") - self.set_up_grid(oneshare=True) - rc, stdout, stderr = yield self.do_cli( - "create-alias", - unicode_to_argv(u"tahoe\u263A"), - ) + def test_list(self): + return self._test_list(u"tahoe") - self.failUnless(unicode_to_argv(u"Alias 'tahoe\u263A' created") in stdout) - self.failIf(stderr) - booms = [] - - def boom(out, indent=4): - if not len(booms): - booms.append(out) - raise UnicodeEncodeError("foo", u"foo", 3, 5, "foo") - return str(out) - - with patch("allmydata.scripts.tahoe_add_alias.json.dumps", boom): - aliases = get_aliases(self.get_clientdir()) - self.failUnless(u"tahoe\u263A" in aliases) - self.failUnless(aliases[u"tahoe\u263A"].startswith("URI:DIR2:")) - - rc, stdout, stderr = yield self.do_cli("list-aliases", "--json") - - self.assertEqual(1, rc) - self.assertIn("could not be converted", stderr) - - @inlineCallbacks - def test_list_unicode_mismatch(self): - self.basedir = "cli/ListAlias/test_list_unicode_mismatch" - skip_if_cannot_represent_argv(u"tahoe\u263A") - self.set_up_grid(oneshare=True) - - rc, stdout, stderr = yield self.do_cli( - "create-alias", - unicode_to_argv(u"tahoe\u263A"), - ) - - def boom(out): - print("boom {}".format(out)) - return out - raise UnicodeEncodeError("foo", u"foo", 3, 5, "foo") - - with patch("allmydata.scripts.tahoe_add_alias.unicode_to_output", boom): - self.failUnless(unicode_to_argv(u"Alias 'tahoe\u263A' created") in stdout) - self.failIf(stderr) - aliases = get_aliases(self.get_clientdir()) - self.failUnless(u"tahoe\u263A" in aliases) - self.failUnless(aliases[u"tahoe\u263A"].startswith("URI:DIR2:")) - - rc, stdout, stderr = yield self.do_cli("list-aliases") - - self.assertEqual(1, rc) - self.assertIn("could not be converted", stderr) + def test_list_unicode(self): + return self._test_list(u"tahoe\{SNOWMAN}") From d29210a140145bb4445a3d12f3d227f236b30a29 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 6 Dec 2020 11:04:05 -0500 Subject: [PATCH 04/51] unused import --- src/allmydata/scripts/tahoe_add_alias.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/scripts/tahoe_add_alias.py b/src/allmydata/scripts/tahoe_add_alias.py index c7215d1db..63fa47c44 100644 --- a/src/allmydata/scripts/tahoe_add_alias.py +++ b/src/allmydata/scripts/tahoe_add_alias.py @@ -10,7 +10,7 @@ from allmydata import uri from allmydata.scripts.common_http import do_http, check_http_error from allmydata.scripts.common import get_aliases from allmydata.util.fileutil import move_into_place -from allmydata.util.encodingutil import unicode_to_output, quote_output +from allmydata.util.encodingutil import quote_output def add_line_to_aliasfile(aliasfile, alias, cap): From c4b58fe00b74fc8fef2670222c31d5d8781861d3 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 6 Dec 2020 11:04:19 -0500 Subject: [PATCH 05/51] unused import --- src/allmydata/test/cli/test_alias.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/allmydata/test/cli/test_alias.py b/src/allmydata/test/cli/test_alias.py index 6eaa82e3e..f2d65bb73 100644 --- a/src/allmydata/test/cli/test_alias.py +++ b/src/allmydata/test/cli/test_alias.py @@ -7,7 +7,6 @@ from allmydata.util.encodingutil import unicode_to_argv from allmydata.scripts.common import get_aliases from allmydata.test.no_network import GridTestMixin from .common import CLITestMixin -from ..common_util import skip_if_cannot_represent_argv # see also test_create_alias From 77bebb99161b729247c02c44a1ae80be81b18081 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 6 Dec 2020 17:00:34 -0500 Subject: [PATCH 06/51] [wip] remove mock from test_alias, along with a bunch of encoding-related changes :/ --- src/allmydata/scripts/admin.py | 2 +- src/allmydata/scripts/tahoe_add_alias.py | 34 ++++++-- src/allmydata/test/cli/common.py | 16 +++- src/allmydata/test/cli/test_alias.py | 45 +++++++---- src/allmydata/test/cli/test_cp.py | 2 +- src/allmydata/test/cli/test_create_alias.py | 12 ++- src/allmydata/test/common_util.py | 88 ++++++++++++++++++--- src/allmydata/util/encodingutil.py | 7 ++ 8 files changed, 165 insertions(+), 41 deletions(-) diff --git a/src/allmydata/scripts/admin.py b/src/allmydata/scripts/admin.py index e472ffd8c..30862a4ae 100644 --- a/src/allmydata/scripts/admin.py +++ b/src/allmydata/scripts/admin.py @@ -22,7 +22,7 @@ def print_keypair(options): class DerivePubkeyOptions(BaseOptions): def parseArgs(self, privkey): - self.privkey = privkey + self.privkey = privkey.encode("ascii") def getSynopsis(self): return "Usage: tahoe [global-options] admin derive-pubkey PRIVKEY" diff --git a/src/allmydata/scripts/tahoe_add_alias.py b/src/allmydata/scripts/tahoe_add_alias.py index 63fa47c44..10e687f32 100644 --- a/src/allmydata/scripts/tahoe_add_alias.py +++ b/src/allmydata/scripts/tahoe_add_alias.py @@ -1,4 +1,5 @@ from __future__ import print_function +from __future__ import unicode_literals import os.path import codecs @@ -10,7 +11,7 @@ from allmydata import uri from allmydata.scripts.common_http import do_http, check_http_error from allmydata.scripts.common import get_aliases from allmydata.util.fileutil import move_into_place -from allmydata.util.encodingutil import quote_output +from allmydata.util.encodingutil import quote_output, quote_output_u def add_line_to_aliasfile(aliasfile, alias, cap): @@ -48,14 +49,13 @@ def add_alias(options): old_aliases = get_aliases(nodedir) if alias in old_aliases: - print("Alias %s already exists!" % quote_output(alias), file=stderr) + print("Alias %s already exists!" % quote_output_u(alias), file=stderr) return 1 aliasfile = os.path.join(nodedir, "private", "aliases") cap = uri.from_string_dirnode(cap).to_string() add_line_to_aliasfile(aliasfile, alias, cap) - - print("Alias %s added" % quote_output(alias), file=stdout) + show_output(stdout, "Alias {alias} added", alias=alias) return 0 def create_alias(options): @@ -93,11 +93,26 @@ def create_alias(options): # probably check for others.. add_line_to_aliasfile(aliasfile, alias, new_uri) - - print("Alias %s created" % (quote_output(alias),), file=stdout) + show_output(stdout, "Alias {alias} created", alias=alias) return 0 +def show_output(fp, template, **kwargs): + assert isinstance(template, unicode) + + # On Python 2 and Python 3 fp has an encoding attribute under real usage + # but the test suite passes StringIO in many places which has no such + # attribute. Make allowances for this until the test suite is fixed. + encoding = getattr(fp, "encoding", "utf-8") + output = template.format(**{ + k: quote_output_u(v, encoding=encoding) + for (k, v) + in kwargs.items() + }) + safe_output = output.encode(encoding, "namereplace").decode(encoding) + print(safe_output, file=fp) + + def _get_alias_details(nodedir): aliases = get_aliases(nodedir) alias_names = sorted(aliases.keys()) @@ -115,7 +130,7 @@ def list_aliases(options): data = _get_alias_details(options['node-directory']) if options['json']: - output = json.dumps(data, indent=4) + output = json.dumps(data, indent=4).decode("utf-8") else: def dircap(details): return ( @@ -132,6 +147,9 @@ def list_aliases(options): in data.items() )) - print(output, file=options.stdout) + if output: + # Show whatever we computed. Skip this if there is no output to avoid + # a spurious blank line. + print(output, file=options.stdout) return 0 diff --git a/src/allmydata/test/cli/common.py b/src/allmydata/test/cli/common.py index 852dce52c..e4c96bab8 100644 --- a/src/allmydata/test/cli/common.py +++ b/src/allmydata/test/cli/common.py @@ -1,6 +1,6 @@ from ...util.encodingutil import unicode_to_argv from ...scripts import runner -from ..common_util import ReallyEqualMixin, run_cli +from ..common_util import ReallyEqualMixin, run_cli, run_cli_ex def parse_options(basedir, command, args): o = runner.Options() @@ -10,10 +10,18 @@ def parse_options(basedir, command, args): return o class CLITestMixin(ReallyEqualMixin): + def do_cli_ex(self, verb, argv, client_num=0, **kwargs): + # client_num is used to execute client CLI commands on a specific + # client. + client_dir = self.get_clientdir(i=client_num) + nodeargs = [ u"--node-directory", client_dir ] + return run_cli_ex(verb, argv, nodeargs=nodeargs, **kwargs) + + def do_cli(self, verb, *args, **kwargs): # client_num is used to execute client CLI commands on a specific # client. - client_num = kwargs.get("client_num", 0) + client_num = kwargs.pop("client_num", 0) client_dir = unicode_to_argv(self.get_clientdir(i=client_num)) - nodeargs = [ "--node-directory", client_dir ] - return run_cli(verb, nodeargs=nodeargs, *args, **kwargs) + nodeargs = [ b"--node-directory", client_dir ] + return run_cli(verb, *args, nodeargs=nodeargs, **kwargs) diff --git a/src/allmydata/test/cli/test_alias.py b/src/allmydata/test/cli/test_alias.py index f2d65bb73..9064a8c2b 100644 --- a/src/allmydata/test/cli/test_alias.py +++ b/src/allmydata/test/cli/test_alias.py @@ -3,47 +3,60 @@ import json from twisted.trial import unittest from twisted.internet.defer import inlineCallbacks -from allmydata.util.encodingutil import unicode_to_argv from allmydata.scripts.common import get_aliases from allmydata.test.no_network import GridTestMixin from .common import CLITestMixin +from allmydata.util.encodingutil import quote_output # see also test_create_alias class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): @inlineCallbacks - def _test_list(self, alias): - self.basedir = "cli/ListAlias/test_list" + def _test_list(self, alias, encoding): + self.basedir = self.mktemp() self.set_up_grid(oneshare=True) - rc, stdout, stderr = yield self.do_cli( - "create-alias", - unicode_to_argv(alias), + rc, stdout, stderr = yield self.do_cli_ex( + u"create-alias", + [alias], + encoding=encoding, ) self.assertIn( - unicode_to_argv(u"Alias '{}' created".format(alias)), - stdout, + b"Alias {} created".format(quote_output(alias, encoding=encoding)), + stdout.encode(encoding), ) self.assertEqual("", stderr) aliases = get_aliases(self.get_clientdir()) self.assertIn(alias, aliases) - self.assertTrue(aliases[alias].startswith("URI:DIR2:")) + self.assertTrue(aliases[alias].startswith(u"URI:DIR2:")) - rc, stdout, stderr = yield self.do_cli("list-aliases", "--json") + rc, stdout, stderr = yield self.do_cli_ex( + u"list-aliases", + [u"--json"], + encoding=encoding, + ) self.assertEqual(0, rc) data = json.loads(stdout) self.assertIn(alias, data) data = data[alias] - self.assertIn("readwrite", data) - self.assertIn("readonly", data) + self.assertIn(u"readwrite", data) + self.assertIn(u"readonly", data) - def test_list(self): - return self._test_list(u"tahoe") + def test_list_ascii(self): + return self._test_list(u"tahoe", encoding="ascii") - def test_list_unicode(self): - return self._test_list(u"tahoe\{SNOWMAN}") + def test_list_nonascii_ascii(self): + return self._test_list(u"tahoe\N{SNOWMAN}", encoding="ascii") + + + def test_list_utf_8(self): + return self._test_list(u"tahoe", encoding="utf-8") + + + def test_list_nonascii_utf_8(self): + return self._test_list(u"tahoe\N{SNOWMAN}", encoding="utf-8") diff --git a/src/allmydata/test/cli/test_cp.py b/src/allmydata/test/cli/test_cp.py index ba1894f1c..6cebec4a5 100644 --- a/src/allmydata/test/cli/test_cp.py +++ b/src/allmydata/test/cli/test_cp.py @@ -661,7 +661,7 @@ starting copy, 2 files, 1 directories # This test ensures that tahoe will copy a file from the grid to # a local directory without a specified file name. # https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2027 - self.basedir = "cli/Cp/cp_verbose" + self.basedir = "cli/Cp/ticket_2027" self.set_up_grid(oneshare=True) # Write a test file, which we'll copy to the grid. diff --git a/src/allmydata/test/cli/test_create_alias.py b/src/allmydata/test/cli/test_create_alias.py index ea3200e2e..324b2364a 100644 --- a/src/allmydata/test/cli/test_create_alias.py +++ b/src/allmydata/test/cli/test_create_alias.py @@ -6,7 +6,7 @@ from allmydata.util import fileutil from allmydata.scripts.common import get_aliases from allmydata.scripts import cli, runner from ..no_network import GridTestMixin -from allmydata.util.encodingutil import quote_output, get_io_encoding +from allmydata.util.encodingutil import quote_output_u, get_io_encoding from .common import CLITestMixin class CreateAlias(GridTestMixin, CLITestMixin, unittest.TestCase): @@ -171,7 +171,15 @@ class CreateAlias(GridTestMixin, CLITestMixin, unittest.TestCase): (rc, out, err) = args self.failUnlessReallyEqual(rc, 0) self.failUnlessReallyEqual(err, "") - self.failUnlessIn("Alias %s created" % quote_output(u"\u00E9tudes"), out) + self.assertIn( + u"Alias %s created" % ( + quote_output_u( + u"\u00E9tudes", + encoding=get_io_encoding(), + ), + ), + out.decode(get_io_encoding()), + ) aliases = get_aliases(self.get_clientdir()) self.failUnless(aliases[u"\u00E9tudes"].startswith("URI:DIR2:")) diff --git a/src/allmydata/test/common_util.py b/src/allmydata/test/common_util.py index e3f5cf750..48e89a851 100644 --- a/src/allmydata/test/common_util.py +++ b/src/allmydata/test/common_util.py @@ -5,6 +5,10 @@ import time import signal from random import randrange from six.moves import StringIO +from io import ( + TextIOWrapper, + BytesIO, +) from twisted.internet import reactor, defer from twisted.python import failure @@ -35,24 +39,90 @@ def skip_if_cannot_represent_argv(u): except UnicodeEncodeError: raise unittest.SkipTest("A non-ASCII argv could not be encoded on this platform.") + +def _getvalue(io): + """ + Read out the complete contents of a file-like object. + """ + io.seek(0) + return io.read() + + def run_cli(verb, *args, **kwargs): - precondition(not [True for arg in args if not isinstance(arg, str)], - "arguments to do_cli must be strs -- convert using unicode_to_argv", args=args) - nodeargs = kwargs.get("nodeargs", []) - argv = nodeargs + [verb] + list(args) - stdin = kwargs.get("stdin", "") - stdout = StringIO() - stderr = StringIO() + """ + Run some CLI command using Python 2 stdout/stderr semantics. + """ + nodeargs = kwargs.pop("nodeargs", []) + stdin = kwargs.pop("stdin", None) + precondition( + all(isinstance(arg, bytes) for arg in [verb] + (nodeargs or []) + list(args)), + "arguments to run_cli must be bytes -- convert using unicode_to_argv", + verb=verb, + args=args, + nodeargs=nodeargs, + ) + encoding = "utf-8" + d = run_cli_ex( + verb=verb.decode(encoding), + argv=list(arg.decode(encoding) for arg in args), + nodeargs=list(nodearg.decode(encoding) for nodearg in nodeargs), + stdin=stdin, + ) + def maybe_encode(result): + code, stdout, stderr = result + # Make sure we produce bytes output since that's what all the code + # written to use this interface expects. If you don't like that, use + # run_cli_ex instead. We use get_io_encoding here to make sure that + # whatever was written can actually be encoded that way, otherwise it + # wouldn't really be writeable under real usage. + if isinstance(stdout, unicode): + stdout = stdout.encode(encoding) + if isinstance(stderr, unicode): + stderr = stderr.encode(encoding) + return code, stdout, stderr + d.addCallback(maybe_encode) + return d + + +def run_cli_ex(verb, argv, nodeargs=None, stdin=None, encoding=None): + precondition( + all(isinstance(arg, unicode) for arg in [verb] + (nodeargs or []) + argv), + "arguments to run_cli_ex must be unicode", + verb=verb, + nodeargs=nodeargs, + argv=argv, + ) + if nodeargs is None: + nodeargs = [] + argv = nodeargs + [verb] + list(argv) + if stdin is None: + stdin = "" + if encoding is None: + # The original behavior, the Python 2 behavior, is to accept either + # bytes or unicode and try to automatically encode or decode as + # necessary. This works okay for ASCII and if LANG is set + # appropriately. These aren't great constraints so we should move + # away from this behavior. + stdout = StringIO() + stderr = StringIO() + else: + # The new behavior, the Python 3 behavior, is to accept unicode and + # encode it using a specific encoding. For older versions of Python + # 3, the encoding is determined from LANG (bad) but for newer Python + # 3, the encoding is always utf-8 (good). Tests can pass in different + # encodings to exercise different behaviors. + stdout = TextIOWrapper(BytesIO(), encoding) + stderr = TextIOWrapper(BytesIO(), encoding) d = defer.succeed(argv) d.addCallback(runner.parse_or_exit_with_explanation, stdout=stdout) d.addCallback(runner.dispatch, stdin=StringIO(stdin), stdout=stdout, stderr=stderr) def _done(rc): - return 0, stdout.getvalue(), stderr.getvalue() + return 0, _getvalue(stdout), _getvalue(stderr) def _err(f): f.trap(SystemExit) - return f.value.code, stdout.getvalue(), stderr.getvalue() + return f.value.code, _getvalue(stdout), _getvalue(stderr) d.addCallbacks(_done, _err) return d diff --git a/src/allmydata/util/encodingutil.py b/src/allmydata/util/encodingutil.py index 17a7a2f38..8ffd7f2f5 100644 --- a/src/allmydata/util/encodingutil.py +++ b/src/allmydata/util/encodingutil.py @@ -252,6 +252,13 @@ ESCAPABLE_UNICODE = re.compile(u'([\uD800-\uDBFF][\uDC00-\uDFFF])|' # valid sur ESCAPABLE_8BIT = re.compile( br'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E]', re.DOTALL) +def quote_output_u(*args, **kwargs): + result = quote_output(*args, **kwargs) + if isinstance(result, unicode): + return result + return result.decode("utf-8") + + def quote_output(s, quotemarks=True, quote_newlines=None, encoding=None): """ Encode either a Unicode string or a UTF-8-encoded bytestring for representation From b464fa6483025bdf8124a4880f00f63bb094e208 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 6 Dec 2020 18:28:11 -0500 Subject: [PATCH 07/51] docstring --- src/allmydata/util/encodingutil.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/allmydata/util/encodingutil.py b/src/allmydata/util/encodingutil.py index 8ffd7f2f5..cab5cd114 100644 --- a/src/allmydata/util/encodingutil.py +++ b/src/allmydata/util/encodingutil.py @@ -253,6 +253,9 @@ ESCAPABLE_UNICODE = re.compile(u'([\uD800-\uDBFF][\uDC00-\uDFFF])|' # valid sur ESCAPABLE_8BIT = re.compile( br'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E]', re.DOTALL) def quote_output_u(*args, **kwargs): + """ + Like ``quote_output`` but always return ``unicode``. + """ result = quote_output(*args, **kwargs) if isinstance(result, unicode): return result From 2955d22f723901612e62cd6411eea945a46bd3e0 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 6 Dec 2020 18:38:51 -0500 Subject: [PATCH 08/51] note a problem with test_system --- src/allmydata/test/test_system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py index 7a7fe117b..d64c56f09 100644 --- a/src/allmydata/test/test_system.py +++ b/src/allmydata/test/test_system.py @@ -2618,7 +2618,7 @@ class SystemTest(SystemTestMixin, RunBinTahoeMixin, unittest.TestCase): def _run_in_subprocess(ignored, verb, *args, **kwargs): stdin = kwargs.get("stdin") - env = kwargs.get("env", os.environ) + env = kwargs.get("env", os.environ) # XXX Gets mutated below, great. # Python warnings from the child process don't matter. env["PYTHONWARNINGS"] = "ignore" newargs = ["--node-directory", self.getdir("client0"), verb] + list(args) From 5aee8b422d8f03c24a3b08a7ea13b8ba382be93b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 6 Dec 2020 18:39:09 -0500 Subject: [PATCH 09/51] Oops there's another case --- src/allmydata/scripts/tahoe_add_alias.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/allmydata/scripts/tahoe_add_alias.py b/src/allmydata/scripts/tahoe_add_alias.py index 10e687f32..1252b0e72 100644 --- a/src/allmydata/scripts/tahoe_add_alias.py +++ b/src/allmydata/scripts/tahoe_add_alias.py @@ -100,10 +100,12 @@ def create_alias(options): def show_output(fp, template, **kwargs): assert isinstance(template, unicode) - # On Python 2 and Python 3 fp has an encoding attribute under real usage - # but the test suite passes StringIO in many places which has no such - # attribute. Make allowances for this until the test suite is fixed. - encoding = getattr(fp, "encoding", "utf-8") + # On Python 3 fp has an encoding attribute under all real usage. On + # Python 2, the encoding attribute is None if stdio is not a tty. The + # test suite often passes StringIO which has no such attribute. Make + # allowances for this until the test suite is fixed and Python 2 is no + # more. + encoding = getattr(fp, "encoding", None) or "utf-8" output = template.format(**{ k: quote_output_u(v, encoding=encoding) for (k, v) From 613777d16619859570bd1f405508e988577aeff6 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 6 Dec 2020 19:23:13 -0500 Subject: [PATCH 10/51] Make sure this one is bytes too --- src/allmydata/scripts/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/scripts/debug.py b/src/allmydata/scripts/debug.py index fd3f2b87c..b6d10c438 100644 --- a/src/allmydata/scripts/debug.py +++ b/src/allmydata/scripts/debug.py @@ -607,7 +607,7 @@ class FindSharesOptions(BaseOptions): def parseArgs(self, storage_index_s, *nodedirs): from allmydata.util.encodingutil import argv_to_abspath - self.si_s = storage_index_s + self.si_s = storage_index_s.encode("ascii") self.nodedirs = map(argv_to_abspath, nodedirs) description = """ From c12b082fa7362d92237952636f4afb8adbe96064 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 6 Dec 2020 20:37:28 -0500 Subject: [PATCH 11/51] Put run_cli back largely how it was Also deal with StringIO better in show_output --- src/allmydata/scripts/tahoe_add_alias.py | 27 ++++++- src/allmydata/test/cli/common.py | 6 +- src/allmydata/test/cli/test_alias.py | 20 ++++-- src/allmydata/test/cli/test_create_alias.py | 12 +--- src/allmydata/test/common_util.py | 79 ++++++++++----------- 5 files changed, 82 insertions(+), 62 deletions(-) diff --git a/src/allmydata/scripts/tahoe_add_alias.py b/src/allmydata/scripts/tahoe_add_alias.py index 1252b0e72..51d48e5a3 100644 --- a/src/allmydata/scripts/tahoe_add_alias.py +++ b/src/allmydata/scripts/tahoe_add_alias.py @@ -98,6 +98,20 @@ def create_alias(options): def show_output(fp, template, **kwargs): + """ + Print to just about anything. + + :param fp: A file-like object to which to print. This handles the case + where ``fp`` declares a support encoding with the ``encoding`` + attribute (eg sys.stdout on Python 3). It handles the case where + ``fp`` declares no supported encoding via ``None`` for its + ``encoding`` attribute (eg sys.stdout on Python 2 when stdout is not a + tty). It handles the case where ``fp`` declares an encoding that does + not support all of the characters in the output by forcing the + "namereplace" error handler. It handles the case where there is no + ``encoding`` attribute at all (eg StringIO.StringIO) by writing + utf-8-encoded bytes. + """ assert isinstance(template, unicode) # On Python 3 fp has an encoding attribute under all real usage. On @@ -105,13 +119,22 @@ def show_output(fp, template, **kwargs): # test suite often passes StringIO which has no such attribute. Make # allowances for this until the test suite is fixed and Python 2 is no # more. - encoding = getattr(fp, "encoding", None) or "utf-8" + try: + encoding = fp.encoding or "utf-8" + except AttributeError: + has_encoding = False + encoding = "utf-8" + else: + has_encoding = True + output = template.format(**{ k: quote_output_u(v, encoding=encoding) for (k, v) in kwargs.items() }) - safe_output = output.encode(encoding, "namereplace").decode(encoding) + safe_output = output.encode(encoding, "namereplace") + if has_encoding: + safe_output = safe_output.decode(encoding) print(safe_output, file=fp) diff --git a/src/allmydata/test/cli/common.py b/src/allmydata/test/cli/common.py index e4c96bab8..3da959604 100644 --- a/src/allmydata/test/cli/common.py +++ b/src/allmydata/test/cli/common.py @@ -1,6 +1,6 @@ from ...util.encodingutil import unicode_to_argv from ...scripts import runner -from ..common_util import ReallyEqualMixin, run_cli, run_cli_ex +from ..common_util import ReallyEqualMixin, run_cli, run_cli_unicode def parse_options(basedir, command, args): o = runner.Options() @@ -10,12 +10,12 @@ def parse_options(basedir, command, args): return o class CLITestMixin(ReallyEqualMixin): - def do_cli_ex(self, verb, argv, client_num=0, **kwargs): + def do_cli_unicode(self, verb, argv, client_num=0, **kwargs): # client_num is used to execute client CLI commands on a specific # client. client_dir = self.get_clientdir(i=client_num) nodeargs = [ u"--node-directory", client_dir ] - return run_cli_ex(verb, argv, nodeargs=nodeargs, **kwargs) + return run_cli_unicode(verb, argv, nodeargs=nodeargs, **kwargs) def do_cli(self, verb, *args, **kwargs): diff --git a/src/allmydata/test/cli/test_alias.py b/src/allmydata/test/cli/test_alias.py index 9064a8c2b..9a2b35c8c 100644 --- a/src/allmydata/test/cli/test_alias.py +++ b/src/allmydata/test/cli/test_alias.py @@ -17,22 +17,24 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): self.basedir = self.mktemp() self.set_up_grid(oneshare=True) - rc, stdout, stderr = yield self.do_cli_ex( + rc, stdout, stderr = yield self.do_cli_unicode( u"create-alias", [alias], encoding=encoding, ) - self.assertIn( - b"Alias {} created".format(quote_output(alias, encoding=encoding)), - stdout.encode(encoding), + self.assertEqual( + b"Alias {} created\n".format( + quote_output(alias, encoding=encoding), + ), + stdout, ) self.assertEqual("", stderr) aliases = get_aliases(self.get_clientdir()) self.assertIn(alias, aliases) self.assertTrue(aliases[alias].startswith(u"URI:DIR2:")) - rc, stdout, stderr = yield self.do_cli_ex( + rc, stdout, stderr = yield self.do_cli_unicode( u"list-aliases", [u"--json"], encoding=encoding, @@ -60,3 +62,11 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): def test_list_nonascii_utf_8(self): return self._test_list(u"tahoe\N{SNOWMAN}", encoding="utf-8") + + + def test_list_none(self): + return self._test_list(u"tahoe", encoding=None) + + + def test_list_nonascii_none(self): + return self._test_list(u"tahoe\N{SNOWMAN}", encoding=None) diff --git a/src/allmydata/test/cli/test_create_alias.py b/src/allmydata/test/cli/test_create_alias.py index 324b2364a..ea3200e2e 100644 --- a/src/allmydata/test/cli/test_create_alias.py +++ b/src/allmydata/test/cli/test_create_alias.py @@ -6,7 +6,7 @@ from allmydata.util import fileutil from allmydata.scripts.common import get_aliases from allmydata.scripts import cli, runner from ..no_network import GridTestMixin -from allmydata.util.encodingutil import quote_output_u, get_io_encoding +from allmydata.util.encodingutil import quote_output, get_io_encoding from .common import CLITestMixin class CreateAlias(GridTestMixin, CLITestMixin, unittest.TestCase): @@ -171,15 +171,7 @@ class CreateAlias(GridTestMixin, CLITestMixin, unittest.TestCase): (rc, out, err) = args self.failUnlessReallyEqual(rc, 0) self.failUnlessReallyEqual(err, "") - self.assertIn( - u"Alias %s created" % ( - quote_output_u( - u"\u00E9tudes", - encoding=get_io_encoding(), - ), - ), - out.decode(get_io_encoding()), - ) + self.failUnlessIn("Alias %s created" % quote_output(u"\u00E9tudes"), out) aliases = get_aliases(self.get_clientdir()) self.failUnless(aliases[u"\u00E9tudes"].startswith("URI:DIR2:")) diff --git a/src/allmydata/test/common_util.py b/src/allmydata/test/common_util.py index 48e89a851..8b4a33197 100644 --- a/src/allmydata/test/common_util.py +++ b/src/allmydata/test/common_util.py @@ -48,55 +48,18 @@ def _getvalue(io): return io.read() -def run_cli(verb, *args, **kwargs): - """ - Run some CLI command using Python 2 stdout/stderr semantics. - """ +def run_cli_bytes(verb, *args, **kwargs): nodeargs = kwargs.pop("nodeargs", []) - stdin = kwargs.pop("stdin", None) + encoding = kwargs.pop("encoding", None) precondition( - all(isinstance(arg, bytes) for arg in [verb] + (nodeargs or []) + list(args)), + all(isinstance(arg, bytes) for arg in [verb] + nodeargs + list(args)), "arguments to run_cli must be bytes -- convert using unicode_to_argv", verb=verb, args=args, nodeargs=nodeargs, ) - encoding = "utf-8" - d = run_cli_ex( - verb=verb.decode(encoding), - argv=list(arg.decode(encoding) for arg in args), - nodeargs=list(nodearg.decode(encoding) for nodearg in nodeargs), - stdin=stdin, - ) - def maybe_encode(result): - code, stdout, stderr = result - # Make sure we produce bytes output since that's what all the code - # written to use this interface expects. If you don't like that, use - # run_cli_ex instead. We use get_io_encoding here to make sure that - # whatever was written can actually be encoded that way, otherwise it - # wouldn't really be writeable under real usage. - if isinstance(stdout, unicode): - stdout = stdout.encode(encoding) - if isinstance(stderr, unicode): - stderr = stderr.encode(encoding) - return code, stdout, stderr - d.addCallback(maybe_encode) - return d - - -def run_cli_ex(verb, argv, nodeargs=None, stdin=None, encoding=None): - precondition( - all(isinstance(arg, unicode) for arg in [verb] + (nodeargs or []) + argv), - "arguments to run_cli_ex must be unicode", - verb=verb, - nodeargs=nodeargs, - argv=argv, - ) - if nodeargs is None: - nodeargs = [] - argv = nodeargs + [verb] + list(argv) - if stdin is None: - stdin = "" + argv = nodeargs + [verb] + list(args) + stdin = kwargs.get("stdin", "") if encoding is None: # The original behavior, the Python 2 behavior, is to accept either # bytes or unicode and try to automatically encode or decode as @@ -126,6 +89,38 @@ def run_cli_ex(verb, argv, nodeargs=None, stdin=None, encoding=None): d.addCallbacks(_done, _err) return d + +def run_cli_unicode(verb, argv, nodeargs=None, stdin=None, encoding=None): + if nodeargs is None: + nodeargs = [] + precondition( + all(isinstance(arg, unicode) for arg in [verb] + nodeargs + argv), + "arguments to run_cli_unicode must be unicode", + verb=verb, + nodeargs=nodeargs, + argv=argv, + ) + d = run_cli_bytes( + verb.encode("utf-8"), + nodeargs=list(arg.encode("utf-8") for arg in nodeargs), + stdin=stdin, + encoding=encoding, + *list(arg.encode("utf-8") for arg in argv) + ) + def maybe_decode(result): + code, stdout, stderr = result + if isinstance(stdout, unicode): + stdout = stdout.encode("utf-8") + if isinstance(stderr, unicode): + stderr = stderr.encode("utf-8") + return code, stdout, stderr + d.addCallback(maybe_decode) + return d + + +run_cli = run_cli_bytes + + def parse_cli(*argv): # This parses the CLI options (synchronously), and returns the Options # argument, or throws usage.UsageError if something went wrong. From 8ca98bb8caf4602918b160cb361ca82bc8507c1f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 09:06:00 -0500 Subject: [PATCH 12/51] using run_cli_unicode, better expect unicode result --- src/allmydata/test/cli/test_alias.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/allmydata/test/cli/test_alias.py b/src/allmydata/test/cli/test_alias.py index 9a2b35c8c..150cf3fa2 100644 --- a/src/allmydata/test/cli/test_alias.py +++ b/src/allmydata/test/cli/test_alias.py @@ -6,7 +6,7 @@ from twisted.internet.defer import inlineCallbacks from allmydata.scripts.common import get_aliases from allmydata.test.no_network import GridTestMixin from .common import CLITestMixin -from allmydata.util.encodingutil import quote_output +from allmydata.util.encodingutil import quote_output_u # see also test_create_alias @@ -24,8 +24,8 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): ) self.assertEqual( - b"Alias {} created\n".format( - quote_output(alias, encoding=encoding), + u"Alias {} created\n".format( + quote_output_u(alias, encoding=encoding), ), stdout, ) From 93b30d0ddecf4f5ce3857e54a670fd9b6317cb23 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 09:06:21 -0500 Subject: [PATCH 13/51] The implementation can't reliably see the encoding we're faking without this --- src/allmydata/test/cli/test_alias.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/allmydata/test/cli/test_alias.py b/src/allmydata/test/cli/test_alias.py index 150cf3fa2..0cad819e7 100644 --- a/src/allmydata/test/cli/test_alias.py +++ b/src/allmydata/test/cli/test_alias.py @@ -7,6 +7,7 @@ from allmydata.scripts.common import get_aliases from allmydata.test.no_network import GridTestMixin from .common import CLITestMixin from allmydata.util.encodingutil import quote_output_u +from allmydata.util import encodingutil # see also test_create_alias @@ -17,6 +18,8 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): self.basedir = self.mktemp() self.set_up_grid(oneshare=True) + self.patch(encodingutil, "io_encoding", encoding) + rc, stdout, stderr = yield self.do_cli_unicode( u"create-alias", [alias], From 72a5b571caa91a88d9c18f0cf3ed2e0d85069563 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 09:10:59 -0500 Subject: [PATCH 14/51] Only test the cases we can make work everywhere These tests previously (in this branch) tried to exercise more ``show_output`` logic than they can actually reach due to the requirement that argv be interpretable. Shrink the test suite down to just what we can squeeze through argv and deal with fully testing ``show_output`` elsewhere. --- src/allmydata/test/cli/test_alias.py | 46 ++++++++++++++++++---------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/allmydata/test/cli/test_alias.py b/src/allmydata/test/cli/test_alias.py index 0cad819e7..a04727db5 100644 --- a/src/allmydata/test/cli/test_alias.py +++ b/src/allmydata/test/cli/test_alias.py @@ -52,24 +52,38 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): def test_list_ascii(self): - return self._test_list(u"tahoe", encoding="ascii") + """ + An alias composed of all ASCII-encodeable code points can be created when + the active encoding is ASCII. + """ + return self._test_list( + u"tahoe", + encoding="ascii", + ) - def test_list_nonascii_ascii(self): - return self._test_list(u"tahoe\N{SNOWMAN}", encoding="ascii") + def test_list_latin_1(self): + """ + An alias composed of all Latin-1-encodeable code points can be created + when the active encoding is Latin-1. + + This is very similar to ``test_list_utf_8`` but the assumption of + UTF-8 is nearly ubiquitous and explicitly exercising the codepaths + with a UTF-8-incompatible encoding helps flush out unintentional UTF-8 + assumptions. + """ + return self._test_list( + u"taho\N{LATIN SMALL LETTER E WITH ACUTE}", + encoding="latin-1", + ) def test_list_utf_8(self): - return self._test_list(u"tahoe", encoding="utf-8") - - - def test_list_nonascii_utf_8(self): - return self._test_list(u"tahoe\N{SNOWMAN}", encoding="utf-8") - - - def test_list_none(self): - return self._test_list(u"tahoe", encoding=None) - - - def test_list_nonascii_none(self): - return self._test_list(u"tahoe\N{SNOWMAN}", encoding=None) + """ + An alias composed of all UTF-8-encodeable code points can be created when + the active encoding is UTF-8. + """ + return self._test_list( + u"tahoe\N{SNOWMAN}", + encoding="utf-8", + ) From 56f141e170b59db25279583a397bb945ad01d569 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 09:12:04 -0500 Subject: [PATCH 15/51] decode instead of encoding in maybe_decode legacy from when the bytes/unicode tower was upsidedown compared to how it is now --- src/allmydata/test/common_util.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/allmydata/test/common_util.py b/src/allmydata/test/common_util.py index 8b4a33197..4f07dda7c 100644 --- a/src/allmydata/test/common_util.py +++ b/src/allmydata/test/common_util.py @@ -109,10 +109,10 @@ def run_cli_unicode(verb, argv, nodeargs=None, stdin=None, encoding=None): ) def maybe_decode(result): code, stdout, stderr = result - if isinstance(stdout, unicode): - stdout = stdout.encode("utf-8") - if isinstance(stderr, unicode): - stderr = stderr.encode("utf-8") + if isinstance(stdout, bytes): + stdout = stdout.decode(encoding) + if isinstance(stderr, bytes): + stderr = stderr.decode(encoding) return code, stdout, stderr d.addCallback(maybe_decode) return d From f4432d3f23987c0e32b8f2f223e7a7ef530558d4 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 09:12:38 -0500 Subject: [PATCH 16/51] Respect the provided encoding UTF-8 is great but if we're claiming the encoding is something else everywhere else we can't just make it UTF-8 here. --- src/allmydata/test/common_util.py | 6 +++--- src/allmydata/util/encodingutil.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/allmydata/test/common_util.py b/src/allmydata/test/common_util.py index 4f07dda7c..c6491d72e 100644 --- a/src/allmydata/test/common_util.py +++ b/src/allmydata/test/common_util.py @@ -101,11 +101,11 @@ def run_cli_unicode(verb, argv, nodeargs=None, stdin=None, encoding=None): argv=argv, ) d = run_cli_bytes( - verb.encode("utf-8"), - nodeargs=list(arg.encode("utf-8") for arg in nodeargs), + verb.encode(encoding), + nodeargs=list(arg.encode(encoding) for arg in nodeargs), stdin=stdin, encoding=encoding, - *list(arg.encode("utf-8") for arg in argv) + *list(arg.encode(encoding) for arg in argv) ) def maybe_decode(result): code, stdout, stderr = result diff --git a/src/allmydata/util/encodingutil.py b/src/allmydata/util/encodingutil.py index cab5cd114..f13dc5b8e 100644 --- a/src/allmydata/util/encodingutil.py +++ b/src/allmydata/util/encodingutil.py @@ -259,7 +259,7 @@ def quote_output_u(*args, **kwargs): result = quote_output(*args, **kwargs) if isinstance(result, unicode): return result - return result.decode("utf-8") + return result.decode(kwargs.get("encoding", None) or io_encoding) def quote_output(s, quotemarks=True, quote_newlines=None, encoding=None): From 7b3a5aceb82f3b28f198de485933f8823e9592c5 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 09:21:56 -0500 Subject: [PATCH 17/51] These tests can't reach any of the codepaths where quote_output matters So simplify --- src/allmydata/test/cli/test_alias.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/allmydata/test/cli/test_alias.py b/src/allmydata/test/cli/test_alias.py index a04727db5..335239d5f 100644 --- a/src/allmydata/test/cli/test_alias.py +++ b/src/allmydata/test/cli/test_alias.py @@ -6,7 +6,6 @@ from twisted.internet.defer import inlineCallbacks from allmydata.scripts.common import get_aliases from allmydata.test.no_network import GridTestMixin from .common import CLITestMixin -from allmydata.util.encodingutil import quote_output_u from allmydata.util import encodingutil # see also test_create_alias @@ -15,6 +14,21 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): @inlineCallbacks def _test_list(self, alias, encoding): + """ + Assert that ``tahoe create-alias`` can be used to create an alias named + ``alias`` when argv is encoded using ``encoding``. + + :param unicode alias: The alias to try to create. + + :param str encoding: The name of an encoding to force the + ``create-alias`` implementation to use. This simulates the + effects of setting LANG and doing other locale-foolishness without + actually having to mess with this process's global locale state. + + :return Deferred: A Deferred that fires with success if the alias can + be created and that creation is reported on stdout appropriately + encoded or with failure if something goes wrong. + """ self.basedir = self.mktemp() self.set_up_grid(oneshare=True) @@ -27,9 +41,7 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): ) self.assertEqual( - u"Alias {} created\n".format( - quote_output_u(alias, encoding=encoding), - ), + u"Alias '{}' created\n".format(alias), stdout, ) self.assertEqual("", stderr) From 05d271c7c89a03e78c32d5bf94da8cbcdace1ac9 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 09:26:58 -0500 Subject: [PATCH 18/51] a little more exposition --- src/allmydata/test/cli/test_alias.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/allmydata/test/cli/test_alias.py b/src/allmydata/test/cli/test_alias.py index 335239d5f..67f438fa5 100644 --- a/src/allmydata/test/cli/test_alias.py +++ b/src/allmydata/test/cli/test_alias.py @@ -32,6 +32,12 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): self.basedir = self.mktemp() self.set_up_grid(oneshare=True) + # We can pass an encoding into the test utilities to invoke the code + # under test but we can't pass such a parameter directly to the code + # under test. Instead, that code looks at io_encoding. So, + # monkey-patch that value to our desired value here. This is the code + # that most directly takes the place of messing with LANG or the + # locale module. self.patch(encodingutil, "io_encoding", encoding) rc, stdout, stderr = yield self.do_cli_unicode( @@ -40,15 +46,20 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): encoding=encoding, ) - self.assertEqual( - u"Alias '{}' created\n".format(alias), - stdout, - ) + # Make sure the result of the create-alias command is as we want it to + # be. + self.assertEqual(u"Alias '{}' created\n".format(alias), stdout) self.assertEqual("", stderr) + self.assertEqual(0, rc) + + # Make sure it had the intended side-effect, too - an alias created in + # the node filesystem state. aliases = get_aliases(self.get_clientdir()) self.assertIn(alias, aliases) self.assertTrue(aliases[alias].startswith(u"URI:DIR2:")) + # And inspect the state via the user interface list-aliases command + # too. rc, stdout, stderr = yield self.do_cli_unicode( u"list-aliases", [u"--json"], From 72744c9464309bc3fd402303c8f080801591e2ea Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 09:47:48 -0500 Subject: [PATCH 19/51] more docstrings and properly support (and use) encoding=None throughout --- src/allmydata/test/cli/common.py | 21 ++++++++++++++++++ src/allmydata/test/cli/test_alias.py | 18 ++++++++++++++-- src/allmydata/test/common_util.py | 32 ++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/allmydata/test/cli/common.py b/src/allmydata/test/cli/common.py index 3da959604..d90b6a39f 100644 --- a/src/allmydata/test/cli/common.py +++ b/src/allmydata/test/cli/common.py @@ -10,7 +10,24 @@ def parse_options(basedir, command, args): return o class CLITestMixin(ReallyEqualMixin): + """ + A mixin for use with ``GridTestMixin`` to execute CLI commands against + nodes created by methods of that mixin. + """ def do_cli_unicode(self, verb, argv, client_num=0, **kwargs): + """ + Run a Tahoe-LAFS CLI command. + + :param verb: See ``run_cli_unicode``. + + :param argv: See ``run_cli_unicode``. + + :param int client_num: The number of the ``GridTestMixin``-created + node against which to execute the command. + + :param kwargs: Additional keyword arguments to pass to + ``run_cli_unicode``. + """ # client_num is used to execute client CLI commands on a specific # client. client_dir = self.get_clientdir(i=client_num) @@ -19,6 +36,10 @@ class CLITestMixin(ReallyEqualMixin): def do_cli(self, verb, *args, **kwargs): + """ + Like ``do_cli_unicode`` but work with ``bytes`` everywhere instead of + ``unicode``. + """ # client_num is used to execute client CLI commands on a specific # client. client_num = kwargs.pop("client_num", 0) diff --git a/src/allmydata/test/cli/test_alias.py b/src/allmydata/test/cli/test_alias.py index 67f438fa5..635ed0aba 100644 --- a/src/allmydata/test/cli/test_alias.py +++ b/src/allmydata/test/cli/test_alias.py @@ -20,10 +20,13 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): :param unicode alias: The alias to try to create. - :param str encoding: The name of an encoding to force the + :param NoneType|str encoding: The name of an encoding to force the ``create-alias`` implementation to use. This simulates the effects of setting LANG and doing other locale-foolishness without actually having to mess with this process's global locale state. + If this is ``None`` then the encoding used will be ascii but the + stdio objects given to the code under test will not declare any + encoding (this is like Python 2 when stdio is not a tty). :return Deferred: A Deferred that fires with success if the alias can be created and that creation is reported on stdout appropriately @@ -38,7 +41,7 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): # monkey-patch that value to our desired value here. This is the code # that most directly takes the place of messing with LANG or the # locale module. - self.patch(encodingutil, "io_encoding", encoding) + self.patch(encodingutil, "io_encoding", encoding or "ascii") rc, stdout, stderr = yield self.do_cli_unicode( u"create-alias", @@ -74,6 +77,17 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): self.assertIn(u"readonly", data) + def test_list_none(self): + """ + An alias composed of all ASCII-encodeable code points can be created when + stdio aren't clearly marked with an encoding. + """ + return self._test_list( + u"tahoe", + encoding=None, + ) + + def test_list_ascii(self): """ An alias composed of all ASCII-encodeable code points can be created when diff --git a/src/allmydata/test/common_util.py b/src/allmydata/test/common_util.py index c6491d72e..c08dc03d5 100644 --- a/src/allmydata/test/common_util.py +++ b/src/allmydata/test/common_util.py @@ -91,6 +91,24 @@ def run_cli_bytes(verb, *args, **kwargs): def run_cli_unicode(verb, argv, nodeargs=None, stdin=None, encoding=None): + """ + Run a Tahoe-LAFS CLI command. + + :param unicode verb: The command to run. For example, ``u"create-node"``. + + :param [unicode] argv: The arguments to pass to the command. For example, + ``[u"--hostname=localhost"]``. + + :param [unicode] nodeargs: Extra arguments to pass to the Tahoe executable + before ``verb``. + + :param unicode stdin: Text to pass to the command via stdin. + + :param NoneType|str encoding: The name of an encoding to use for all + bytes/unicode conversions necessary *and* the encoding to cause stdio + to declare with its ``encoding`` attribute. ``None`` means ASCII will + be used and no declaration will be made at all. + """ if nodeargs is None: nodeargs = [] precondition( @@ -100,19 +118,21 @@ def run_cli_unicode(verb, argv, nodeargs=None, stdin=None, encoding=None): nodeargs=nodeargs, argv=argv, ) + codec = encoding or "ascii" + encode = lambda t: None if t is None else t.encode(codec) d = run_cli_bytes( - verb.encode(encoding), - nodeargs=list(arg.encode(encoding) for arg in nodeargs), - stdin=stdin, + encode(verb), + nodeargs=list(encode(arg) for arg in nodeargs), + stdin=encode(stdin), encoding=encoding, - *list(arg.encode(encoding) for arg in argv) + *list(encode(arg) for arg in argv) ) def maybe_decode(result): code, stdout, stderr = result if isinstance(stdout, bytes): - stdout = stdout.decode(encoding) + stdout = stdout.decode(codec) if isinstance(stderr, bytes): - stderr = stderr.decode(encoding) + stderr = stderr.decode(codec) return code, stdout, stderr d.addCallback(maybe_decode) return d From d2664121b98bbb9300f1504d6ea84cd35a483bea Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 09:51:34 -0500 Subject: [PATCH 20/51] backout no-longer required unrelated change --- src/allmydata/scripts/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/scripts/admin.py b/src/allmydata/scripts/admin.py index 30862a4ae..e472ffd8c 100644 --- a/src/allmydata/scripts/admin.py +++ b/src/allmydata/scripts/admin.py @@ -22,7 +22,7 @@ def print_keypair(options): class DerivePubkeyOptions(BaseOptions): def parseArgs(self, privkey): - self.privkey = privkey.encode("ascii") + self.privkey = privkey def getSynopsis(self): return "Usage: tahoe [global-options] admin derive-pubkey PRIVKEY" From a8e3424ef635afbc5e2432e8cbef253c16cccb2b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 09:55:27 -0500 Subject: [PATCH 21/51] remove another unrelated change that's no longer required --- src/allmydata/scripts/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/scripts/debug.py b/src/allmydata/scripts/debug.py index b6d10c438..fd3f2b87c 100644 --- a/src/allmydata/scripts/debug.py +++ b/src/allmydata/scripts/debug.py @@ -607,7 +607,7 @@ class FindSharesOptions(BaseOptions): def parseArgs(self, storage_index_s, *nodedirs): from allmydata.util.encodingutil import argv_to_abspath - self.si_s = storage_index_s.encode("ascii") + self.si_s = storage_index_s self.nodedirs = map(argv_to_abspath, nodedirs) description = """ From 82aee95ef6acccb71dc0d618340fe23a9b2cdcb6 Mon Sep 17 00:00:00 2001 From: jbaeth Date: Mon, 7 Dec 2020 17:06:28 +0300 Subject: [PATCH 22/51] readme fixes --- CREDITS | 4 ++++ README.rst | 17 +++++++++-------- newsfragments/3545.other | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/CREDITS b/CREDITS index 1810a86b9..1394d87d8 100644 --- a/CREDITS +++ b/CREDITS @@ -203,3 +203,7 @@ N: meejah E: meejah@meejah.ca P: 0xC2602803128069A7, 9D5A 2BD5 688E CB88 9DEB CD3F C260 2803 1280 69A7 D: various bug-fixes and features + +N: Viktoriia Savchuk +W: https://twitter.com/viktoriiasvchk +D: Developer community focused improvements on the README file. \ No newline at end of file diff --git a/README.rst b/README.rst index 33bb16a52..dae521e2d 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,6 @@ -**Free and Open decentralized data store** +====================================== +Free and Open decentralized data store +====================================== |image0| @@ -41,19 +43,18 @@ The client creates pieces (β€œshares”) that have a configurable amount of redu Tahoe-LAFS was first designed in 2007, following the "principle of least authority", a security best practice requiring system components to only have the privilege necessary to complete their intended function and not more. -Please read more about Tahoe-LAFS architecture `here `__. +Please read more about Tahoe-LAFS architecture `here `__. βœ… Installation --------------- -For more detailed instructions, read `docs/INSTALL.rst `__ . +For more detailed instructions, read `docs/INSTALL.rst `__ . -- Building Tahoe-LAFS on Windows: https://github.com/tahoe-lafs/tahoe-lafs/blob/master/docs/windows.rst +- `Building Tahoe-LAFS on Windows `__ -- | OS-X Packaging: - | https://github.com/tahoe-lafs/tahoe-lafs/blob/master/docs/OS-X.rst +- `OS-X Packaging `__ -Once tahoe --version works, see `docs/running.rst `__ to learn how to set up your first Tahoe-LAFS node. +Once tahoe --version works, see `docs/running.rst `__ to learn how to set up your first Tahoe-LAFS node. πŸ€— Contributing --------------- @@ -110,7 +111,7 @@ See `TGPPL.PDF `__ for why the TGPPL ex .. |image0| image:: docs/_static/media/image2.png :width: 3in :height: 0.91667in -.. |image2| image:: docs/_static//media/image1.png +.. |image2| image:: docs/_static/media/image1.png :width: 6.9252in :height: 2.73611in .. |readthedocs| image:: http://readthedocs.org/projects/tahoe-lafs/badge/?version=latest diff --git a/newsfragments/3545.other b/newsfragments/3545.other index f2a8bf182..4c5a0700b 100644 --- a/newsfragments/3545.other +++ b/newsfragments/3545.other @@ -1 +1 @@ -new README based on Victoriia's changes after integrating the team's feedback. +new README based on Viktoriia's changes after integrating the team's feedback. README document now is more focused on the developer's community and it provide more information about Tahoe-LAFS, why it's important and how someone can use it or start contributing to it. From c7358e66396642f0acf08c20d2ab06a338205ed2 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 10:16:48 -0500 Subject: [PATCH 23/51] Switch over to the helper in the two functions that matter for this PR --- src/allmydata/scripts/tahoe_add_alias.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/scripts/tahoe_add_alias.py b/src/allmydata/scripts/tahoe_add_alias.py index 51d48e5a3..4451f7fad 100644 --- a/src/allmydata/scripts/tahoe_add_alias.py +++ b/src/allmydata/scripts/tahoe_add_alias.py @@ -49,7 +49,7 @@ def add_alias(options): old_aliases = get_aliases(nodedir) if alias in old_aliases: - print("Alias %s already exists!" % quote_output_u(alias), file=stderr) + show_output(stderr, "Alias {alias} already exists!", alias=alias) return 1 aliasfile = os.path.join(nodedir, "private", "aliases") cap = uri.from_string_dirnode(cap).to_string() @@ -75,7 +75,7 @@ def create_alias(options): old_aliases = get_aliases(nodedir) if alias in old_aliases: - print("Alias %s already exists!" % quote_output(alias), file=stderr) + show_output(stderr, "Alias {alias} already exists!", alias=alias) return 1 aliasfile = os.path.join(nodedir, "private", "aliases") From 87e808b3927981321a4aa4441d5f65c1ad824834 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 10:18:05 -0500 Subject: [PATCH 24/51] one more switch --- src/allmydata/scripts/tahoe_add_alias.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/allmydata/scripts/tahoe_add_alias.py b/src/allmydata/scripts/tahoe_add_alias.py index 4451f7fad..df53c95a2 100644 --- a/src/allmydata/scripts/tahoe_add_alias.py +++ b/src/allmydata/scripts/tahoe_add_alias.py @@ -152,6 +152,9 @@ def _get_alias_details(nodedir): def list_aliases(options): + """ + Show aliases that exist. + """ data = _get_alias_details(options['node-directory']) if options['json']: @@ -175,6 +178,6 @@ def list_aliases(options): if output: # Show whatever we computed. Skip this if there is no output to avoid # a spurious blank line. - print(output, file=options.stdout) + show_output(options.stdout, output) return 0 From d6d64f6b2759be794e758370ef36395601eb3738 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 10:37:22 -0500 Subject: [PATCH 25/51] fix the json case --- src/allmydata/scripts/tahoe_add_alias.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/allmydata/scripts/tahoe_add_alias.py b/src/allmydata/scripts/tahoe_add_alias.py index df53c95a2..581fb653a 100644 --- a/src/allmydata/scripts/tahoe_add_alias.py +++ b/src/allmydata/scripts/tahoe_add_alias.py @@ -151,6 +151,15 @@ def _get_alias_details(nodedir): return data +def _escape_format(t): + """ + _escape_format(t).format() == t + + :param unicode t: The text to escape. + """ + return t.replace("{", "{{").replace("}", "}}") + + def list_aliases(options): """ Show aliases that exist. @@ -158,7 +167,7 @@ def list_aliases(options): data = _get_alias_details(options['node-directory']) if options['json']: - output = json.dumps(data, indent=4).decode("utf-8") + output = _escape_format(json.dumps(data, indent=4).decode("utf-8")) else: def dircap(details): return ( From 1a77ba5698420ea75f7c1014e89bbc3d6cb8d94f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 10:37:25 -0500 Subject: [PATCH 26/51] remove redundant u prefix --- src/allmydata/scripts/tahoe_add_alias.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/scripts/tahoe_add_alias.py b/src/allmydata/scripts/tahoe_add_alias.py index 581fb653a..442fecf71 100644 --- a/src/allmydata/scripts/tahoe_add_alias.py +++ b/src/allmydata/scripts/tahoe_add_alias.py @@ -178,7 +178,7 @@ def list_aliases(options): max_width = max([len(quote_output(name)) for name in data.keys()] + [0]) fmt = "%" + str(max_width) + "s: %s" - output = u"\n".join(list( + output = "\n".join(list( fmt % (name, dircap(details)) for name, details in data.items() From 61ee26fb00f39efb78633f320d51d883aeeaf950 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 7 Dec 2020 10:46:20 -0500 Subject: [PATCH 27/51] ticket reference --- src/allmydata/test/test_system.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py index d64c56f09..7b9ec5849 100644 --- a/src/allmydata/test/test_system.py +++ b/src/allmydata/test/test_system.py @@ -2618,7 +2618,8 @@ class SystemTest(SystemTestMixin, RunBinTahoeMixin, unittest.TestCase): def _run_in_subprocess(ignored, verb, *args, **kwargs): stdin = kwargs.get("stdin") - env = kwargs.get("env", os.environ) # XXX Gets mutated below, great. + # XXX https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3548 + env = kwargs.get("env", os.environ) # Python warnings from the child process don't matter. env["PYTHONWARNINGS"] = "ignore" newargs = ["--node-directory", self.getdir("client0"), verb] + list(args) From bb48898f946783865773979f3d7565042f54baa5 Mon Sep 17 00:00:00 2001 From: jbaeth Date: Tue, 8 Dec 2020 15:28:29 +0300 Subject: [PATCH 28/51] reorder sections and remove redundant "code of conduct" link --- README.rst | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index dae521e2d..49bd61946 100644 --- a/README.rst +++ b/README.rst @@ -56,6 +56,17 @@ For more detailed instructions, read `docs/INSTALL.rst `__ . Once tahoe --version works, see `docs/running.rst `__ to learn how to set up your first Tahoe-LAFS node. +πŸ’¬ Community +------------ + +Get involved with the Tahoe-LAFS community: + +- Chat with Tahoe-LAFS developers at #tahoe-lafs chat on irc.freenode.net or `Slack `__. + +- Join our `weekly conference calls `__ with core developers and interested community members. + +- Subscribe to `the tahoe-dev mailing list `__, the community forum for discussion of Tahoe-LAFS design, implementation, and usage. + πŸ€— Contributing --------------- @@ -81,18 +92,6 @@ Tahoe-LAFS uses the Trac instance to track `issues `__. -πŸ’¬ Community ------------- - -Get involved with the Tahoe-LAFS community: - -- Chat with Tahoe-LAFS developers at #tahoe-lafs chat on irc.freenode.net or `Slack `__. - -- Join our `weekly conference calls `__ with core developers and interested community members. - -- Subscribe to `the tahoe-dev mailing list `__, the community forum for discussion of Tahoe-LAFS design, implementation, and usage. - -- Familiarize yourself with our `Contributor Code of Conduct `__. ❓ FAQ ------ From 6d1f3861fc415358b5a3aaf3cc77b9d7c58769d2 Mon Sep 17 00:00:00 2001 From: jbaeth Date: Tue, 8 Dec 2020 19:30:24 +0300 Subject: [PATCH 29/51] rewording news fragment fixing some links fix ToC order --- README.rst | 8 ++++---- newsfragments/3545.other | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 49bd61946..19344d7ae 100644 --- a/README.rst +++ b/README.rst @@ -15,14 +15,14 @@ Table of contents - `Installation <#installation>`__ -- `Contributing <#contributing>`__ - - `Issues <#issues>`__ - `Documentation <#documentation>`__ - `Community <#community>`__ +- `Contributing <#contributing>`__ + - `FAQ <#faq>`__ - `License <#license>`__ @@ -80,7 +80,7 @@ As a community-driven open source project, Tahoe-LAFS welcomes contributions of - `Patch reviews `__ -Before authoring or reviewing a patch, please familiarize yourself with the `Coding Standard `__ and the `Contributor Code of Conduct `__. +Before authoring or reviewing a patch, please familiarize yourself with the `Coding Standard `__ and the `Contributor Code of Conduct `__. πŸ€– Issues --------- @@ -103,7 +103,7 @@ Need more information? Please check our `FAQ page `__ for the terms of the GNU General Public License, version 2. See the file `COPYING.TGPPL `__ for the terms of the Transitive Grace Period Public Licence, version 1.0. +You may use this package under the GNU General Public License, version 2 or, at your option, any later version. You may use this package under the Transitive Grace Period Public Licence, version 1.0, or at your choice, any later version. (You may choose to use this package under the terms of either license, at your option.) See the file `COPYING.GPL `__ for the terms of the GNU General Public License, version 2. See the file `COPYING.TGPPL `__ for the terms of the Transitive Grace Period Public Licence, version 1.0. See `TGPPL.PDF `__ for why the TGPPL exists, graphically illustrated on three slides. diff --git a/newsfragments/3545.other b/newsfragments/3545.other index 4c5a0700b..fd8adc37b 100644 --- a/newsfragments/3545.other +++ b/newsfragments/3545.other @@ -1 +1 @@ -new README based on Viktoriia's changes after integrating the team's feedback. README document now is more focused on the developer's community and it provide more information about Tahoe-LAFS, why it's important and how someone can use it or start contributing to it. +The README, revised by Viktoriia with feedback from the team, is now more focused on the developer community and provides more information about Tahoe-LAFS, why it's important, and how someone can use it or start contributing to it. \ No newline at end of file From 07e4fe841dfb6993182f1a3fa18b318b0e99bd25 Mon Sep 17 00:00:00 2001 From: jbaeth Date: Wed, 9 Dec 2020 12:04:26 +0300 Subject: [PATCH 30/51] fix ToC order --- README.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 19344d7ae..98150ed27 100644 --- a/README.rst +++ b/README.rst @@ -56,6 +56,17 @@ For more detailed instructions, read `docs/INSTALL.rst `__ . Once tahoe --version works, see `docs/running.rst `__ to learn how to set up your first Tahoe-LAFS node. + +πŸ€– Issues +--------- + +Tahoe-LAFS uses the Trac instance to track `issues `__. Please email jean-paul plus tahoe-lafs at leastauthority dot com for an account. + +πŸ“‘ Documentation +---------------- + +You can find the full Tahoe-LAFS documentation at our `documentation site `__. + πŸ’¬ Community ------------ @@ -82,16 +93,6 @@ As a community-driven open source project, Tahoe-LAFS welcomes contributions of Before authoring or reviewing a patch, please familiarize yourself with the `Coding Standard `__ and the `Contributor Code of Conduct `__. -πŸ€– Issues ---------- - -Tahoe-LAFS uses the Trac instance to track `issues `__. Please email jean-paul plus tahoe-lafs at leastauthority dot com for an account. - -πŸ“‘ Documentation ----------------- - -You can find the full Tahoe-LAFS documentation at our `documentation site `__. - ❓ FAQ ------ From d916c725e69eccacdbda5bb8d832d9a36f139057 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 9 Dec 2020 10:32:26 -0500 Subject: [PATCH 31/51] Don't set up or query a stats gatherer in test_system --- src/allmydata/test/test_system.py | 68 ++++--------------------------- 1 file changed, 7 insertions(+), 61 deletions(-) diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py index 7a7fe117b..3f68ffc61 100644 --- a/src/allmydata/test/test_system.py +++ b/src/allmydata/test/test_system.py @@ -23,7 +23,6 @@ from allmydata.util import log, base32 from allmydata.util.encodingutil import quote_output, unicode_to_argv from allmydata.util.fileutil import abspath_expanduser_unicode from allmydata.util.consumer import MemoryConsumer, download_to_data -from allmydata.stats import StatsGathererService from allmydata.interfaces import IDirectoryNode, IFileNode, \ NoSuchChildError, NoSharesError from allmydata.monitor import Monitor @@ -667,9 +666,6 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): self.sparent = service.MultiService() self.sparent.startService() - self.stats_gatherer = None - self.stats_gatherer_furl = None - def tearDown(self): log.msg("shutting down SystemTest services") d = self.sparent.stopService() @@ -713,7 +709,7 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): return f.read().strip() @inlineCallbacks - def set_up_nodes(self, NUMCLIENTS=5, use_stats_gatherer=False): + def set_up_nodes(self, NUMCLIENTS=5): """ Create an introducer and ``NUMCLIENTS`` client nodes pointed at it. All of the nodes are running in this process. @@ -726,9 +722,6 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): :param int NUMCLIENTS: The number of client nodes to create. - :param bool use_stats_gatherer: If ``True`` then also create a stats - gatherer and configure the other nodes to use it. - :return: A ``Deferred`` that fires when the nodes have connected to each other. """ @@ -737,33 +730,7 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): self.introducer = yield self._create_introducer() self.add_service(self.introducer) self.introweb_url = self._get_introducer_web() - - if use_stats_gatherer: - yield self._set_up_stats_gatherer() yield self._set_up_client_nodes() - if use_stats_gatherer: - yield self._grab_stats() - - def _set_up_stats_gatherer(self): - statsdir = self.getdir("stats_gatherer") - fileutil.make_dirs(statsdir) - - location_hint, port_endpoint = self.port_assigner.assign(reactor) - fileutil.write(os.path.join(statsdir, "location"), location_hint) - fileutil.write(os.path.join(statsdir, "port"), port_endpoint) - self.stats_gatherer_svc = StatsGathererService(statsdir) - self.stats_gatherer = self.stats_gatherer_svc.stats_gatherer - self.stats_gatherer_svc.setServiceParent(self.sparent) - - d = fireEventually() - sgf = os.path.join(statsdir, 'stats_gatherer.furl') - def check_for_furl(): - return os.path.exists(sgf) - d.addCallback(lambda junk: self.poll(check_for_furl, timeout=30)) - def get_furl(junk): - self.stats_gatherer_furl = file(sgf, 'rb').read().strip() - d.addCallback(get_furl) - return d @inlineCallbacks def _set_up_client_nodes(self): @@ -839,9 +806,6 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): setnode("nickname", u"client %d \N{BLACK SMILING FACE}" % (which,)) - if self.stats_gatherer_furl: - setclient("stats_gatherer.furl", self.stats_gatherer_furl) - tub_location_hint, tub_port_endpoint = self.port_assigner.assign(reactor) setnode("tub.port", tub_port_endpoint) setnode("tub.location", tub_location_hint) @@ -872,10 +836,6 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): fileutil.write(os.path.join(basedir, 'tahoe.cfg'), config) return basedir - def _grab_stats(self): - d = self.stats_gatherer.poll() - return d - def bounce_client(self, num): c = self.clients[num] d = c.disownServiceParent() @@ -1303,25 +1263,11 @@ class SystemTest(SystemTestMixin, RunBinTahoeMixin, unittest.TestCase): d.addCallback(_upload_resumable) def _grab_stats(ignored): - # the StatsProvider doesn't normally publish a FURL: - # instead it passes a live reference to the StatsGatherer - # (if and when it connects). To exercise the remote stats - # interface, we manually publish client0's StatsProvider - # and use client1 to query it. - sp = self.clients[0].stats_provider - sp_furl = self.clients[0].tub.registerReference(sp) - d = self.clients[1].tub.getReference(sp_furl) - d.addCallback(lambda sp_rref: sp_rref.callRemote("get_stats")) - def _got_stats(stats): - #print("STATS") - #from pprint import pprint - #pprint(stats) - s = stats["stats"] - self.failUnlessEqual(s["storage_server.accepting_immutable_shares"], 1) - c = stats["counters"] - self.failUnless("storage_server.allocate" in c) - d.addCallback(_got_stats) - return d + stats = self.clients[0].stats_provider.get_stats() + s = stats["stats"] + self.failUnlessEqual(s["storage_server.accepting_immutable_shares"], 1) + c = stats["counters"] + self.failUnless("storage_server.allocate" in c) d.addCallback(_grab_stats) return d @@ -1629,7 +1575,7 @@ class SystemTest(SystemTestMixin, RunBinTahoeMixin, unittest.TestCase): def test_filesystem(self): self.basedir = "system/SystemTest/test_filesystem" self.data = LARGE_DATA - d = self.set_up_nodes(use_stats_gatherer=True) + d = self.set_up_nodes() def _new_happy_semantics(ign): for c in self.clients: c.encoding_params['happy'] = 1 From 3fd1b336b4f6a7c9ae18c06426cd29126d4aeb88 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 9 Dec 2020 10:32:49 -0500 Subject: [PATCH 32/51] Don't test stats gatherer support in the runner --- src/allmydata/test/test_runner.py | 42 ++----------------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/src/allmydata/test/test_runner.py b/src/allmydata/test/test_runner.py index 7d614d486..2ec871231 100644 --- a/src/allmydata/test/test_runner.py +++ b/src/allmydata/test/test_runner.py @@ -142,9 +142,8 @@ class BinTahoe(common_util.SignalMixin, unittest.TestCase, RunBinTahoeMixin): class CreateNode(unittest.TestCase): - # exercise "tahoe create-node", create-introducer, - # create-key-generator, and create-stats-gatherer, by calling the - # corresponding code as a subroutine. + # exercise "tahoe create-node", create-introducer, and + # create-key-generator by calling the corresponding code as a subroutine. def workdir(self, name): basedir = os.path.join("test_runner", "CreateNode", name) @@ -243,48 +242,11 @@ class CreateNode(unittest.TestCase): def test_introducer(self): self.do_create("introducer", "--hostname=127.0.0.1") - def test_stats_gatherer(self): - self.do_create("stats-gatherer", "--hostname=127.0.0.1") - def test_subcommands(self): # no arguments should trigger a command listing, via UsageError self.failUnlessRaises(usage.UsageError, parse_cli, ) - @inlineCallbacks - def test_stats_gatherer_good_args(self): - rc,out,err = yield run_cli("create-stats-gatherer", "--hostname=foo", - self.mktemp()) - self.assertEqual(rc, 0) - rc,out,err = yield run_cli("create-stats-gatherer", - "--location=tcp:foo:1234", - "--port=tcp:1234", self.mktemp()) - self.assertEqual(rc, 0) - - - def test_stats_gatherer_bad_args(self): - def _test(args): - argv = args.split() - self.assertRaises(usage.UsageError, parse_cli, *argv) - - # missing hostname/location/port - _test("create-stats-gatherer D") - - # missing port - _test("create-stats-gatherer --location=foo D") - - # missing location - _test("create-stats-gatherer --port=foo D") - - # can't provide both - _test("create-stats-gatherer --hostname=foo --port=foo D") - - # can't provide both - _test("create-stats-gatherer --hostname=foo --location=foo D") - - # can't provide all three - _test("create-stats-gatherer --hostname=foo --location=foo --port=foo D") - class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin, RunBinTahoeMixin): From d7ec5a19be053f8d869d3855cd027e173d245cc8 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 9 Dec 2020 10:34:16 -0500 Subject: [PATCH 33/51] Don't implement the stats gatherer or support configuring or talking to one --- src/allmydata/client.py | 8 +- src/allmydata/interfaces.py | 32 ---- src/allmydata/scripts/create_node.py | 1 - src/allmydata/scripts/run_common.py | 7 +- src/allmydata/scripts/runner.py | 5 +- src/allmydata/scripts/stats_gatherer.py | 103 ---------- src/allmydata/stats.py | 239 +----------------------- 7 files changed, 10 insertions(+), 385 deletions(-) delete mode 100644 src/allmydata/scripts/stats_gatherer.py diff --git a/src/allmydata/client.py b/src/allmydata/client.py index 55db7e690..ce5c570fc 100644 --- a/src/allmydata/client.py +++ b/src/allmydata/client.py @@ -3,7 +3,6 @@ from past.builtins import unicode import os, stat, time, weakref from base64 import urlsafe_b64encode from functools import partial - # On Python 2 this will be the backported package: from configparser import NoSectionError @@ -85,7 +84,6 @@ _client_config = configutil.ValidConfiguration( "shares.happy", "shares.needed", "shares.total", - "stats_gatherer.furl", "storage.plugins", ), "ftpd": ( @@ -678,11 +676,7 @@ class _Client(node.Node, pollmixin.PollMixin): self.init_web(webport) # strports string def init_stats_provider(self): - gatherer_furl = self.config.get_config("client", "stats_gatherer.furl", None) - if gatherer_furl: - # FURLs should be bytes: - gatherer_furl = gatherer_furl.encode("utf-8") - self.stats_provider = StatsProvider(self, gatherer_furl) + self.stats_provider = StatsProvider(self) self.stats_provider.setServiceParent(self) self.stats_provider.register_producer(self) diff --git a/src/allmydata/interfaces.py b/src/allmydata/interfaces.py index b9a757b08..bbeed2540 100644 --- a/src/allmydata/interfaces.py +++ b/src/allmydata/interfaces.py @@ -2931,38 +2931,6 @@ class RIHelper(RemoteInterface): return (UploadResults, ChoiceOf(RICHKUploadHelper, None)) -class RIStatsProvider(RemoteInterface): - __remote_name__ = native_str("RIStatsProvider.tahoe.allmydata.com") - """ - Provides access to statistics and monitoring information. - """ - - def get_stats(): - """ - returns a dictionary containing 'counters' and 'stats', each a - dictionary with string counter/stat name keys, and numeric or None values. - counters are monotonically increasing measures of work done, and - stats are instantaneous measures (potentially time averaged - internally) - """ - return DictOf(bytes, DictOf(bytes, ChoiceOf(float, int, long, None))) - - -class RIStatsGatherer(RemoteInterface): - __remote_name__ = native_str("RIStatsGatherer.tahoe.allmydata.com") - """ - Provides a monitoring service for centralised collection of stats - """ - - def provide(provider=RIStatsProvider, nickname=bytes): - """ - @param provider: a stats collector instance that should be polled - periodically by the gatherer to collect stats. - @param nickname: a name useful to identify the provided client - """ - return None - - class IStatsProducer(Interface): def get_stats(): """ diff --git a/src/allmydata/scripts/create_node.py b/src/allmydata/scripts/create_node.py index a4b2213ed..ac17cf445 100644 --- a/src/allmydata/scripts/create_node.py +++ b/src/allmydata/scripts/create_node.py @@ -318,7 +318,6 @@ def write_client_config(c, config): c.write("[client]\n") c.write("helper.furl =\n") - c.write("#stats_gatherer.furl =\n") c.write("\n") c.write("# Encoding parameters this client will use for newly-uploaded files\n") c.write("# This can be changed at any time: the encoding is saved in\n") diff --git a/src/allmydata/scripts/run_common.py b/src/allmydata/scripts/run_common.py index fa19c2076..a19eb40b4 100644 --- a/src/allmydata/scripts/run_common.py +++ b/src/allmydata/scripts/run_common.py @@ -47,8 +47,8 @@ def get_pid_from_pidfile(pidfile): def identify_node_type(basedir): """ - :return unicode: None or one of: 'client', 'introducer', - 'key-generator' or 'stats-gatherer' + :return unicode: None or one of: 'client', 'introducer', or + 'key-generator' """ tac = u'' try: @@ -59,7 +59,7 @@ def identify_node_type(basedir): except OSError: return None - for t in (u"client", u"introducer", u"key-generator", u"stats-gatherer"): + for t in (u"client", u"introducer", u"key-generator"): if t in tac: return t return None @@ -135,7 +135,6 @@ class DaemonizeTheRealService(Service, HookMixin): node_to_instance = { u"client": lambda: maybeDeferred(namedAny("allmydata.client.create_client"), self.basedir), u"introducer": lambda: maybeDeferred(namedAny("allmydata.introducer.server.create_introducer"), self.basedir), - u"stats-gatherer": lambda: maybeDeferred(namedAny("allmydata.stats.StatsGathererService"), read_config(self.basedir, None), self.basedir, verbose=True), u"key-generator": key_generator_removed, } diff --git a/src/allmydata/scripts/runner.py b/src/allmydata/scripts/runner.py index 3436a1b84..273a05af1 100644 --- a/src/allmydata/scripts/runner.py +++ b/src/allmydata/scripts/runner.py @@ -9,7 +9,7 @@ from twisted.internet import defer, task, threads from allmydata.scripts.common import get_default_nodedir from allmydata.scripts import debug, create_node, cli, \ - stats_gatherer, admin, tahoe_daemonize, tahoe_start, \ + admin, tahoe_daemonize, tahoe_start, \ tahoe_stop, tahoe_restart, tahoe_run, tahoe_invite from allmydata.util.encodingutil import quote_output, quote_local_unicode_path, get_io_encoding from allmydata.util.eliotutil import ( @@ -60,7 +60,6 @@ class Options(usage.Options): stderr = sys.stderr subCommands = ( create_node.subCommands - + stats_gatherer.subCommands + admin.subCommands + process_control_commands + debug.subCommands @@ -107,7 +106,7 @@ class Options(usage.Options): create_dispatch = {} -for module in (create_node, stats_gatherer): +for module in (create_node,): create_dispatch.update(module.dispatch) def parse_options(argv, config=None): diff --git a/src/allmydata/scripts/stats_gatherer.py b/src/allmydata/scripts/stats_gatherer.py deleted file mode 100644 index 26848a23c..000000000 --- a/src/allmydata/scripts/stats_gatherer.py +++ /dev/null @@ -1,103 +0,0 @@ -from __future__ import print_function - -import os - -# Python 2 compatibility -from future.utils import PY2 -if PY2: - from future.builtins import str # noqa: F401 - -from twisted.python import usage - -from allmydata.scripts.common import NoDefaultBasedirOptions -from allmydata.scripts.create_node import write_tac -from allmydata.util.assertutil import precondition -from allmydata.util.encodingutil import listdir_unicode, quote_output -from allmydata.util import fileutil, iputil - - -class CreateStatsGathererOptions(NoDefaultBasedirOptions): - subcommand_name = "create-stats-gatherer" - optParameters = [ - ("hostname", None, None, "Hostname of this machine, used to build location"), - ("location", None, None, "FURL connection hints, e.g. 'tcp:HOSTNAME:PORT'"), - ("port", None, None, "listening endpoint, e.g. 'tcp:PORT'"), - ] - def postOptions(self): - if self["hostname"] and (not self["location"]) and (not self["port"]): - pass - elif (not self["hostname"]) and self["location"] and self["port"]: - pass - else: - raise usage.UsageError("You must provide --hostname, or --location and --port.") - - description = """ - Create a "stats-gatherer" service, which is a standalone process that - collects and stores runtime statistics from many server nodes. This is a - tool for operations personnel to keep track of free disk space, server - load, and protocol activity, across a fleet of Tahoe storage servers. - - The "stats-gatherer" listens on a TCP port and publishes a Foolscap FURL - by writing it into a file named "stats_gatherer.furl". You must copy this - FURL into the servers' tahoe.cfg, as the [client] stats_gatherer.furl= - entry. Those servers will then establish a connection to the - stats-gatherer and publish their statistics on a periodic basis. The - gatherer writes a summary JSON file out to disk after each update. - - The stats-gatherer listens on a configurable port, and writes a - configurable hostname+port pair into the FURL that it publishes. There - are two configuration modes you can use. - - * In the first, you provide --hostname=, and the service chooses its own - TCP port number. If the host is named "example.org" and you provide - --hostname=example.org, the node will pick a port number (e.g. 12345) - and use location="tcp:example.org:12345" and port="tcp:12345". - - * In the second, you provide both --location= and --port=, and the - service will refrain from doing any allocation of its own. --location= - must be a Foolscap "FURL connection hint sequence", which is a - comma-separated list of "tcp:HOSTNAME:PORTNUM" strings. --port= must be - a Twisted server endpoint specification, which is generally - "tcp:PORTNUM". So, if your host is named "example.org" and you want to - use port 6789, you should provide --location=tcp:example.org:6789 and - --port=tcp:6789. You are responsible for making sure --location= and - --port= match each other. - """ - - -def create_stats_gatherer(config): - err = config.stderr - basedir = config['basedir'] - # This should always be called with an absolute Unicode basedir. - precondition(isinstance(basedir, str), basedir) - - if os.path.exists(basedir): - if listdir_unicode(basedir): - print("The base directory %s is not empty." % quote_output(basedir), file=err) - print("To avoid clobbering anything, I am going to quit now.", file=err) - print("Please use a different directory, or empty this one.", file=err) - return -1 - # we're willing to use an empty directory - else: - os.mkdir(basedir) - write_tac(basedir, "stats-gatherer") - if config["hostname"]: - portnum = iputil.allocate_tcp_port() - location = "tcp:%s:%d" % (config["hostname"], portnum) - port = "tcp:%d" % portnum - else: - location = config["location"] - port = config["port"] - fileutil.write(os.path.join(basedir, "location"), location+"\n") - fileutil.write(os.path.join(basedir, "port"), port+"\n") - return 0 - -subCommands = [ - ["create-stats-gatherer", None, CreateStatsGathererOptions, "Create a stats-gatherer service."], -] - -dispatch = { - "create-stats-gatherer": create_stats_gatherer, - } - - diff --git a/src/allmydata/stats.py b/src/allmydata/stats.py index f669b0861..18b22c30a 100644 --- a/src/allmydata/stats.py +++ b/src/allmydata/stats.py @@ -1,79 +1,19 @@ from __future__ import print_function -import json -import os -import pprint import time -from collections import deque # Python 2 compatibility from future.utils import PY2 if PY2: from future.builtins import str # noqa: F401 -from twisted.internet import reactor from twisted.application import service from twisted.application.internet import TimerService from zope.interface import implementer -from foolscap.api import eventually, DeadReferenceError, Referenceable, Tub +from foolscap.api import eventually from allmydata.util import log -from allmydata.util.encodingutil import quote_local_unicode_path -from allmydata.interfaces import RIStatsProvider, RIStatsGatherer, IStatsProducer - -@implementer(IStatsProducer) -class LoadMonitor(service.MultiService): - - loop_interval = 1 - num_samples = 60 - - def __init__(self, provider, warn_if_delay_exceeds=1): - service.MultiService.__init__(self) - self.provider = provider - self.warn_if_delay_exceeds = warn_if_delay_exceeds - self.started = False - self.last = None - self.stats = deque() - self.timer = None - - def startService(self): - if not self.started: - self.started = True - self.timer = reactor.callLater(self.loop_interval, self.loop) - service.MultiService.startService(self) - - def stopService(self): - self.started = False - if self.timer: - self.timer.cancel() - self.timer = None - return service.MultiService.stopService(self) - - def loop(self): - self.timer = None - if not self.started: - return - now = time.time() - if self.last is not None: - delay = now - self.last - self.loop_interval - if delay > self.warn_if_delay_exceeds: - log.msg(format='excessive reactor delay (%ss)', args=(delay,), - level=log.UNUSUAL) - self.stats.append(delay) - while len(self.stats) > self.num_samples: - self.stats.popleft() - - self.last = now - self.timer = reactor.callLater(self.loop_interval, self.loop) - - def get_stats(self): - if self.stats: - avg = sum(self.stats) / len(self.stats) - m_x = max(self.stats) - else: - avg = m_x = 0 - return { 'load_monitor.avg_load': avg, - 'load_monitor.max_load': m_x, } +from allmydata.interfaces import IStatsProducer @implementer(IStatsProducer) class CPUUsageMonitor(service.MultiService): @@ -128,37 +68,18 @@ class CPUUsageMonitor(service.MultiService): return s -@implementer(RIStatsProvider) -class StatsProvider(Referenceable, service.MultiService): +class StatsProvider(service.MultiService): - def __init__(self, node, gatherer_furl): + def __init__(self, node): service.MultiService.__init__(self) self.node = node - self.gatherer_furl = gatherer_furl # might be None self.counters = {} self.stats_producers = [] - - # only run the LoadMonitor (which submits a timer every second) if - # there is a gatherer who is going to be paying attention. Our stats - # are visible through HTTP even without a gatherer, so run the rest - # of the stats (including the once-per-minute CPUUsageMonitor) - if gatherer_furl: - self.load_monitor = LoadMonitor(self) - self.load_monitor.setServiceParent(self) - self.register_producer(self.load_monitor) - self.cpu_monitor = CPUUsageMonitor() self.cpu_monitor.setServiceParent(self) self.register_producer(self.cpu_monitor) - def startService(self): - if self.node and self.gatherer_furl: - nickname_utf8 = self.node.nickname.encode("utf-8") - self.node.tub.connectTo(self.gatherer_furl, - self._connected, nickname_utf8) - service.MultiService.startService(self) - def count(self, name, delta=1): if isinstance(name, str): name = name.encode("utf-8") @@ -175,155 +96,3 @@ class StatsProvider(Referenceable, service.MultiService): ret = { 'counters': self.counters, 'stats': stats } log.msg(format='get_stats() -> %(stats)s', stats=ret, level=log.NOISY) return ret - - def remote_get_stats(self): - # The remote API expects keys to be bytes: - def to_bytes(d): - result = {} - for (k, v) in d.items(): - if isinstance(k, str): - k = k.encode("utf-8") - result[k] = v - return result - - stats = self.get_stats() - return {b"counters": to_bytes(stats["counters"]), - b"stats": to_bytes(stats["stats"])} - - def _connected(self, gatherer, nickname): - gatherer.callRemoteOnly('provide', self, nickname or '') - - -@implementer(RIStatsGatherer) -class StatsGatherer(Referenceable, service.MultiService): - - poll_interval = 60 - - def __init__(self, basedir): - service.MultiService.__init__(self) - self.basedir = basedir - - self.clients = {} - self.nicknames = {} - - self.timer = TimerService(self.poll_interval, self.poll) - self.timer.setServiceParent(self) - - def get_tubid(self, rref): - return rref.getRemoteTubID() - - def remote_provide(self, provider, nickname): - tubid = self.get_tubid(provider) - if tubid == '': - print("WARNING: failed to get tubid for %s (%s)" % (provider, nickname)) - # don't add to clients to poll (polluting data) don't care about disconnect - return - self.clients[tubid] = provider - self.nicknames[tubid] = nickname - - def poll(self): - for tubid,client in self.clients.items(): - nickname = self.nicknames.get(tubid) - d = client.callRemote('get_stats') - d.addCallbacks(self.got_stats, self.lost_client, - callbackArgs=(tubid, nickname), - errbackArgs=(tubid,)) - d.addErrback(self.log_client_error, tubid) - - def lost_client(self, f, tubid): - # this is called lazily, when a get_stats request fails - del self.clients[tubid] - del self.nicknames[tubid] - f.trap(DeadReferenceError) - - def log_client_error(self, f, tubid): - log.msg("StatsGatherer: error in get_stats(), peerid=%s" % tubid, - level=log.UNUSUAL, failure=f) - - def got_stats(self, stats, tubid, nickname): - raise NotImplementedError() - -class StdOutStatsGatherer(StatsGatherer): - verbose = True - def remote_provide(self, provider, nickname): - tubid = self.get_tubid(provider) - if self.verbose: - print('connect "%s" [%s]' % (nickname, tubid)) - provider.notifyOnDisconnect(self.announce_lost_client, tubid) - StatsGatherer.remote_provide(self, provider, nickname) - - def announce_lost_client(self, tubid): - print('disconnect "%s" [%s]' % (self.nicknames[tubid], tubid)) - - def got_stats(self, stats, tubid, nickname): - print('"%s" [%s]:' % (nickname, tubid)) - pprint.pprint(stats) - -class JSONStatsGatherer(StdOutStatsGatherer): - # inherit from StdOutStatsGatherer for connect/disconnect notifications - - def __init__(self, basedir=u".", verbose=True): - self.verbose = verbose - StatsGatherer.__init__(self, basedir) - self.jsonfile = os.path.join(basedir, "stats.json") - - if os.path.exists(self.jsonfile): - try: - with open(self.jsonfile, 'rb') as f: - self.gathered_stats = json.load(f) - except Exception: - print("Error while attempting to load stats file %s.\n" - "You may need to restore this file from a backup," - " or delete it if no backup is available.\n" % - quote_local_unicode_path(self.jsonfile)) - raise - else: - self.gathered_stats = {} - - def got_stats(self, stats, tubid, nickname): - s = self.gathered_stats.setdefault(tubid, {}) - s['timestamp'] = time.time() - s['nickname'] = nickname - s['stats'] = stats - self.dump_json() - - def dump_json(self): - tmp = "%s.tmp" % (self.jsonfile,) - with open(tmp, 'wb') as f: - json.dump(self.gathered_stats, f) - if os.path.exists(self.jsonfile): - os.unlink(self.jsonfile) - os.rename(tmp, self.jsonfile) - -class StatsGathererService(service.MultiService): - furl_file = "stats_gatherer.furl" - - def __init__(self, basedir=".", verbose=False): - service.MultiService.__init__(self) - self.basedir = basedir - self.tub = Tub(certFile=os.path.join(self.basedir, - "stats_gatherer.pem")) - self.tub.setServiceParent(self) - self.tub.setOption("logLocalFailures", True) - self.tub.setOption("logRemoteFailures", True) - self.tub.setOption("expose-remote-exception-types", False) - - self.stats_gatherer = JSONStatsGatherer(self.basedir, verbose) - self.stats_gatherer.setServiceParent(self) - - try: - with open(os.path.join(self.basedir, "location")) as f: - location = f.read().strip() - except EnvironmentError: - raise ValueError("Unable to find 'location' in BASEDIR, please rebuild your stats-gatherer") - try: - with open(os.path.join(self.basedir, "port")) as f: - port = f.read().strip() - except EnvironmentError: - raise ValueError("Unable to find 'port' in BASEDIR, please rebuild your stats-gatherer") - - self.tub.listenOn(port) - self.tub.setLocation(location) - ff = os.path.join(self.basedir, self.furl_file) - self.gatherer_furl = self.tub.registerReference(self.stats_gatherer, - furlFile=ff) From a4e1451fa3e7b982eba71bdceb18c346495d6684 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 9 Dec 2020 10:34:37 -0500 Subject: [PATCH 34/51] Don't document the stats gatherer --- docs/configuration.rst | 5 --- docs/man/man1/tahoe.1 | 3 -- docs/stats.rst | 76 +----------------------------------------- 3 files changed, 1 insertion(+), 83 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 0aab9b395..5bd89eec9 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -911,11 +911,6 @@ This section describes these other files. This file is used to construct an introducer, and is created by the "``tahoe create-introducer``" command. -``tahoe-stats-gatherer.tac`` - - This file is used to construct a statistics gatherer, and is created by the - "``tahoe create-stats-gatherer``" command. - ``private/control.furl`` This file contains a FURL that provides access to a control port on the diff --git a/docs/man/man1/tahoe.1 b/docs/man/man1/tahoe.1 index 23162af63..113f6a311 100644 --- a/docs/man/man1/tahoe.1 +++ b/docs/man/man1/tahoe.1 @@ -45,9 +45,6 @@ Create a client node (with storage initially disabled). .TP .B \f[B]create-introducer\f[] Create an introducer node. -.TP -.B \f[B]create-stats-gatherer\f[] -Create a stats-gatherer service. .SS OPTIONS .TP .B \f[B]-C,\ --basedir=\f[] diff --git a/docs/stats.rst b/docs/stats.rst index 8fbc8647a..200523d07 100644 --- a/docs/stats.rst +++ b/docs/stats.rst @@ -6,8 +6,7 @@ Tahoe Statistics 1. `Overview`_ 2. `Statistics Categories`_ -3. `Running a Tahoe Stats-Gatherer Service`_ -4. `Using Munin To Graph Stats Values`_ +3. `Using Munin To Graph Stats Values`_ Overview ======== @@ -257,79 +256,6 @@ The currently available stats (as of release 1.6.0 or so) are described here: maximum "load" value over the last minute -Running a Tahoe Stats-Gatherer Service -====================================== - -The "stats-gatherer" is a simple daemon that periodically collects stats from -several tahoe nodes. It could be useful, e.g., in a production environment, -where you want to monitor dozens of storage servers from a central management -host. It merely gatherers statistics from many nodes into a single place: it -does not do any actual analysis. - -The stats gatherer listens on a network port using the same Foolscap_ -connection library that Tahoe clients use to connect to storage servers. -Tahoe nodes can be configured to connect to the stats gatherer and publish -their stats on a periodic basis. (In fact, what happens is that nodes connect -to the gatherer and offer it a second FURL which points back to the node's -"stats port", which the gatherer then uses to pull stats on a periodic basis. -The initial connection is flipped to allow the nodes to live behind NAT -boxes, as long as the stats-gatherer has a reachable IP address.) - -.. _Foolscap: https://foolscap.lothar.com/trac - -The stats-gatherer is created in the same fashion as regular tahoe client -nodes and introducer nodes. Choose a base directory for the gatherer to live -in (but do not create the directory). Choose the hostname that should be -advertised in the gatherer's FURL. Then run: - -:: - - tahoe create-stats-gatherer --hostname=HOSTNAME $BASEDIR - -and start it with "tahoe start $BASEDIR". Once running, the gatherer will -write a FURL into $BASEDIR/stats_gatherer.furl . - -To configure a Tahoe client/server node to contact the stats gatherer, copy -this FURL into the node's tahoe.cfg file, in a section named "[client]", -under a key named "stats_gatherer.furl", like so: - -:: - - [client] - stats_gatherer.furl = pb://qbo4ktl667zmtiuou6lwbjryli2brv6t@HOSTNAME:PORTNUM/wxycb4kaexzskubjnauxeoptympyf45y - -or simply copy the stats_gatherer.furl file into the node's base directory -(next to the tahoe.cfg file): it will be interpreted in the same way. - -When the gatherer is created, it will allocate a random unused TCP port, so -it should not conflict with anything else that you have running on that host -at that time. To explicitly control which port it uses, run the creation -command with ``--location=`` and ``--port=`` instead of ``--hostname=``. If -you use a hostname of ``example.org`` and a port number of ``1234``, then -run:: - - tahoe create-stats-gatherer --location=tcp:example.org:1234 --port=tcp:1234 - -``--location=`` is a Foolscap FURL hints string (so it can be a -comma-separated list of connection hints), and ``--port=`` is a Twisted -"server endpoint specification string", as described in :doc:`configuration`. - -Once running, the stats gatherer will create a standard JSON file in -``$BASEDIR/stats.json``. Once a minute, the gatherer will pull stats -information from every connected node and write them into the file. The file -will contain a dictionary, in which node identifiers (known as "tubid" -strings) are the keys, and the values are a dict with 'timestamp', -'nickname', and 'stats' keys. d[tubid][stats] will contain the stats -dictionary as made available at http://localhost:3456/statistics?t=json . The -file will only contain the most recent update from each node. - -Other tools can be built to examine these stats and render them into -something useful. For example, a tool could sum the -"storage_server.disk_avail' values from all servers to compute a -total-disk-available number for the entire grid (however, the "disk watcher" -daemon, in misc/operations_helpers/spacetime/, is better suited for this -specific task). - Using Munin To Graph Stats Values ================================= From 4f01c8f33e1991b344b5a003c37a5663800ff923 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 9 Dec 2020 10:35:43 -0500 Subject: [PATCH 35/51] news fragment --- newsfragments/3549.removed | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3549.removed diff --git a/newsfragments/3549.removed b/newsfragments/3549.removed new file mode 100644 index 000000000..bf152cfb0 --- /dev/null +++ b/newsfragments/3549.removed @@ -0,0 +1 @@ +The stats gatherer has been removed. The ``[client]stats_gatherer.furl`` configuration item in ``tahoe.cfg`` is no longer allowed. From 9412cf70c23ffe3c14ece1c2cf39a406c2862498 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 9 Dec 2020 10:36:17 -0500 Subject: [PATCH 36/51] remove unused helper --- src/allmydata/test/test_system.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py index 3f68ffc61..53b670886 100644 --- a/src/allmydata/test/test_system.py +++ b/src/allmydata/test/test_system.py @@ -800,7 +800,6 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): value = value.encode("utf-8") config.setdefault(section, {})[feature] = value - setclient = partial(setconf, config, which, "client") setnode = partial(setconf, config, which, "node") sethelper = partial(setconf, config, which, "helper") From 9a27254afa89c1182cc8d27560e1edc2640b72fa Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 9 Dec 2020 10:39:03 -0500 Subject: [PATCH 37/51] unused import --- src/allmydata/scripts/run_common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/allmydata/scripts/run_common.py b/src/allmydata/scripts/run_common.py index a19eb40b4..71934414d 100644 --- a/src/allmydata/scripts/run_common.py +++ b/src/allmydata/scripts/run_common.py @@ -10,7 +10,6 @@ from twisted.application.service import Service from allmydata.scripts.default_nodedir import _default_nodedir from allmydata.util import fileutil -from allmydata.node import read_config from allmydata.util.encodingutil import listdir_unicode, quote_local_unicode_path from allmydata.util.configutil import UnknownConfigError from allmydata.util.deferredutil import HookMixin From baed5fd7348110fa6d35ba5ecebd1468c1c2664a Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 9 Dec 2020 13:22:51 -0500 Subject: [PATCH 38/51] Port to Python 3. --- src/allmydata/immutable/checker.py | 12 ++++++++++++ src/allmydata/util/_python3.py | 1 + 2 files changed, 13 insertions(+) diff --git a/src/allmydata/immutable/checker.py b/src/allmydata/immutable/checker.py index 2bed90e1c..9636b9a2f 100644 --- a/src/allmydata/immutable/checker.py +++ b/src/allmydata/immutable/checker.py @@ -1,3 +1,15 @@ +""" +Ported to Python 3. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from future.utils import PY2 +if PY2: + from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 + from zope.interface import implementer from twisted.internet import defer from foolscap.api import DeadReferenceError, RemoteException diff --git a/src/allmydata/util/_python3.py b/src/allmydata/util/_python3.py index 9763c35d7..540e52871 100644 --- a/src/allmydata/util/_python3.py +++ b/src/allmydata/util/_python3.py @@ -35,6 +35,7 @@ PORTED_MODULES = [ "allmydata.crypto.rsa", "allmydata.crypto.util", "allmydata.hashtree", + "allmydata.immutable.checker", "allmydata.immutable.downloader", "allmydata.immutable.downloader.common", "allmydata.immutable.downloader.fetcher", From 2edd029261b4847d2f3fc56d88a2bac4a0b0088e Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 9 Dec 2020 13:23:11 -0500 Subject: [PATCH 39/51] News file. --- newsfragments/3551.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3551.minor diff --git a/newsfragments/3551.minor b/newsfragments/3551.minor new file mode 100644 index 000000000..e69de29bb From eb55c10eea459d0ef92efa839594c3e516b1f502 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 9 Dec 2020 13:28:16 -0500 Subject: [PATCH 40/51] Tests pass on Python 3. --- src/allmydata/test/common.py | 6 ++++-- src/allmydata/test/test_repairer.py | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index f93272540..48415eabb 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -11,6 +11,8 @@ __all__ = [ "skipIf", ] +from past.builtins import chr as byteschr + import os, random, struct import six import tempfile @@ -1057,7 +1059,7 @@ def _corrupt_share_data_last_byte(data, debug=False): sharedatasize = struct.unpack(">Q", data[0x0c+0x08:0x0c+0x0c+8])[0] offset = 0x0c+0x44+sharedatasize-1 - newdata = data[:offset] + chr(ord(data[offset])^0xFF) + data[offset+1:] + newdata = data[:offset] + byteschr(ord(data[offset:offset+1])^0xFF) + data[offset+1:] if debug: log.msg("testing: flipping all bits of byte at offset %d: %r, newdata: %r" % (offset, data[offset], newdata[offset])) return newdata @@ -1085,7 +1087,7 @@ def _corrupt_crypttext_hash_tree_byte_x221(data, debug=False): assert sharevernum in (1, 2), "This test is designed to corrupt immutable shares of v1 or v2 in specific ways." if debug: log.msg("original data: %r" % (data,)) - return data[:0x0c+0x221] + chr(ord(data[0x0c+0x221])^0x02) + data[0x0c+0x2210+1:] + return data[:0x0c+0x221] + byteschr(ord(data[0x0c+0x221:0x0c+0x221+1])^0x02) + data[0x0c+0x2210+1:] def _corrupt_block_hashes(data, debug=False): """Scramble the file data -- the field containing the block hash tree diff --git a/src/allmydata/test/test_repairer.py b/src/allmydata/test/test_repairer.py index 4fdffe70e..394e8fe2a 100644 --- a/src/allmydata/test/test_repairer.py +++ b/src/allmydata/test/test_repairer.py @@ -62,7 +62,7 @@ class RepairTestMixin(object): c0 = self.g.clients[0] c1 = self.g.clients[1] c0.encoding_params['max_segment_size'] = 12 - d = c0.upload(upload.Data(common.TEST_DATA, convergence="")) + d = c0.upload(upload.Data(common.TEST_DATA, convergence=b"")) def _stash_uri(ur): self.uri = ur.get_uri() self.c0_filenode = c0.create_node_from_uri(ur.get_uri()) @@ -527,7 +527,7 @@ class Repairer(GridTestMixin, unittest.TestCase, RepairTestMixin, # distributing the shares widely enough to satisfy the default # happiness setting. def _delete_some_servers(ignored): - for i in xrange(7): + for i in range(7): self.g.remove_server(self.g.servers_by_number[i].my_nodeid) assert len(self.g.servers_by_number) == 3 @@ -679,10 +679,10 @@ class Repairer(GridTestMixin, unittest.TestCase, RepairTestMixin, self.basedir = "repairer/Repairer/test_tiny_reads" self.set_up_grid() c0 = self.g.clients[0] - DATA = "a"*135 + DATA = b"a"*135 c0.encoding_params['k'] = 22 c0.encoding_params['n'] = 66 - d = c0.upload(upload.Data(DATA, convergence="")) + d = c0.upload(upload.Data(DATA, convergence=b"")) def _then(ur): self.uri = ur.get_uri() self.delete_shares_numbered(self.uri, [0]) From 63ff67a7be050fd6c2762102e3585fe0cc4ce9f4 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 9 Dec 2020 13:33:01 -0500 Subject: [PATCH 41/51] Ported to Python 3. --- src/allmydata/immutable/repairer.py | 12 ++++++++++++ src/allmydata/test/test_repairer.py | 18 ++++++++++++++---- src/allmydata/util/_python3.py | 2 ++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/allmydata/immutable/repairer.py b/src/allmydata/immutable/repairer.py index 1d3782d10..bccd8453d 100644 --- a/src/allmydata/immutable/repairer.py +++ b/src/allmydata/immutable/repairer.py @@ -1,3 +1,15 @@ +""" +Ported to Python 3. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from future.utils import PY2 +if PY2: + from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 + from zope.interface import implementer from twisted.internet import defer from allmydata.storage.server import si_b2a diff --git a/src/allmydata/test/test_repairer.py b/src/allmydata/test/test_repairer.py index 394e8fe2a..63a54a505 100644 --- a/src/allmydata/test/test_repairer.py +++ b/src/allmydata/test/test_repairer.py @@ -1,5 +1,15 @@ # -*- coding: utf-8 -*- +""" +Ported to Python 3. +""" from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + +from future.utils import PY2 +if PY2: + from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 from allmydata.test import common from allmydata.monitor import Monitor @@ -464,7 +474,7 @@ class Repairer(GridTestMixin, unittest.TestCase, RepairTestMixin, # previously-deleted share #2. d.addCallback(lambda ignored: - self.delete_shares_numbered(self.uri, range(3, 10+1))) + self.delete_shares_numbered(self.uri, list(range(3, 10+1)))) d.addCallback(lambda ignored: download_to_data(self.c1_filenode)) d.addCallback(lambda newdata: self.failUnlessEqual(newdata, common.TEST_DATA)) @@ -476,7 +486,7 @@ class Repairer(GridTestMixin, unittest.TestCase, RepairTestMixin, self.set_up_grid(num_clients=2) d = self.upload_and_stash() d.addCallback(lambda ignored: - self.delete_shares_numbered(self.uri, range(7))) + self.delete_shares_numbered(self.uri, list(range(7)))) d.addCallback(lambda ignored: self._stash_counts()) d.addCallback(lambda ignored: self.c0_filenode.check_and_repair(Monitor(), @@ -509,7 +519,7 @@ class Repairer(GridTestMixin, unittest.TestCase, RepairTestMixin, # previously-deleted share #2. d.addCallback(lambda ignored: - self.delete_shares_numbered(self.uri, range(3, 10+1))) + self.delete_shares_numbered(self.uri, list(range(3, 10+1)))) d.addCallback(lambda ignored: download_to_data(self.c1_filenode)) d.addCallback(lambda newdata: self.failUnlessEqual(newdata, common.TEST_DATA)) @@ -640,7 +650,7 @@ class Repairer(GridTestMixin, unittest.TestCase, RepairTestMixin, # downloading and has the right contents. This can't work # unless it has already repaired the previously-corrupted share. def _then_delete_7_and_try_a_download(unused=None): - shnums = range(10) + shnums = list(range(10)) shnums.remove(shnum) random.shuffle(shnums) for sharenum in shnums[:7]: diff --git a/src/allmydata/util/_python3.py b/src/allmydata/util/_python3.py index 540e52871..4d1d4356a 100644 --- a/src/allmydata/util/_python3.py +++ b/src/allmydata/util/_python3.py @@ -50,6 +50,7 @@ PORTED_MODULES = [ "allmydata.immutable.layout", "allmydata.immutable.literal", "allmydata.immutable.offloaded", + "allmydata.immutable.repairer", "allmydata.immutable.upload", "allmydata.interfaces", "allmydata.introducer.client", @@ -155,6 +156,7 @@ PORTED_TEST_MODULES = [ "allmydata.test.test_observer", "allmydata.test.test_pipeline", "allmydata.test.test_python3", + "allmydata.test.test_repairer", "allmydata.test.test_spans", "allmydata.test.test_statistics", "allmydata.test.test_storage", From c39f7721af8a4359c49efdb1357331a4ec45e8f2 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 10 Dec 2020 07:03:24 -0500 Subject: [PATCH 42/51] run_cli_bytes docstring --- src/allmydata/test/common_util.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/allmydata/test/common_util.py b/src/allmydata/test/common_util.py index c08dc03d5..2a8aad0e2 100644 --- a/src/allmydata/test/common_util.py +++ b/src/allmydata/test/common_util.py @@ -49,6 +49,27 @@ def _getvalue(io): def run_cli_bytes(verb, *args, **kwargs): + """ + Run a Tahoe-LAFS CLI command specified as bytes. + + Most code should prefer ``run_cli_unicode`` which deals with all the + necessary encoding considerations. + + :param bytes verb: The command to run. For example, ``b"create-node"``. + + :param [bytes] args: The arguments to pass to the command. For example, + ``(b"--hostname=localhost",)``. + + :param [bytes] nodeargs: Extra arguments to pass to the Tahoe executable + before ``verb``. + + :param bytes stdin: Text to pass to the command via stdin. + + :param NoneType|str encoding: The name of an encoding which stdout and + stderr will be configured to use. ``None`` means stdout and stderr + will accept bytes and unicode and use the default system encoding for + translating between them. + """ nodeargs = kwargs.pop("nodeargs", []) encoding = kwargs.pop("encoding", None) precondition( From 4bb28cadcb7c2963aa1a84f78533d5e2dc80243e Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 10 Dec 2020 07:04:28 -0500 Subject: [PATCH 43/51] motivate its existence a bit more --- src/allmydata/test/common_util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/allmydata/test/common_util.py b/src/allmydata/test/common_util.py index 2a8aad0e2..341d383c1 100644 --- a/src/allmydata/test/common_util.py +++ b/src/allmydata/test/common_util.py @@ -53,7 +53,9 @@ def run_cli_bytes(verb, *args, **kwargs): Run a Tahoe-LAFS CLI command specified as bytes. Most code should prefer ``run_cli_unicode`` which deals with all the - necessary encoding considerations. + necessary encoding considerations. This helper still exists so that novel + misconfigurations can be explicitly tested (for example, receiving UTF-8 + bytes when the system encoding claims to be ASCII). :param bytes verb: The command to run. For example, ``b"create-node"``. From 2f532257650fcf651c7220a95811bc49d97eb3a3 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 10 Dec 2020 07:06:01 -0500 Subject: [PATCH 44/51] better helper name --- src/allmydata/test/cli/test_alias.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/allmydata/test/cli/test_alias.py b/src/allmydata/test/cli/test_alias.py index 635ed0aba..72b634608 100644 --- a/src/allmydata/test/cli/test_alias.py +++ b/src/allmydata/test/cli/test_alias.py @@ -13,9 +13,9 @@ from allmydata.util import encodingutil class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): @inlineCallbacks - def _test_list(self, alias, encoding): + def _check_create_alias(self, alias, encoding): """ - Assert that ``tahoe create-alias`` can be used to create an alias named + Verify that ``tahoe create-alias`` can be used to create an alias named ``alias`` when argv is encoded using ``encoding``. :param unicode alias: The alias to try to create. @@ -82,7 +82,7 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): An alias composed of all ASCII-encodeable code points can be created when stdio aren't clearly marked with an encoding. """ - return self._test_list( + return self._check_create_alias( u"tahoe", encoding=None, ) @@ -93,7 +93,7 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): An alias composed of all ASCII-encodeable code points can be created when the active encoding is ASCII. """ - return self._test_list( + return self._check_create_alias( u"tahoe", encoding="ascii", ) @@ -109,7 +109,7 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): with a UTF-8-incompatible encoding helps flush out unintentional UTF-8 assumptions. """ - return self._test_list( + return self._check_create_alias( u"taho\N{LATIN SMALL LETTER E WITH ACUTE}", encoding="latin-1", ) @@ -120,7 +120,7 @@ class ListAlias(GridTestMixin, CLITestMixin, unittest.TestCase): An alias composed of all UTF-8-encodeable code points can be created when the active encoding is UTF-8. """ - return self._test_list( + return self._check_create_alias( u"tahoe\N{SNOWMAN}", encoding="utf-8", ) From d0c22a529ee1cf0734f131ecda4180ad8aa2c9a8 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 10 Dec 2020 07:16:00 -0500 Subject: [PATCH 45/51] json.dumps output should always be ascii --- src/allmydata/scripts/tahoe_add_alias.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/scripts/tahoe_add_alias.py b/src/allmydata/scripts/tahoe_add_alias.py index 442fecf71..44d08fb21 100644 --- a/src/allmydata/scripts/tahoe_add_alias.py +++ b/src/allmydata/scripts/tahoe_add_alias.py @@ -167,7 +167,7 @@ def list_aliases(options): data = _get_alias_details(options['node-directory']) if options['json']: - output = _escape_format(json.dumps(data, indent=4).decode("utf-8")) + output = _escape_format(json.dumps(data, indent=4).decode("ascii")) else: def dircap(details): return ( From 066e98874be50343ab2bbb03760d0d7887c88333 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 10 Dec 2020 07:17:24 -0500 Subject: [PATCH 46/51] Point at do_cli_unicode here too --- src/allmydata/test/cli/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/allmydata/test/cli/common.py b/src/allmydata/test/cli/common.py index d90b6a39f..bf175de44 100644 --- a/src/allmydata/test/cli/common.py +++ b/src/allmydata/test/cli/common.py @@ -39,6 +39,8 @@ class CLITestMixin(ReallyEqualMixin): """ Like ``do_cli_unicode`` but work with ``bytes`` everywhere instead of ``unicode``. + + Where possible, prefer ``do_cli_unicode``. """ # client_num is used to execute client CLI commands on a specific # client. From 6f80862ec582eb0c66ebbb99c9b3486a9b4abf58 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 10 Dec 2020 07:19:27 -0500 Subject: [PATCH 47/51] Slightly clean up formatting implementation --- src/allmydata/scripts/tahoe_add_alias.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/allmydata/scripts/tahoe_add_alias.py b/src/allmydata/scripts/tahoe_add_alias.py index 44d08fb21..6f931556d 100644 --- a/src/allmydata/scripts/tahoe_add_alias.py +++ b/src/allmydata/scripts/tahoe_add_alias.py @@ -176,10 +176,13 @@ def list_aliases(options): else details['readwrite'] ).decode("utf-8") + def format_dircap(name, details): + return fmt % (name, dircap(details)) + max_width = max([len(quote_output(name)) for name in data.keys()] + [0]) fmt = "%" + str(max_width) + "s: %s" output = "\n".join(list( - fmt % (name, dircap(details)) + format_dircap(name, details) for name, details in data.items() )) From 33b6fe59279069602b3bf6ade5146aef19bb54b7 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 10 Dec 2020 18:20:30 -0500 Subject: [PATCH 48/51] Remove more stats gatherer references --- docs/configuration.rst | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 5bd89eec9..6f920d09a 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -75,7 +75,7 @@ The item descriptions below use the following types: Node Types ========== -A node can be a client/server, an introducer, or a statistics gatherer. +A node can be a client/server or an introducer. Client/server nodes provide one or more of the following services: @@ -593,11 +593,6 @@ Client Configuration If provided, the node will attempt to connect to and use the given helper for uploads. See :doc:`helper` for details. -``stats_gatherer.furl = (FURL string, optional)`` - - If provided, the node will connect to the given stats gatherer and - provide it with operational statistics. - ``shares.needed = (int, optional) aka "k", default 3`` ``shares.total = (int, optional) aka "N", N >= k, default 10`` From 8afb4cba7c57c5b195a0af93565da61ce8265e9a Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 10 Dec 2020 18:22:06 -0500 Subject: [PATCH 49/51] Make a recommendation --- newsfragments/3549.removed | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/3549.removed b/newsfragments/3549.removed index bf152cfb0..53c7a7de1 100644 --- a/newsfragments/3549.removed +++ b/newsfragments/3549.removed @@ -1 +1 @@ -The stats gatherer has been removed. The ``[client]stats_gatherer.furl`` configuration item in ``tahoe.cfg`` is no longer allowed. +The stats gatherer, broken since at least Tahoe-LAFS 1.13.0, has been removed. The ``[client]stats_gatherer.furl`` configuration item in ``tahoe.cfg`` is no longer allowed. The Tahoe-LAFS project recommends using a third-party metrics aggregation tool instead. From 83167cfe647eecfc694bdcff99abac370b2de09c Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 10 Dec 2020 18:22:45 -0500 Subject: [PATCH 50/51] load_monitor is also gone --- docs/stats.rst | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/docs/stats.rst b/docs/stats.rst index 200523d07..50642d816 100644 --- a/docs/stats.rst +++ b/docs/stats.rst @@ -242,19 +242,6 @@ The currently available stats (as of release 1.6.0 or so) are described here: the process was started. Ticket #472 indicates that .total may sometimes be negative due to wraparound of the kernel's counter. -**stats.load_monitor.\*** - - When enabled, the "load monitor" continually schedules a one-second - callback, and measures how late the response is. This estimates system load - (if the system is idle, the response should be on time). This is only - enabled if a stats-gatherer is configured. - - avg_load - average "load" value (seconds late) over the last minute - - max_load - maximum "load" value over the last minute - Using Munin To Graph Stats Values ================================= From e8e928aced24b52962060fff937685b757de5a3a Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 10 Dec 2020 18:24:00 -0500 Subject: [PATCH 51/51] Remove the web view onto the removed metrics --- src/allmydata/web/statistics.xhtml | 2 -- src/allmydata/web/status.py | 8 -------- 2 files changed, 10 deletions(-) diff --git a/src/allmydata/web/statistics.xhtml b/src/allmydata/web/statistics.xhtml index 42376079d..2cc7e2b5a 100644 --- a/src/allmydata/web/statistics.xhtml +++ b/src/allmydata/web/statistics.xhtml @@ -12,8 +12,6 @@

General

    -
  • Load Average:
  • -
  • Peak Load:
  • Files Uploaded (immutable):
  • Files Downloaded (immutable):
  • Files Published (mutable):
  • diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 7f6020a99..ec55b73eb 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1565,14 +1565,6 @@ class StatisticsElement(Element): # Note that `counters` can be empty. self._stats = provider.get_stats() - @renderer - def load_average(self, req, tag): - return tag(str(self._stats["stats"].get("load_monitor.avg_load"))) - - @renderer - def peak_load(self, req, tag): - return tag(str(self._stats["stats"].get("load_monitor.max_load"))) - @renderer def uploads(self, req, tag): files = self._stats["counters"].get("uploader.files_uploaded", 0)