mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-04-09 11:51:21 +00:00
Merge branch 'master' into 3636.doc-toc-reorg
This commit is contained in:
commit
6cac282817
@ -1,7 +1,7 @@
|
||||
ARG TAG
|
||||
FROM debian:${TAG}
|
||||
ARG PYTHON_VERSION
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV WHEELHOUSE_PATH /tmp/wheelhouse
|
||||
ENV VIRTUALENV_PATH /tmp/venv
|
||||
# This will get updated by the CircleCI checkout step.
|
||||
@ -18,15 +18,11 @@ RUN apt-get --quiet update && \
|
||||
libffi-dev \
|
||||
libssl-dev \
|
||||
libyaml-dev \
|
||||
virtualenv
|
||||
virtualenv \
|
||||
tor
|
||||
|
||||
# Get the project source. This is better than it seems. CircleCI will
|
||||
# *update* this checkout on each job run, saving us more time per-job.
|
||||
COPY . ${BUILD_SRC_ROOT}
|
||||
|
||||
RUN "${BUILD_SRC_ROOT}"/.circleci/prepare-image.sh "${WHEELHOUSE_PATH}" "${VIRTUALENV_PATH}" "${BUILD_SRC_ROOT}" "python${PYTHON_VERSION}"
|
||||
|
||||
# Only the integration tests currently need this but it doesn't hurt to always
|
||||
# have it present and it's simpler than building a whole extra image just for
|
||||
# the integration tests.
|
||||
RUN ${BUILD_SRC_ROOT}/integration/install-tor.sh
|
||||
|
@ -1,5 +1,5 @@
|
||||
ARG TAG
|
||||
FROM centos:${TAG}
|
||||
FROM oraclelinux:${TAG}
|
||||
ARG PYTHON_VERSION
|
||||
|
||||
ENV WHEELHOUSE_PATH /tmp/wheelhouse
|
||||
@ -13,7 +13,6 @@ RUN yum install --assumeyes \
|
||||
sudo \
|
||||
make automake gcc gcc-c++ \
|
||||
python${PYTHON_VERSION} \
|
||||
python${PYTHON_VERSION}-devel \
|
||||
libffi-devel \
|
||||
openssl-devel \
|
||||
libyaml \
|
@ -1,7 +1,7 @@
|
||||
ARG TAG
|
||||
FROM ubuntu:${TAG}
|
||||
ARG PYTHON_VERSION
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV WHEELHOUSE_PATH /tmp/wheelhouse
|
||||
ENV VIRTUALENV_PATH /tmp/venv
|
||||
# This will get updated by the CircleCI checkout step.
|
||||
|
78
.circleci/circleci.txt
Normal file
78
.circleci/circleci.txt
Normal file
@ -0,0 +1,78 @@
|
||||
# A master build looks like this:
|
||||
|
||||
# BASH_ENV=/tmp/.bash_env-63d018969ca480003a031e62-0-build
|
||||
# CI=true
|
||||
# CIRCLECI=true
|
||||
# CIRCLE_BRANCH=master
|
||||
# CIRCLE_BUILD_NUM=76545
|
||||
# CIRCLE_BUILD_URL=https://circleci.com/gh/tahoe-lafs/tahoe-lafs/76545
|
||||
# CIRCLE_JOB=NixOS 21.11
|
||||
# CIRCLE_NODE_INDEX=0
|
||||
# CIRCLE_NODE_TOTAL=1
|
||||
# CIRCLE_PROJECT_REPONAME=tahoe-lafs
|
||||
# CIRCLE_PROJECT_USERNAME=tahoe-lafs
|
||||
# CIRCLE_REPOSITORY_URL=git@github.com:tahoe-lafs/tahoe-lafs.git
|
||||
# CIRCLE_SHA1=ed0bda2d7456f4a2cd60870072e1fe79864a49a1
|
||||
# CIRCLE_SHELL_ENV=/tmp/.bash_env-63d018969ca480003a031e62-0-build
|
||||
# CIRCLE_USERNAME=alice
|
||||
# CIRCLE_WORKFLOW_ID=6d9bb71c-be3a-4659-bf27-60954180619b
|
||||
# CIRCLE_WORKFLOW_JOB_ID=0793c975-7b9f-489f-909b-8349b72d2785
|
||||
# CIRCLE_WORKFLOW_WORKSPACE_ID=6d9bb71c-be3a-4659-bf27-60954180619b
|
||||
# CIRCLE_WORKING_DIRECTORY=~/project
|
||||
|
||||
# A build of an in-repo PR looks like this:
|
||||
|
||||
# BASH_ENV=/tmp/.bash_env-63d1971a0298086d8841287e-0-build
|
||||
# CI=true
|
||||
# CIRCLECI=true
|
||||
# CIRCLE_BRANCH=3946-less-chatty-downloads
|
||||
# CIRCLE_BUILD_NUM=76612
|
||||
# CIRCLE_BUILD_URL=https://circleci.com/gh/tahoe-lafs/tahoe-lafs/76612
|
||||
# CIRCLE_JOB=NixOS 21.11
|
||||
# CIRCLE_NODE_INDEX=0
|
||||
# CIRCLE_NODE_TOTAL=1
|
||||
# CIRCLE_PROJECT_REPONAME=tahoe-lafs
|
||||
# CIRCLE_PROJECT_USERNAME=tahoe-lafs
|
||||
# CIRCLE_PULL_REQUEST=https://github.com/tahoe-lafs/tahoe-lafs/pull/1251
|
||||
# CIRCLE_PULL_REQUESTS=https://github.com/tahoe-lafs/tahoe-lafs/pull/1251
|
||||
# CIRCLE_REPOSITORY_URL=git@github.com:tahoe-lafs/tahoe-lafs.git
|
||||
# CIRCLE_SHA1=921a2083dcefdb5f431cdac195fc9ac510605349
|
||||
# CIRCLE_SHELL_ENV=/tmp/.bash_env-63d1971a0298086d8841287e-0-build
|
||||
# CIRCLE_USERNAME=bob
|
||||
# CIRCLE_WORKFLOW_ID=5e32c12e-be37-4868-9fa8-6a6929fec2f1
|
||||
# CIRCLE_WORKFLOW_JOB_ID=316ca408-81b4-4c96-bbdd-644e4c3e01e5
|
||||
# CIRCLE_WORKFLOW_WORKSPACE_ID=5e32c12e-be37-4868-9fa8-6a6929fec2f1
|
||||
# CIRCLE_WORKING_DIRECTORY=~/project
|
||||
# CI_PULL_REQUEST=https://github.com/tahoe-lafs/tahoe-lafs/pull/1251
|
||||
|
||||
# A build of a PR from a fork looks like this:
|
||||
|
||||
# BASH_ENV=/tmp/.bash_env-63d40f7b2e89cd3de10e0db9-0-build
|
||||
# CI=true
|
||||
# CIRCLECI=true
|
||||
# CIRCLE_BRANCH=pull/1252
|
||||
# CIRCLE_BUILD_NUM=76678
|
||||
# CIRCLE_BUILD_URL=https://circleci.com/gh/tahoe-lafs/tahoe-lafs/76678
|
||||
# CIRCLE_JOB=NixOS 21.05
|
||||
# CIRCLE_NODE_INDEX=0
|
||||
# CIRCLE_NODE_TOTAL=1
|
||||
# CIRCLE_PROJECT_REPONAME=tahoe-lafs
|
||||
# CIRCLE_PROJECT_USERNAME=tahoe-lafs
|
||||
# CIRCLE_PR_NUMBER=1252
|
||||
# CIRCLE_PR_REPONAME=tahoe-lafs
|
||||
# CIRCLE_PR_USERNAME=carol
|
||||
# CIRCLE_PULL_REQUEST=https://github.com/tahoe-lafs/tahoe-lafs/pull/1252
|
||||
# CIRCLE_PULL_REQUESTS=https://github.com/tahoe-lafs/tahoe-lafs/pull/1252
|
||||
# CIRCLE_REPOSITORY_URL=git@github.com:tahoe-lafs/tahoe-lafs.git
|
||||
# CIRCLE_SHA1=15c7916e0812e6baa2a931cd54b18f3382a8456e
|
||||
# CIRCLE_SHELL_ENV=/tmp/.bash_env-63d40f7b2e89cd3de10e0db9-0-build
|
||||
# CIRCLE_USERNAME=
|
||||
# CIRCLE_WORKFLOW_ID=19c917c8-3a38-4b20-ac10-3265259fa03e
|
||||
# CIRCLE_WORKFLOW_JOB_ID=58e95215-eccf-4664-a231-1dba7fd2d323
|
||||
# CIRCLE_WORKFLOW_WORKSPACE_ID=19c917c8-3a38-4b20-ac10-3265259fa03e
|
||||
# CIRCLE_WORKING_DIRECTORY=~/project
|
||||
# CI_PULL_REQUEST=https://github.com/tahoe-lafs/tahoe-lafs/pull/1252
|
||||
|
||||
# A build of a PR from a fork where the owner has enabled CircleCI looks
|
||||
# the same as a build of an in-repo PR, except it runs on th owner's
|
||||
# CircleCI namespace.
|
@ -11,188 +11,354 @@
|
||||
#
|
||||
version: 2.1
|
||||
|
||||
# Every job that pushes a Docker image from Docker Hub must authenticate to
|
||||
# it. Define a couple yaml anchors that can be used to supply the necessary
|
||||
# credentials.
|
||||
|
||||
# First is a CircleCI job context which makes Docker Hub credentials available
|
||||
# in the environment.
|
||||
#
|
||||
# Contexts are managed in the CircleCI web interface:
|
||||
#
|
||||
# https://app.circleci.com/settings/organization/github/tahoe-lafs/contexts
|
||||
dockerhub-context-template: &DOCKERHUB_CONTEXT
|
||||
context: "dockerhub-auth"
|
||||
|
||||
# Required environment for using the coveralls tool to upload partial coverage
|
||||
# reports and then finish the process.
|
||||
coveralls-environment: &COVERALLS_ENVIRONMENT
|
||||
COVERALLS_REPO_TOKEN: "JPf16rLB7T2yjgATIxFzTsEgMdN1UNq6o"
|
||||
|
||||
# Next is a Docker executor template that gets the credentials from the
|
||||
# environment and supplies them to the executor.
|
||||
dockerhub-auth-template: &DOCKERHUB_AUTH
|
||||
- auth:
|
||||
username: $DOCKERHUB_USERNAME
|
||||
password: $DOCKERHUB_PASSWORD
|
||||
|
||||
# A template that can be shared between the two different image-building
|
||||
# workflows.
|
||||
.images: &IMAGES
|
||||
jobs:
|
||||
- "build-image-debian-11":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-ubuntu-20-04":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-ubuntu-22-04":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-fedora-35":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-oraclelinux-8":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
# Restore later as PyPy38
|
||||
#- "build-image-pypy27-buster":
|
||||
# <<: *DOCKERHUB_CONTEXT
|
||||
|
||||
parameters:
|
||||
# Control whether the image-building workflow runs as part of this pipeline.
|
||||
# Generally we do not want this to run because we don't need our
|
||||
# dependencies to move around all the time and because building the image
|
||||
# takes a couple minutes.
|
||||
#
|
||||
# An easy way to trigger a pipeline with this set to true is with the
|
||||
# rebuild-images.sh tool in this directory. You can also do so via the
|
||||
# CircleCI web UI.
|
||||
build-images:
|
||||
default: false
|
||||
type: "boolean"
|
||||
|
||||
# Control whether the test-running workflow runs as part of this pipeline.
|
||||
# Generally we do want this to run because running the tests is the primary
|
||||
# purpose of this pipeline.
|
||||
run-tests:
|
||||
default: true
|
||||
type: "boolean"
|
||||
|
||||
workflows:
|
||||
ci:
|
||||
when: "<< pipeline.parameters.run-tests >>"
|
||||
jobs:
|
||||
# Start with jobs testing various platforms.
|
||||
- "debian-9":
|
||||
- "debian-11":
|
||||
{}
|
||||
- "debian-10":
|
||||
requires:
|
||||
- "debian-9"
|
||||
|
||||
- "ubuntu-20-04":
|
||||
{}
|
||||
- "ubuntu-18-04":
|
||||
requires:
|
||||
- "ubuntu-20-04"
|
||||
- "ubuntu-16-04":
|
||||
requires:
|
||||
- "ubuntu-20-04"
|
||||
|
||||
- "fedora-29":
|
||||
{}
|
||||
- "fedora-28":
|
||||
requires:
|
||||
- "fedora-29"
|
||||
|
||||
- "centos-8":
|
||||
- "ubuntu-22-04":
|
||||
{}
|
||||
|
||||
- "nixos-19-09":
|
||||
# Equivalent to RHEL 8; CentOS 8 is dead.
|
||||
- "oraclelinux-8":
|
||||
{}
|
||||
|
||||
- "nixos-21-05":
|
||||
{}
|
||||
- "nixos":
|
||||
name: "<<matrix.pythonVersion>>"
|
||||
nixpkgs: "nixpkgs-unstable"
|
||||
matrix:
|
||||
parameters:
|
||||
pythonVersion:
|
||||
- "python39"
|
||||
- "python310"
|
||||
- "python311"
|
||||
|
||||
# Test against PyPy 2.7
|
||||
- "pypy27-buster":
|
||||
{}
|
||||
|
||||
# Just one Python 3.6 configuration while the port is in-progress.
|
||||
- "python36":
|
||||
{}
|
||||
# Eventually, test against PyPy 3.8
|
||||
#- "pypy27-buster":
|
||||
# {}
|
||||
|
||||
# Other assorted tasks and configurations
|
||||
- "lint":
|
||||
{}
|
||||
- "codechecks3":
|
||||
- "codechecks":
|
||||
{}
|
||||
- "pyinstaller":
|
||||
{}
|
||||
- "deprecations":
|
||||
{}
|
||||
- "c-locale":
|
||||
{}
|
||||
# Any locale other than C or UTF-8.
|
||||
- "another-locale":
|
||||
{}
|
||||
|
||||
- "windows-server-2022":
|
||||
name: "Windows Server 2022, CPython <<matrix.pythonVersion>>"
|
||||
matrix:
|
||||
parameters:
|
||||
# Run the job for a number of CPython versions. These are the
|
||||
# two versions installed on the version of the Windows VM image
|
||||
# we specify (in the executor). This is handy since it means we
|
||||
# don't have to do any Python installation work. We pin the
|
||||
# Windows VM image so these shouldn't shuffle around beneath us
|
||||
# but if we want to update that image or get different versions
|
||||
# of Python, we probably have to do something here.
|
||||
pythonVersion:
|
||||
- "3.9"
|
||||
- "3.11"
|
||||
|
||||
- "integration":
|
||||
# Run even the slow integration tests here. We need the `--` to
|
||||
# sneak past tox and get to pytest.
|
||||
tox-args: "-- --runslow integration"
|
||||
requires:
|
||||
# If the unit test suite doesn't pass, don't bother running the
|
||||
# integration tests.
|
||||
- "debian-9"
|
||||
- "debian-11"
|
||||
|
||||
- "typechecks":
|
||||
{}
|
||||
- "docs":
|
||||
{}
|
||||
|
||||
- "finish-coverage-report":
|
||||
requires:
|
||||
# Referencing the job by "alias" (as CircleCI calls the mapping
|
||||
# key) instead of the value of its "name" property causes us to
|
||||
# require every instance of the job from its matrix expansion. So
|
||||
# this requirement is enough to require every Windows Server 2022
|
||||
# job.
|
||||
- "windows-server-2022"
|
||||
|
||||
images:
|
||||
# Build the Docker images used by the ci jobs. This makes the ci jobs
|
||||
# faster and takes various spurious failures out of the critical path.
|
||||
triggers:
|
||||
# Build once a day
|
||||
- schedule:
|
||||
cron: "0 0 * * *"
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- "master"
|
||||
|
||||
jobs:
|
||||
# Every job that pushes a Docker image from Docker Hub needs to provide
|
||||
# credentials. Use this first job to define a yaml anchor that can be
|
||||
# used to supply a CircleCI job context which makes Docker Hub
|
||||
# credentials available in the environment.
|
||||
#
|
||||
# Contexts are managed in the CircleCI web interface:
|
||||
#
|
||||
# https://app.circleci.com/settings/organization/github/tahoe-lafs/contexts
|
||||
- "build-image-debian-10": &DOCKERHUB_CONTEXT
|
||||
context: "dockerhub-auth"
|
||||
- "build-image-debian-9":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-ubuntu-16-04":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-ubuntu-18-04":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-ubuntu-20-04":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-fedora-28":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-fedora-29":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-centos-8":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-pypy27-buster":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
- "build-image-python36-ubuntu":
|
||||
<<: *DOCKERHUB_CONTEXT
|
||||
<<: *IMAGES
|
||||
|
||||
# Build as part of the workflow but only if requested.
|
||||
when: "<< pipeline.parameters.build-images >>"
|
||||
|
||||
jobs:
|
||||
dockerhub-auth-template:
|
||||
# This isn't a real job. It doesn't get scheduled as part of any
|
||||
# workflow. Instead, it's just a place we can hang a yaml anchor to
|
||||
# finish the Docker Hub authentication configuration. Workflow jobs using
|
||||
# the DOCKERHUB_CONTEXT anchor will have access to the environment
|
||||
# variables used here. These variables will allow the Docker Hub image
|
||||
# pull to be authenticated and hopefully avoid hitting and rate limits.
|
||||
docker: &DOCKERHUB_AUTH
|
||||
- image: "null"
|
||||
auth:
|
||||
username: $DOCKERHUB_USERNAME
|
||||
password: $DOCKERHUB_PASSWORD
|
||||
finish-coverage-report:
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "python:3-slim"
|
||||
|
||||
steps:
|
||||
- run:
|
||||
name: "CircleCI YAML schema conformity"
|
||||
name: "Indicate completion to coveralls.io"
|
||||
environment:
|
||||
<<: *COVERALLS_ENVIRONMENT
|
||||
command: |
|
||||
# This isn't a real command. We have to have something in this
|
||||
# space, though, or the CircleCI yaml schema validator gets angry.
|
||||
# Since this job is never scheduled this step is never run so the
|
||||
# actual value here is irrelevant.
|
||||
pip install coveralls==3.3.1
|
||||
python -m coveralls --finish
|
||||
|
||||
lint:
|
||||
codechecks:
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "circleci/python:2"
|
||||
image: "cimg/python:3.9"
|
||||
|
||||
steps:
|
||||
- "checkout"
|
||||
|
||||
- run:
|
||||
- run: &INSTALL_TOX
|
||||
name: "Install tox"
|
||||
command: |
|
||||
pip install --user tox
|
||||
pip install --user 'tox~=3.0'
|
||||
|
||||
- run:
|
||||
name: "Static-ish code checks"
|
||||
command: |
|
||||
~/.local/bin/tox -e codechecks
|
||||
|
||||
codechecks3:
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "circleci/python:3"
|
||||
windows-server-2022:
|
||||
parameters:
|
||||
pythonVersion:
|
||||
description: >-
|
||||
An argument to pass to the `py` launcher to choose a Python version.
|
||||
type: "string"
|
||||
default: ""
|
||||
|
||||
executor: "windows"
|
||||
environment:
|
||||
# Tweak Hypothesis to make its behavior more suitable for the CI
|
||||
# environment. This should improve reproducibility and lessen the
|
||||
# effects of variable compute resources.
|
||||
TAHOE_LAFS_HYPOTHESIS_PROFILE: "ci"
|
||||
|
||||
# Tell pip where its download cache lives. This must agree with the
|
||||
# "save_cache" step below or caching won't really work right.
|
||||
PIP_CACHE_DIR: "pip-cache"
|
||||
|
||||
# And tell pip where it can find out cached wheelhouse for fast wheel
|
||||
# installation, even for projects that don't distribute wheels. This
|
||||
# must also agree with the "save_cache" step below.
|
||||
PIP_FIND_LINKS: "wheelhouse"
|
||||
|
||||
steps:
|
||||
- "checkout"
|
||||
|
||||
- run:
|
||||
name: "Install tox"
|
||||
command: |
|
||||
pip install --user tox
|
||||
# If possible, restore a pip download cache to save us from having to
|
||||
# download all our Python dependencies from PyPI.
|
||||
- "restore_cache":
|
||||
keys:
|
||||
# The download cache and/or the wheelhouse may contain Python
|
||||
# version-specific binary packages so include the Python version
|
||||
# in this key, as well as the canonical source of our
|
||||
# dependencies.
|
||||
- &CACHE_KEY "pip-packages-v1-<< parameters.pythonVersion >>-{{ checksum \"setup.py\" }}"
|
||||
|
||||
- run:
|
||||
name: "Static-ish code checks"
|
||||
- "run":
|
||||
name: "Fix $env:PATH"
|
||||
command: |
|
||||
~/.local/bin/tox -e codechecks3
|
||||
# The Python this job is parameterized is not necessarily the one
|
||||
# at the front of $env:PATH. Modify $env:PATH so that it is so we
|
||||
# can just say "python" in the rest of the steps. Also get the
|
||||
# related Scripts directory so tools from packages we install are
|
||||
# also available.
|
||||
$p = py -<<parameters.pythonVersion>> -c "import sys; print(sys.prefix)"
|
||||
$q = py -<<parameters.pythonVersion>> -c "import sysconfig; print(sysconfig.get_path('scripts'))"
|
||||
|
||||
New-Item $Profile.CurrentUserAllHosts -Force
|
||||
# $p gets "python" on PATH and $q gets tools from packages we
|
||||
# install. Note we carefully construct the string so that
|
||||
# $env:PATH is not substituted now but $p and $q are. ` is the
|
||||
# PowerShell string escape character.
|
||||
Add-Content -Path $Profile.CurrentUserAllHosts -Value "`$env:PATH = `"$p;$q;`$env:PATH`""
|
||||
|
||||
- "run":
|
||||
name: "Display tool versions"
|
||||
command: |
|
||||
python misc/build_helpers/show-tool-versions.py
|
||||
|
||||
- "run":
|
||||
# It's faster to install a wheel than a source package. If we don't
|
||||
# have a cached wheelhouse then build all of the wheels and dump
|
||||
# them into a directory where they can become a cached wheelhouse.
|
||||
# We would have built these wheels during installation anyway so it
|
||||
# doesn't cost us anything extra and saves us effort next time.
|
||||
name: "(Maybe) Build Wheels"
|
||||
command: |
|
||||
if ((Test-Path .\wheelhouse) -and (Test-Path .\wheelhouse\*)) {
|
||||
echo "Found populated wheelhouse, skipping wheel building."
|
||||
} else {
|
||||
python -m pip install wheel
|
||||
python -m pip wheel --wheel-dir $env:PIP_FIND_LINKS .[testenv] .[test]
|
||||
}
|
||||
|
||||
- "save_cache":
|
||||
paths:
|
||||
# Make sure this agrees with PIP_CACHE_DIR in the environment.
|
||||
- "pip-cache"
|
||||
- "wheelhouse"
|
||||
key: *CACHE_KEY
|
||||
|
||||
- "run":
|
||||
name: "Install Dependencies"
|
||||
environment:
|
||||
# By this point we should no longer need an index.
|
||||
PIP_NO_INDEX: "1"
|
||||
command: |
|
||||
python -m pip install .[testenv] .[test]
|
||||
|
||||
- "run":
|
||||
name: "Run Unit Tests"
|
||||
environment:
|
||||
# Configure the results location for the subunitv2-file reporter
|
||||
# from subunitreporter
|
||||
SUBUNITREPORTER_OUTPUT_PATH: "test-results.subunit2"
|
||||
|
||||
# Try to get prompt output from the reporter to avoid no-output
|
||||
# timeouts.
|
||||
PYTHONUNBUFFERED: "1"
|
||||
|
||||
command: |
|
||||
# Run the test suite under coverage measurement using the
|
||||
# parameterized version of Python, writing subunitv2-format
|
||||
# results to the file given in the environment.
|
||||
python -b -m coverage run -m twisted.trial --reporter=subunitv2-file --rterrors allmydata
|
||||
|
||||
- "run":
|
||||
name: "Upload Coverage"
|
||||
environment:
|
||||
<<: *COVERALLS_ENVIRONMENT
|
||||
# Mark the data as just one piece of many because we have more
|
||||
# than one instance of this job (two on Windows now, some on other
|
||||
# platforms later) which collects and reports coverage. This is
|
||||
# necessary to cause Coveralls to merge multiple coverage results
|
||||
# into a single report. Note the merge only happens when we
|
||||
# "finish" a particular build, as identified by its "build_num"
|
||||
# (aka "service_number").
|
||||
COVERALLS_PARALLEL: "true"
|
||||
command: |
|
||||
python -m pip install coveralls==3.3.1
|
||||
|
||||
# .coveragerc sets parallel = True so we don't have a `.coverage`
|
||||
# file but a `.coverage.<unique stuff>` file (or maybe more than
|
||||
# one, but probably not). coveralls can't work with these so
|
||||
# merge them before invoking it.
|
||||
python -m coverage combine
|
||||
|
||||
# Now coveralls will be able to find the data, so have it do the
|
||||
# upload. Also, have it strip the system config-specific prefix
|
||||
# from all of the source paths.
|
||||
$prefix = python -c "import sysconfig; print(sysconfig.get_path('purelib'))"
|
||||
python -m coveralls --basedir $prefix
|
||||
|
||||
- "run":
|
||||
name: "Convert Result Log"
|
||||
command: |
|
||||
# subunit2junitxml exits with error if the result stream it is
|
||||
# converting has test failures in it! So this step might fail.
|
||||
# Since the step in which we actually _ran_ the tests won't fail
|
||||
# even if there are test failures, this is a good thing for now.
|
||||
subunit2junitxml.exe --output-to=test-results.xml test-results.subunit2
|
||||
|
||||
- "store_test_results":
|
||||
path: "test-results.xml"
|
||||
|
||||
- "store_artifacts":
|
||||
path: "_trial_temp/test.log"
|
||||
|
||||
- "store_artifacts":
|
||||
path: "eliot.log"
|
||||
|
||||
- "store_artifacts":
|
||||
path: ".coverage"
|
||||
|
||||
pyinstaller:
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "circleci/python:2"
|
||||
image: "cimg/python:3.9"
|
||||
|
||||
steps:
|
||||
- "checkout"
|
||||
|
||||
- run:
|
||||
name: "Install tox"
|
||||
command: |
|
||||
pip install --user tox
|
||||
<<: *INSTALL_TOX
|
||||
|
||||
- run:
|
||||
name: "Make PyInstaller executable"
|
||||
@ -207,12 +373,7 @@ jobs:
|
||||
command: |
|
||||
dist/Tahoe-LAFS/tahoe --version
|
||||
|
||||
debian-9: &DEBIAN
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/debian:9-py2.7"
|
||||
user: "nobody"
|
||||
|
||||
debian-11: &DEBIAN
|
||||
environment: &UTF_8_ENVIRONMENT
|
||||
# In general, the test suite is not allowed to fail while the job
|
||||
# succeeds. But you can set this to "yes" if you want it to be
|
||||
@ -224,7 +385,7 @@ jobs:
|
||||
# filenames and argv).
|
||||
LANG: "en_US.UTF-8"
|
||||
# Select a tox environment to run for this job.
|
||||
TAHOE_LAFS_TOX_ENVIRONMENT: "py27"
|
||||
TAHOE_LAFS_TOX_ENVIRONMENT: "py39"
|
||||
# Additional arguments to pass to tox.
|
||||
TAHOE_LAFS_TOX_ARGS: ""
|
||||
# The path in which test artifacts will be placed.
|
||||
@ -289,32 +450,28 @@ jobs:
|
||||
name: "Submit coverage results"
|
||||
command: |
|
||||
if [ -n "${UPLOAD_COVERAGE}" ]; then
|
||||
/tmp/venv/bin/codecov
|
||||
echo "TODO: Need a new coverage solution, see https://tahoe-lafs.org/trac/tahoe-lafs/ticket/4011"
|
||||
fi
|
||||
|
||||
|
||||
debian-10:
|
||||
<<: *DEBIAN
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/debian:10-py2.7"
|
||||
image: "tahoelafsci/debian:11-py3.9"
|
||||
user: "nobody"
|
||||
|
||||
|
||||
pypy27-buster:
|
||||
<<: *DEBIAN
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/pypy:buster-py2"
|
||||
user: "nobody"
|
||||
|
||||
environment:
|
||||
<<: *UTF_8_ENVIRONMENT
|
||||
# We don't do coverage since it makes PyPy far too slow:
|
||||
TAHOE_LAFS_TOX_ENVIRONMENT: "pypy27"
|
||||
# Since we didn't collect it, don't upload it.
|
||||
UPLOAD_COVERAGE: ""
|
||||
|
||||
# Restore later using PyPy3.8
|
||||
# pypy27-buster:
|
||||
# <<: *DEBIAN
|
||||
# docker:
|
||||
# - <<: *DOCKERHUB_AUTH
|
||||
# image: "tahoelafsci/pypy:buster-py2"
|
||||
# user: "nobody"
|
||||
# environment:
|
||||
# <<: *UTF_8_ENVIRONMENT
|
||||
# # We don't do coverage since it makes PyPy far too slow:
|
||||
# TAHOE_LAFS_TOX_ENVIRONMENT: "pypy27"
|
||||
# # Since we didn't collect it, don't upload it.
|
||||
# UPLOAD_COVERAGE: ""
|
||||
|
||||
c-locale:
|
||||
<<: *DEBIAN
|
||||
@ -332,23 +489,21 @@ jobs:
|
||||
# aka "Latin 1"
|
||||
LANG: "en_US.ISO-8859-1"
|
||||
|
||||
|
||||
deprecations:
|
||||
<<: *DEBIAN
|
||||
|
||||
environment:
|
||||
<<: *UTF_8_ENVIRONMENT
|
||||
# Select the deprecations tox environments.
|
||||
TAHOE_LAFS_TOX_ENVIRONMENT: "deprecations,upcoming-deprecations"
|
||||
# Put the logs somewhere we can report them.
|
||||
TAHOE_LAFS_WARNINGS_LOG: "/tmp/artifacts/deprecation-warnings.log"
|
||||
# The deprecations tox environments don't do coverage measurement.
|
||||
UPLOAD_COVERAGE: ""
|
||||
|
||||
|
||||
integration:
|
||||
<<: *DEBIAN
|
||||
|
||||
parameters:
|
||||
tox-args:
|
||||
description: >-
|
||||
Additional arguments to pass to the tox command.
|
||||
type: "string"
|
||||
default: ""
|
||||
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/debian:11-py3.9"
|
||||
user: "nobody"
|
||||
|
||||
environment:
|
||||
<<: *UTF_8_ENVIRONMENT
|
||||
# Select the integration tests tox environments.
|
||||
@ -356,60 +511,44 @@ jobs:
|
||||
# Disable artifact collection because py.test can't produce any.
|
||||
ARTIFACTS_OUTPUT_PATH: ""
|
||||
|
||||
# Pass on anything we got in our parameters.
|
||||
TAHOE_LAFS_TOX_ARGS: "<< parameters.tox-args >>"
|
||||
|
||||
steps:
|
||||
- "checkout"
|
||||
# DRY, YAML-style. See the debian-9 steps.
|
||||
- run: *SETUP_VIRTUALENV
|
||||
- run: *RUN_TESTS
|
||||
|
||||
|
||||
ubuntu-16-04:
|
||||
<<: *DEBIAN
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/ubuntu:16.04-py2.7"
|
||||
user: "nobody"
|
||||
|
||||
|
||||
ubuntu-18-04: &UBUNTU_18_04
|
||||
<<: *DEBIAN
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/ubuntu:18.04-py2.7"
|
||||
user: "nobody"
|
||||
|
||||
|
||||
python36:
|
||||
<<: *UBUNTU_18_04
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/ubuntu:18.04-py3"
|
||||
user: "nobody"
|
||||
|
||||
environment:
|
||||
<<: *UTF_8_ENVIRONMENT
|
||||
# The default trial args include --rterrors which is incompatible with
|
||||
# this reporter on Python 3. So drop that and just specify the
|
||||
# reporter.
|
||||
TAHOE_LAFS_TRIAL_ARGS: "--reporter=subunitv2-file"
|
||||
TAHOE_LAFS_TOX_ENVIRONMENT: "py36"
|
||||
|
||||
|
||||
ubuntu-20-04:
|
||||
<<: *DEBIAN
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/ubuntu:20.04"
|
||||
image: "tahoelafsci/ubuntu:20.04-py3.9"
|
||||
user: "nobody"
|
||||
environment:
|
||||
<<: *UTF_8_ENVIRONMENT
|
||||
TAHOE_LAFS_TOX_ENVIRONMENT: "py39"
|
||||
|
||||
|
||||
centos-8: &RHEL_DERIV
|
||||
ubuntu-22-04:
|
||||
<<: *DEBIAN
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/centos:8-py2"
|
||||
image: "tahoelafsci/ubuntu:22.04-py3.10"
|
||||
user: "nobody"
|
||||
environment:
|
||||
<<: *UTF_8_ENVIRONMENT
|
||||
TAHOE_LAFS_TOX_ENVIRONMENT: "py310"
|
||||
|
||||
oraclelinux-8: &RHEL_DERIV
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/oraclelinux:8-py3.8"
|
||||
user: "nobody"
|
||||
|
||||
environment: *UTF_8_ENVIRONMENT
|
||||
environment:
|
||||
<<: *UTF_8_ENVIRONMENT
|
||||
TAHOE_LAFS_TOX_ENVIRONMENT: "py38"
|
||||
|
||||
# pip cannot install packages if the working directory is not readable.
|
||||
# We want to run a lot of steps as nobody instead of as root.
|
||||
@ -425,63 +564,51 @@ jobs:
|
||||
- store_artifacts: *STORE_OTHER_ARTIFACTS
|
||||
- run: *SUBMIT_COVERAGE
|
||||
|
||||
|
||||
fedora-28:
|
||||
fedora-35:
|
||||
<<: *RHEL_DERIV
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/fedora:28-py"
|
||||
image: "tahoelafsci/fedora:35-py3"
|
||||
user: "nobody"
|
||||
|
||||
nixos:
|
||||
parameters:
|
||||
nixpkgs:
|
||||
description: >-
|
||||
Reference the name of a flake-managed nixpkgs input (see `nix flake
|
||||
metadata` and flake.nix)
|
||||
type: "string"
|
||||
pythonVersion:
|
||||
description: >-
|
||||
Reference the name of a Python package in nixpkgs to use.
|
||||
type: "string"
|
||||
|
||||
fedora-29:
|
||||
<<: *RHEL_DERIV
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/fedora:29-py"
|
||||
user: "nobody"
|
||||
|
||||
nixos-19-09: &NIXOS
|
||||
docker:
|
||||
# Run in a highly Nix-capable environment.
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "nixorg/nix:circleci"
|
||||
|
||||
environment:
|
||||
NIX_PATH: "nixpkgs=https://github.com/NixOS/nixpkgs-channels/archive/nixos-19.09-small.tar.gz"
|
||||
SOURCE: "nix/"
|
||||
executor: "nix"
|
||||
|
||||
steps:
|
||||
- "checkout"
|
||||
- "run":
|
||||
name: "Build and Test"
|
||||
command: |
|
||||
# CircleCI build environment looks like it has a zillion and a
|
||||
# half cores. Don't let Nix autodetect this high core count
|
||||
# because it blows up memory usage and fails the test run. Pick a
|
||||
# number of cores that suites the build environment we're paying
|
||||
# for (the free one!).
|
||||
#
|
||||
# Also, let it run more than one job at a time because we have to
|
||||
# build a couple simple little dependencies that don't take
|
||||
# advantage of multiple cores and we get a little speedup by doing
|
||||
# them in parallel.
|
||||
nix-build --cores 3 --max-jobs 2 "$SOURCE"
|
||||
- "nix-build":
|
||||
nixpkgs: "<<parameters.nixpkgs>>"
|
||||
pythonVersion: "<<parameters.pythonVersion>>"
|
||||
buildSteps:
|
||||
- "run":
|
||||
name: "Unit Test"
|
||||
command: |
|
||||
source .circleci/lib.sh
|
||||
|
||||
nixos-21-05:
|
||||
<<: *NIXOS
|
||||
# Translate the nixpkgs selection into a flake reference we
|
||||
# can use to override the default nixpkgs input.
|
||||
NIXPKGS=$(nixpkgs_flake_reference <<parameters.nixpkgs>>)
|
||||
|
||||
environment:
|
||||
# Note this doesn't look more similar to the 19.09 NIX_PATH URL because
|
||||
# there was some internal shuffling by the NixOS project about how they
|
||||
# publish stable revisions.
|
||||
NIX_PATH: "nixpkgs=https://github.com/NixOS/nixpkgs/archive/d32b07e6df276d78e3640eb43882b80c9b2b3459.tar.gz"
|
||||
SOURCE: "nix/py3.nix"
|
||||
cache_if_able nix run \
|
||||
--override-input nixpkgs "$NIXPKGS" \
|
||||
.#<<parameters.pythonVersion>>-unittest -- \
|
||||
--jobs $UNITTEST_CORES \
|
||||
allmydata
|
||||
|
||||
typechecks:
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/ubuntu:18.04-py3"
|
||||
image: "tahoelafsci/ubuntu:20.04-py3.9"
|
||||
|
||||
steps:
|
||||
- "checkout"
|
||||
@ -493,7 +620,7 @@ jobs:
|
||||
docs:
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "tahoelafsci/ubuntu:18.04-py3"
|
||||
image: "tahoelafsci/ubuntu:20.04-py3.9"
|
||||
|
||||
steps:
|
||||
- "checkout"
|
||||
@ -511,16 +638,19 @@ jobs:
|
||||
# https://circleci.com/blog/how-to-build-a-docker-image-on-circleci-2-0/
|
||||
docker:
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "docker:17.05.0-ce-git"
|
||||
# CircleCI build images; https://github.com/CircleCI-Public/cimg-base
|
||||
# for details.
|
||||
image: "cimg/base:2022.01"
|
||||
|
||||
environment:
|
||||
DISTRO: "tahoelafsci/<DISTRO>:foo-py2"
|
||||
TAG: "tahoelafsci/distro:<TAG>-py2"
|
||||
DISTRO: "tahoelafsci/<DISTRO>:foo-py3.9"
|
||||
TAG: "tahoelafsci/distro:<TAG>-py3.9"
|
||||
PYTHON_VERSION: "tahoelafsci/distro:tag-py<PYTHON_VERSION}"
|
||||
|
||||
steps:
|
||||
- "checkout"
|
||||
- "setup_remote_docker"
|
||||
- setup_remote_docker:
|
||||
version: "20.10.11"
|
||||
- run:
|
||||
name: "Log in to Dockerhub"
|
||||
command: |
|
||||
@ -541,49 +671,13 @@ jobs:
|
||||
docker push tahoelafsci/${DISTRO}:${TAG}-py${PYTHON_VERSION}
|
||||
|
||||
|
||||
build-image-debian-10:
|
||||
build-image-debian-11:
|
||||
<<: *BUILD_IMAGE
|
||||
|
||||
environment:
|
||||
DISTRO: "debian"
|
||||
TAG: "10"
|
||||
PYTHON_VERSION: "2.7"
|
||||
|
||||
|
||||
build-image-debian-9:
|
||||
<<: *BUILD_IMAGE
|
||||
|
||||
environment:
|
||||
DISTRO: "debian"
|
||||
TAG: "9"
|
||||
PYTHON_VERSION: "2.7"
|
||||
|
||||
|
||||
build-image-ubuntu-16-04:
|
||||
<<: *BUILD_IMAGE
|
||||
|
||||
environment:
|
||||
DISTRO: "ubuntu"
|
||||
TAG: "16.04"
|
||||
PYTHON_VERSION: "2.7"
|
||||
|
||||
|
||||
build-image-ubuntu-18-04:
|
||||
<<: *BUILD_IMAGE
|
||||
|
||||
environment:
|
||||
DISTRO: "ubuntu"
|
||||
TAG: "18.04"
|
||||
PYTHON_VERSION: "2.7"
|
||||
|
||||
|
||||
build-image-python36-ubuntu:
|
||||
<<: *BUILD_IMAGE
|
||||
|
||||
environment:
|
||||
DISTRO: "ubuntu"
|
||||
TAG: "18.04"
|
||||
PYTHON_VERSION: "3"
|
||||
TAG: "11"
|
||||
PYTHON_VERSION: "3.9"
|
||||
|
||||
|
||||
build-image-ubuntu-20-04:
|
||||
@ -592,43 +686,122 @@ jobs:
|
||||
environment:
|
||||
DISTRO: "ubuntu"
|
||||
TAG: "20.04"
|
||||
PYTHON_VERSION: "2.7"
|
||||
PYTHON_VERSION: "3.9"
|
||||
|
||||
|
||||
build-image-centos-8:
|
||||
build-image-ubuntu-22-04:
|
||||
<<: *BUILD_IMAGE
|
||||
|
||||
environment:
|
||||
DISTRO: "centos"
|
||||
DISTRO: "ubuntu"
|
||||
TAG: "22.04"
|
||||
PYTHON_VERSION: "3.10"
|
||||
|
||||
|
||||
build-image-oraclelinux-8:
|
||||
<<: *BUILD_IMAGE
|
||||
|
||||
environment:
|
||||
DISTRO: "oraclelinux"
|
||||
TAG: "8"
|
||||
PYTHON_VERSION: "2"
|
||||
PYTHON_VERSION: "3.8"
|
||||
|
||||
|
||||
build-image-fedora-28:
|
||||
build-image-fedora-35:
|
||||
<<: *BUILD_IMAGE
|
||||
|
||||
environment:
|
||||
DISTRO: "fedora"
|
||||
TAG: "28"
|
||||
# The default on Fedora (this version anyway) is still Python 2.
|
||||
PYTHON_VERSION: ""
|
||||
TAG: "35"
|
||||
PYTHON_VERSION: "3"
|
||||
|
||||
# build-image-pypy27-buster:
|
||||
# <<: *BUILD_IMAGE
|
||||
# environment:
|
||||
# DISTRO: "pypy"
|
||||
# TAG: "buster"
|
||||
# # We only have Python 2 for PyPy right now so there's no support for
|
||||
# # setting up PyPy 3 in the image building toolchain. This value is just
|
||||
# # for constructing the right Docker image tag.
|
||||
# PYTHON_VERSION: "2"
|
||||
|
||||
build-image-fedora-29:
|
||||
<<: *BUILD_IMAGE
|
||||
executors:
|
||||
windows:
|
||||
# Choose a Windows environment that closest matches our testing
|
||||
# requirements and goals.
|
||||
# https://circleci.com/developer/orbs/orb/circleci/windows#executors-server-2022
|
||||
machine:
|
||||
image: "windows-server-2022-gui:2023.06.1"
|
||||
shell: "powershell.exe -ExecutionPolicy Bypass"
|
||||
resource_class: "windows.large"
|
||||
|
||||
nix:
|
||||
docker:
|
||||
# Run in a highly Nix-capable environment.
|
||||
- <<: *DOCKERHUB_AUTH
|
||||
image: "nixos/nix:2.16.1"
|
||||
environment:
|
||||
DISTRO: "fedora"
|
||||
TAG: "29"
|
||||
# CACHIX_AUTH_TOKEN is manually set in the CircleCI web UI and allows us
|
||||
# to push to CACHIX_NAME. CACHIX_NAME tells cachix which cache to push
|
||||
# to.
|
||||
CACHIX_NAME: "tahoe-lafs-opensource"
|
||||
# Let us use features marked "experimental". For example, most/all of
|
||||
# the `nix <subcommand>` forms.
|
||||
NIX_CONFIG: "experimental-features = nix-command flakes"
|
||||
|
||||
commands:
|
||||
nix-build:
|
||||
parameters:
|
||||
nixpkgs:
|
||||
description: >-
|
||||
Reference the name of a flake-managed nixpkgs input (see `nix flake
|
||||
metadata` and flake.nix)
|
||||
type: "string"
|
||||
pythonVersion:
|
||||
description: >-
|
||||
Reference the name of a Python package in nixpkgs to use.
|
||||
type: "string"
|
||||
buildSteps:
|
||||
description: >-
|
||||
The build steps to execute after setting up the build environment.
|
||||
type: "steps"
|
||||
|
||||
build-image-pypy27-buster:
|
||||
<<: *BUILD_IMAGE
|
||||
steps:
|
||||
- "run":
|
||||
# Get cachix for Nix-friendly caching.
|
||||
name: "Install Basic Dependencies"
|
||||
command: |
|
||||
# Get some build environment dependencies and let them float on a
|
||||
# certain release branch. These aren't involved in the actual
|
||||
# package build (only in CI environment setup) so the fact that
|
||||
# they float shouldn't hurt reproducibility.
|
||||
NIXPKGS="nixpkgs/nixos-23.05"
|
||||
nix profile install $NIXPKGS#cachix $NIXPKGS#bash $NIXPKGS#jp
|
||||
|
||||
environment:
|
||||
DISTRO: "pypy"
|
||||
TAG: "buster"
|
||||
# We only have Python 2 for PyPy right now so there's no support for
|
||||
# setting up PyPy 3 in the image building toolchain. This value is just
|
||||
# for constructing the right Docker image tag.
|
||||
PYTHON_VERSION: "2"
|
||||
# Activate our cachix cache for "binary substitution". This sets
|
||||
# up configuration tht lets Nix download something from the cache
|
||||
# instead of building it locally, if possible.
|
||||
cachix use "${CACHIX_NAME}"
|
||||
|
||||
- "checkout"
|
||||
|
||||
- "run":
|
||||
# The Nix package doesn't know how to do this part, unfortunately.
|
||||
name: "Generate version"
|
||||
command: |
|
||||
nix-shell \
|
||||
-p 'python3.withPackages (ps: [ ps.setuptools ])' \
|
||||
--run 'python setup.py update_version'
|
||||
|
||||
- "run":
|
||||
name: "Build Package"
|
||||
command: |
|
||||
source .circleci/lib.sh
|
||||
NIXPKGS=$(nixpkgs_flake_reference <<parameters.nixpkgs>>)
|
||||
cache_if_able nix build \
|
||||
--verbose \
|
||||
--print-build-logs \
|
||||
--cores "$DEPENDENCY_CORES" \
|
||||
--override-input nixpkgs "$NIXPKGS" \
|
||||
.#<<parameters.pythonVersion>>-tahoe-lafs
|
||||
|
||||
- steps: "<<parameters.buildSteps>>"
|
||||
|
@ -46,4 +46,8 @@ export PIP_FIND_LINKS="file://${WHEELHOUSE_PATH}"
|
||||
# setuptools 45 requires Python 3.5 or newer. Even though we upgraded pip
|
||||
# above, it may still not be able to get us a compatible version unless we
|
||||
# explicitly ask for one.
|
||||
"${PIP}" install --upgrade setuptools==44.0.0 wheel
|
||||
"${PIP}" install --upgrade setuptools wheel
|
||||
|
||||
# Just about every user of this image wants to use tox from the bootstrap
|
||||
# virtualenv so go ahead and install it now.
|
||||
"${PIP}" install "tox~=4.0"
|
||||
|
148
.circleci/lib.sh
Normal file
148
.circleci/lib.sh
Normal file
@ -0,0 +1,148 @@
|
||||
# CircleCI build environment looks like it has a zillion and a half cores.
|
||||
# Don't let Nix autodetect this high core count because it blows up memory
|
||||
# usage and fails the test run. Pick a number of cores that suits the build
|
||||
# environment we're paying for (the free one!).
|
||||
DEPENDENCY_CORES=3
|
||||
|
||||
# Once dependencies are built, we can allow some more concurrency for our own
|
||||
# test suite.
|
||||
UNITTEST_CORES=8
|
||||
|
||||
# Run a command, enabling cache writes to cachix if possible. The command is
|
||||
# accepted as a variable number of positional arguments (like argv).
|
||||
function cache_if_able() {
|
||||
# Dump some info about our build environment.
|
||||
describe_build
|
||||
|
||||
if is_cache_writeable; then
|
||||
# If the cache is available we'll use it. This lets fork owners set
|
||||
# up their own caching if they want.
|
||||
echo "Cachix credentials present; will attempt to write to cache."
|
||||
|
||||
# The `cachix watch-exec ...` does our cache population. When it sees
|
||||
# something added to the store (I guess) it pushes it to the named
|
||||
# cache.
|
||||
cachix watch-exec "${CACHIX_NAME}" -- "$@"
|
||||
else
|
||||
if is_cache_required; then
|
||||
echo "Required credentials (CACHIX_AUTH_TOKEN) are missing."
|
||||
return 1
|
||||
else
|
||||
echo "Cachix credentials missing; will not attempt cache writes."
|
||||
"$@"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function is_cache_writeable() {
|
||||
# We can only *push* to the cache if we have a CACHIX_AUTH_TOKEN. in-repo
|
||||
# jobs will get this from CircleCI configuration but jobs from forks may
|
||||
# not.
|
||||
[ -v CACHIX_AUTH_TOKEN ]
|
||||
}
|
||||
|
||||
function is_cache_required() {
|
||||
# If we're building in tahoe-lafs/tahoe-lafs then we must use the cache.
|
||||
# If we're building anything from a fork then we're allowed to not have
|
||||
# the credentials.
|
||||
is_upstream
|
||||
}
|
||||
|
||||
# Return success if the origin of this build is the tahoe-lafs/tahoe-lafs
|
||||
# repository itself (and so we expect to have cache credentials available),
|
||||
# failure otherwise.
|
||||
#
|
||||
# See circleci.txt for notes about how this determination is made.
|
||||
function is_upstream() {
|
||||
# CIRCLE_PROJECT_USERNAME is set to the org the build is happening for.
|
||||
# If a PR targets a fork of the repo then this is set to something other
|
||||
# than "tahoe-lafs".
|
||||
[ "$CIRCLE_PROJECT_USERNAME" == "tahoe-lafs" ] &&
|
||||
|
||||
# CIRCLE_BRANCH is set to the real branch name for in-repo PRs and
|
||||
# "pull/NNNN" for pull requests from forks.
|
||||
#
|
||||
# CIRCLE_PULL_REQUESTS is set to a comma-separated list of the full
|
||||
# URLs of the PR pages which share an underlying branch, with one of
|
||||
# them ended with that same "pull/NNNN" for PRs from forks.
|
||||
! any_element_endswith "/$CIRCLE_BRANCH" "," "$CIRCLE_PULL_REQUESTS"
|
||||
}
|
||||
|
||||
# Return success if splitting $3 on $2 results in an array with any element
|
||||
# that ends with $1, failure otherwise.
|
||||
function any_element_endswith() {
|
||||
suffix=$1
|
||||
shift
|
||||
|
||||
sep=$1
|
||||
shift
|
||||
|
||||
haystack=$1
|
||||
shift
|
||||
|
||||
IFS="${sep}" read -r -a elements <<< "$haystack"
|
||||
for elem in "${elements[@]}"; do
|
||||
if endswith "$suffix" "$elem"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Return success if $2 ends with $1, failure otherwise.
|
||||
function endswith() {
|
||||
suffix=$1
|
||||
shift
|
||||
|
||||
haystack=$1
|
||||
shift
|
||||
|
||||
case "$haystack" in
|
||||
*${suffix})
|
||||
return 0
|
||||
;;
|
||||
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function describe_build() {
|
||||
echo "Building PR for user/org: ${CIRCLE_PROJECT_USERNAME}"
|
||||
echo "Building branch: ${CIRCLE_BRANCH}"
|
||||
if is_upstream; then
|
||||
echo "Upstream build."
|
||||
else
|
||||
echo "Non-upstream build."
|
||||
fi
|
||||
if is_cache_required; then
|
||||
echo "Cache is required."
|
||||
else
|
||||
echo "Cache not required."
|
||||
fi
|
||||
if is_cache_writeable; then
|
||||
echo "Cache is writeable."
|
||||
else
|
||||
echo "Cache not writeable."
|
||||
fi
|
||||
}
|
||||
|
||||
# Inspect the flake input metadata for an input of a given name and return the
|
||||
# revision at which that input is pinned. If the input does not exist then
|
||||
# return garbage (probably "null").
|
||||
read_input_revision() {
|
||||
input_name=$1
|
||||
shift
|
||||
|
||||
nix flake metadata --json | jp --unquoted 'locks.nodes."'"$input_name"'".locked.rev'
|
||||
}
|
||||
|
||||
# Return a flake reference that refers to a certain revision of nixpkgs. The
|
||||
# certain revision is the revision to which the specified input is pinned.
|
||||
nixpkgs_flake_reference() {
|
||||
input_name=$1
|
||||
shift
|
||||
|
||||
echo "github:NixOS/nixpkgs?rev=$(read_input_revision $input_name)"
|
||||
}
|
@ -3,18 +3,6 @@
|
||||
# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
|
||||
set -euxo pipefail
|
||||
|
||||
# Basic Python packages that you just need to have around to do anything,
|
||||
# practically speaking.
|
||||
BASIC_DEPS="pip wheel"
|
||||
|
||||
# Python packages we need to support the test infrastructure. *Not* packages
|
||||
# Tahoe-LAFS itself (implementation or test suite) need.
|
||||
TEST_DEPS="tox codecov"
|
||||
|
||||
# Python packages we need to generate test reports for CI infrastructure.
|
||||
# *Not* packages Tahoe-LAFS itself (implement or test suite) need.
|
||||
REPORTING_DEPS="python-subunit junitxml subunitreporter"
|
||||
|
||||
# The filesystem location of the wheelhouse which we'll populate with wheels
|
||||
# for all of our dependencies.
|
||||
WHEELHOUSE_PATH="$1"
|
||||
@ -41,15 +29,5 @@ export PIP_FIND_LINKS="file://${WHEELHOUSE_PATH}"
|
||||
LANG="en_US.UTF-8" "${PIP}" \
|
||||
wheel \
|
||||
--wheel-dir "${WHEELHOUSE_PATH}" \
|
||||
"${PROJECT_ROOT}"[test] \
|
||||
${BASIC_DEPS} \
|
||||
${TEST_DEPS} \
|
||||
${REPORTING_DEPS}
|
||||
|
||||
# Not strictly wheelhouse population but ... Note we omit basic deps here.
|
||||
# They're in the wheelhouse if Tahoe-LAFS wants to drag them in but it will
|
||||
# have to ask.
|
||||
"${PIP}" \
|
||||
install \
|
||||
${TEST_DEPS} \
|
||||
${REPORTING_DEPS}
|
||||
"${PROJECT_ROOT}"[testenv] \
|
||||
"${PROJECT_ROOT}"[test]
|
||||
|
20
.circleci/rebuild-images.sh
Executable file
20
.circleci/rebuild-images.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Get your API token here:
|
||||
# https://app.circleci.com/settings/user/tokens
|
||||
API_TOKEN=$1
|
||||
shift
|
||||
|
||||
# Name the branch you want to trigger the build for
|
||||
BRANCH=$1
|
||||
shift
|
||||
|
||||
curl \
|
||||
--verbose \
|
||||
--request POST \
|
||||
--url https://circleci.com/api/v2/project/gh/tahoe-lafs/tahoe-lafs/pipeline \
|
||||
--header "Circle-Token: $API_TOKEN" \
|
||||
--header "content-type: application/json" \
|
||||
--data '{"branch":"'"$BRANCH"'","parameters":{"build-images":true,"run-tests":false}}'
|
@ -45,14 +45,15 @@ fi
|
||||
|
||||
# A prefix for the test command that ensure it will exit after no more than a
|
||||
# certain amount of time. Ideally, we would only enforce a "silent" period
|
||||
# timeout but there isn't obviously a ready-made tool for that. The test
|
||||
# suite only takes about 5 - 6 minutes on CircleCI right now. 15 minutes
|
||||
# seems like a moderately safe window.
|
||||
# timeout but there isn't obviously a ready-made tool for that. The unit test
|
||||
# suite only takes about 5 - 6 minutes on CircleCI right now. The integration
|
||||
# tests are a bit longer than that. 45 minutes seems like a moderately safe
|
||||
# window.
|
||||
#
|
||||
# This is primarily aimed at catching hangs on the PyPy job which runs for
|
||||
# about 21 minutes and then gets killed by CircleCI in a way that fails the
|
||||
# job and bypasses our "allowed failure" logic.
|
||||
TIMEOUT="timeout --kill-after 1m 15m"
|
||||
TIMEOUT="timeout --kill-after 1m 45m"
|
||||
|
||||
# Run the test suite as a non-root user. This is the expected usage some
|
||||
# small areas of the test suite assume non-root privileges (such as unreadable
|
||||
@ -78,9 +79,10 @@ else
|
||||
alternative="false"
|
||||
fi
|
||||
|
||||
WORKDIR=/tmp/tahoe-lafs.tox
|
||||
${TIMEOUT} ${BOOTSTRAP_VENV}/bin/tox \
|
||||
-c ${PROJECT_ROOT}/tox.ini \
|
||||
--workdir /tmp/tahoe-lafs.tox \
|
||||
--workdir "${WORKDIR}" \
|
||||
-e "${TAHOE_LAFS_TOX_ENVIRONMENT}" \
|
||||
${TAHOE_LAFS_TOX_ARGS} || "${alternative}"
|
||||
|
||||
@ -92,5 +94,6 @@ if [ -n "${ARTIFACTS}" ]; then
|
||||
|
||||
# Create a junitxml results area.
|
||||
mkdir -p "$(dirname "${JUNITXML}")"
|
||||
"${BOOTSTRAP_VENV}"/bin/subunit2junitxml < "${SUBUNIT2}" > "${JUNITXML}" || "${alternative}"
|
||||
|
||||
"${WORKDIR}/${TAHOE_LAFS_TOX_ENVIRONMENT}/bin/subunit2junitxml" < "${SUBUNIT2}" > "${JUNITXML}" || "${alternative}"
|
||||
fi
|
||||
|
@ -26,12 +26,7 @@ shift || :
|
||||
|
||||
# Tell pip where it can find any existing wheels.
|
||||
export PIP_FIND_LINKS="file://${WHEELHOUSE_PATH}"
|
||||
|
||||
# It is tempting to also set PIP_NO_INDEX=1 but (a) that will cause problems
|
||||
# between the time dependencies change and the images are re-built and (b) the
|
||||
# upcoming-deprecations job wants to install some dependencies from github and
|
||||
# it's awkward to get that done any earlier than the tox run. So, we don't
|
||||
# set it.
|
||||
export PIP_NO_INDEX="1"
|
||||
|
||||
# Get everything else installed in it, too.
|
||||
"${BOOTSTRAP_VENV}"/bin/tox \
|
||||
|
@ -19,7 +19,7 @@ skip_covered = True
|
||||
source =
|
||||
# It looks like this in the checkout
|
||||
src/
|
||||
# It looks like this in the Windows build environment
|
||||
# It looks like this in the GitHub Actions Windows build environment
|
||||
D:/a/tahoe-lafs/tahoe-lafs/.tox/py*-coverage/Lib/site-packages/
|
||||
# Although sometimes it looks like this instead. Also it looks like this on macOS.
|
||||
.tox/py*-coverage/lib/python*/site-packages/
|
||||
|
6
.github/CONTRIBUTING.rst
vendored
6
.github/CONTRIBUTING.rst
vendored
@ -18,3 +18,9 @@ Examples of contributions include:
|
||||
|
||||
Before authoring or reviewing a patch,
|
||||
please familiarize yourself with the `Coding Standards <https://tahoe-lafs.org/trac/tahoe-lafs/wiki/CodingStandards>`_ and the `Contributor Code of Conduct <../docs/CODE_OF_CONDUCT.md>`_.
|
||||
|
||||
|
||||
🥳 First Contribution?
|
||||
======================
|
||||
|
||||
If you are committing to Tahoe for the very first time, consider adding your name to our contributor list in `CREDITS <../CREDITS>`__
|
||||
|
209
.github/workflows/ci.yml
vendored
209
.github/workflows/ci.yml
vendored
@ -6,6 +6,33 @@ on:
|
||||
- "master"
|
||||
pull_request:
|
||||
|
||||
# At the start of each workflow run, GitHub creates a unique
|
||||
# GITHUB_TOKEN secret to use in the workflow. It is a good idea for
|
||||
# this GITHUB_TOKEN to have the minimum of permissions. See:
|
||||
#
|
||||
# - https://docs.github.com/en/actions/security-guides/automatic-token-authentication
|
||||
# - https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
#
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Control to what degree jobs in this workflow will run concurrently with
|
||||
# other instances of themselves.
|
||||
#
|
||||
# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency
|
||||
concurrency:
|
||||
# We want every revision on master to run the workflow completely.
|
||||
# "head_ref" is not set for the "push" event but it is set for the
|
||||
# "pull_request" event. If it is set then it is the name of the branch and
|
||||
# we can use it to make sure each branch has only one active workflow at a
|
||||
# time. If it is not set then we can compute a unique string that gives
|
||||
# every master/push workflow its own group.
|
||||
group: "${{ github.head_ref || format('{0}-{1}', github.run_number, github.run_attempt) }}"
|
||||
|
||||
# Then, we say that if a new workflow wants to start in the same group as a
|
||||
# running workflow, the running workflow should be cancelled.
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Tell Hypothesis which configuration we want it to use.
|
||||
TAHOE_LAFS_HYPOTHESIS_PROFILE: "ci"
|
||||
@ -17,73 +44,64 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- windows-latest
|
||||
- ubuntu-latest
|
||||
python-version:
|
||||
- 2.7
|
||||
- 3.6
|
||||
- 3.7
|
||||
- 3.8
|
||||
- 3.9
|
||||
include:
|
||||
# On macOS don't bother with 3.6-3.8, just to get faster builds.
|
||||
- os: macos-10.15
|
||||
python-version: 2.7
|
||||
- os: macos-latest
|
||||
python-version: 3.9
|
||||
- os: macos-12
|
||||
python-version: "3.12"
|
||||
# We only support PyPy on Linux at the moment.
|
||||
- os: ubuntu-latest
|
||||
python-version: "pypy-3.8"
|
||||
- os: ubuntu-latest
|
||||
python-version: "pypy-3.9"
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.12"
|
||||
- os: windows-latest
|
||||
python-version: "3.12"
|
||||
|
||||
steps:
|
||||
# See https://github.com/actions/checkout. A fetch-depth of 0
|
||||
# fetches all tags and branches.
|
||||
- name: Check out Tahoe-LAFS sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
# To use pip caching with GitHub Actions in an OS-independent
|
||||
# manner, we need `pip cache dir` command, which became
|
||||
# available since pip v20.1+. At the time of writing this,
|
||||
# GitHub Actions offers pip v20.3.3 for both ubuntu-latest and
|
||||
# windows-latest, and pip v20.3.1 for macos-latest.
|
||||
- name: Get pip cache directory
|
||||
id: pip-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
|
||||
# See https://github.com/actions/cache
|
||||
- name: Use pip cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
cache: 'pip' # caching pip dependencies
|
||||
|
||||
- name: Install Python packages
|
||||
run: |
|
||||
pip install --upgrade codecov tox tox-gh-actions setuptools
|
||||
pip install --upgrade tox tox-gh-actions setuptools
|
||||
pip list
|
||||
|
||||
- name: Display tool versions
|
||||
run: python misc/build_helpers/show-tool-versions.py
|
||||
|
||||
- name: Run tox for corresponding Python version
|
||||
if: ${{ !contains(matrix.os, 'windows') }}
|
||||
run: python -m tox
|
||||
|
||||
# On Windows, a non-blocking pipe might respond (when emulating Unix-y
|
||||
# API) with ENOSPC to indicate buffer full. Trial doesn't handle this
|
||||
# well, so it breaks test runs. To attempt to solve this, we pipe the
|
||||
# output through passthrough.py that will hopefully be able to do the right
|
||||
# thing by using Windows APIs.
|
||||
- name: Run tox for corresponding Python version
|
||||
if: ${{ contains(matrix.os, 'windows') }}
|
||||
run: |
|
||||
pip install twisted pywin32
|
||||
python -m tox | python misc/windows-enospc/passthrough.py
|
||||
|
||||
- name: Upload eliot.log
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: eliot.log
|
||||
path: eliot.log
|
||||
|
||||
- name: Upload trial log
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: test.log
|
||||
path: _trial_temp/test.log
|
||||
@ -92,25 +110,6 @@ jobs:
|
||||
# Action for this, as of Jan 2021 it does not support Python coverage
|
||||
# files - only lcov files. Therefore, we use coveralls-python, the
|
||||
# coveralls.io-supplied Python reporter, for this.
|
||||
#
|
||||
# It is coveralls-python 1.x that has maintained compatibility
|
||||
# with Python 2, while coveralls-python 3.x is compatible with
|
||||
# Python 3. Sadly we can't use them both in the same workflow.
|
||||
#
|
||||
# The two versions of coveralls-python are somewhat mutually
|
||||
# incompatible. Mixing these two different versions when
|
||||
# reporting coverage to coveralls.io will lead to grief, since
|
||||
# they get job IDs in different fashion. If we use both
|
||||
# versions of coveralls in the same workflow, the finalizing
|
||||
# step will be able to mark only part of the jobs as done, and
|
||||
# the other part will be left hanging, never marked as done: it
|
||||
# does not matter if we make an API call or `coveralls --finish`
|
||||
# to indicate that CI has finished running.
|
||||
#
|
||||
# So we try to use the newer coveralls-python that is available
|
||||
# via Python 3 (which is present in GitHub Actions tool cache,
|
||||
# even when we're running Python 2.7 tests) throughout this
|
||||
# workflow.
|
||||
- name: "Report Coverage to Coveralls"
|
||||
run: |
|
||||
pip3 install --upgrade coveralls==3.0.1
|
||||
@ -160,23 +159,23 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
# 22.04 has some issue with Tor at the moment:
|
||||
# https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3943
|
||||
- ubuntu-20.04
|
||||
- macos-12
|
||||
- windows-latest
|
||||
- ubuntu-latest
|
||||
python-version:
|
||||
- 2.7
|
||||
- 3.6
|
||||
- 3.9
|
||||
- "3.11"
|
||||
force-foolscap:
|
||||
- false
|
||||
include:
|
||||
# On macOS don't bother with 3.6, just to get faster builds.
|
||||
- os: macos-10.15
|
||||
python-version: 2.7
|
||||
- os: macos-latest
|
||||
python-version: 3.9
|
||||
|
||||
- os: ubuntu-20.04
|
||||
python-version: "3.12"
|
||||
force-foolscap: true
|
||||
steps:
|
||||
|
||||
- name: Install Tor [Ubuntu]
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
if: ${{ contains(matrix.os, 'ubuntu') }}
|
||||
run: sudo apt install tor
|
||||
|
||||
# TODO: See https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3744.
|
||||
@ -185,38 +184,24 @@ jobs:
|
||||
- name: Install Tor [macOS, ${{ matrix.python-version }} ]
|
||||
if: ${{ contains(matrix.os, 'macos') }}
|
||||
run: |
|
||||
brew extract --version 0.4.5.8 tor homebrew/cask
|
||||
brew install tor@0.4.5.8
|
||||
brew link --overwrite tor@0.4.5.8
|
||||
brew install tor
|
||||
|
||||
- name: Install Tor [Windows]
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: crazy-max/ghaction-chocolatey@v1
|
||||
uses: crazy-max/ghaction-chocolatey@v2
|
||||
with:
|
||||
args: install tor
|
||||
|
||||
- name: Check out Tahoe-LAFS sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Get pip cache directory
|
||||
id: pip-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
|
||||
- name: Use pip cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
cache: 'pip' # caching pip dependencies
|
||||
|
||||
- name: Install Python packages
|
||||
run: |
|
||||
@ -226,16 +211,28 @@ jobs:
|
||||
- name: Display tool versions
|
||||
run: python misc/build_helpers/show-tool-versions.py
|
||||
|
||||
- name: Run "Python 2 integration tests"
|
||||
if: ${{ matrix.python-version == '2.7' }}
|
||||
run: tox -e integration
|
||||
|
||||
- name: Run "Python 3 integration tests"
|
||||
if: ${{ matrix.python-version != '2.7' }}
|
||||
run: tox -e integration3
|
||||
if: "${{ !matrix.force-foolscap }}"
|
||||
env:
|
||||
# On macOS this is necessary to ensure unix socket paths for tor
|
||||
# aren't too long. On Windows tox won't pass it through so it has no
|
||||
# effect. On Linux it doesn't make a difference one way or another.
|
||||
TMPDIR: "/tmp"
|
||||
run: |
|
||||
tox -e integration
|
||||
|
||||
- name: Run "Python 3 integration tests (force Foolscap)"
|
||||
if: "${{ matrix.force-foolscap }}"
|
||||
env:
|
||||
# On macOS this is necessary to ensure unix socket paths for tor
|
||||
# aren't too long. On Windows tox won't pass it through so it has no
|
||||
# effect. On Linux it doesn't make a difference one way or another.
|
||||
TMPDIR: "/tmp"
|
||||
run: |
|
||||
tox -e integration -- --force-foolscap integration/
|
||||
|
||||
- name: Upload eliot.log in case of failure
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: integration.eliot.json
|
||||
@ -247,36 +244,24 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- macos-10.15
|
||||
- macos-12
|
||||
- windows-latest
|
||||
- ubuntu-latest
|
||||
python-version:
|
||||
- 2.7
|
||||
- 3.9
|
||||
|
||||
steps:
|
||||
|
||||
- name: Check out Tahoe-LAFS sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Get pip cache directory
|
||||
id: pip-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
|
||||
- name: Use pip cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
cache: 'pip' # caching pip dependencies
|
||||
|
||||
- name: Install Python packages
|
||||
run: |
|
||||
@ -294,7 +279,7 @@ jobs:
|
||||
run: dist/Tahoe-LAFS/tahoe --version
|
||||
|
||||
- name: Upload PyInstaller package
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Tahoe-LAFS-${{ matrix.os }}-Python-${{ matrix.python-version }}
|
||||
path: dist/Tahoe-LAFS-*-*.*
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -29,8 +29,7 @@ zope.interface-*.egg
|
||||
.pc
|
||||
|
||||
/src/allmydata/test/plugins/dropin.cache
|
||||
/_trial_temp*
|
||||
/_test_memory/
|
||||
**/_trial_temp*
|
||||
/tmp*
|
||||
/*.patch
|
||||
/dist/
|
||||
@ -54,3 +53,5 @@ zope.interface-*.egg
|
||||
# This is the plaintext of the private environment needed for some CircleCI
|
||||
# operations. It's never supposed to be checked in.
|
||||
secret-env-plain
|
||||
|
||||
.ruff_cache
|
10
.readthedocs.yaml
Normal file
10
.readthedocs.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.10"
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
18
.ruff.toml
Normal file
18
.ruff.toml
Normal file
@ -0,0 +1,18 @@
|
||||
select = [
|
||||
# Pyflakes checks
|
||||
"F",
|
||||
# Prohibit tabs:
|
||||
"W191",
|
||||
# No trailing whitespace:
|
||||
"W291",
|
||||
"W293",
|
||||
# Make sure we bind closure variables in a loop (equivalent to pylint
|
||||
# cell-var-from-loop):
|
||||
"B023",
|
||||
# Don't silence exceptions in finally by accident:
|
||||
"B012",
|
||||
# Don't use mutable default arguments:
|
||||
"B006",
|
||||
# Errors from PyLint:
|
||||
"PLE",
|
||||
]
|
24
CREDITS
24
CREDITS
@ -240,3 +240,27 @@ N: Lukas Pirl
|
||||
E: tahoe@lukas-pirl.de
|
||||
W: http://lukas-pirl.de
|
||||
D: Buildslaves (Debian, Fedora, CentOS; 2016-2021)
|
||||
|
||||
N: Anxhelo Lushka
|
||||
E: anxhelo1995@gmail.com
|
||||
D: Web site design and updates
|
||||
|
||||
N: Fon E. Noel
|
||||
E: fenn25.fn@gmail.com
|
||||
D: bug-fixes and refactoring
|
||||
|
||||
N: Jehad Baeth
|
||||
E: jehad@leastauthority.com
|
||||
D: Documentation improvement
|
||||
|
||||
N: May-Lee Sia
|
||||
E: mayleesia@gmail.com
|
||||
D: Community-manager and documentation improvements
|
||||
|
||||
N: Yash Nayani
|
||||
E: yashaswi.nram@gmail.com
|
||||
D: Installation Guide improvements
|
||||
|
||||
N: Florian Sesser
|
||||
E: florian@private.storage
|
||||
D: OpenMetrics support
|
10
Dockerfile
10
Dockerfile
@ -1,10 +0,0 @@
|
||||
FROM python:2.7
|
||||
|
||||
ADD . /tahoe-lafs
|
||||
RUN \
|
||||
cd /tahoe-lafs && \
|
||||
git pull --depth=100 && \
|
||||
pip install . && \
|
||||
rm -rf ~/.cache/
|
||||
|
||||
WORKDIR /root
|
@ -1,25 +0,0 @@
|
||||
FROM debian:9
|
||||
LABEL maintainer "gordon@leastauthority.com"
|
||||
RUN apt-get update
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get -yq upgrade
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get -yq install build-essential python-dev libffi-dev libssl-dev python-virtualenv git
|
||||
RUN \
|
||||
git clone https://github.com/tahoe-lafs/tahoe-lafs.git /root/tahoe-lafs; \
|
||||
cd /root/tahoe-lafs; \
|
||||
virtualenv --python=python2.7 venv; \
|
||||
./venv/bin/pip install --upgrade setuptools; \
|
||||
./venv/bin/pip install --editable .; \
|
||||
./venv/bin/tahoe --version;
|
||||
RUN \
|
||||
cd /root; \
|
||||
mkdir /root/.tahoe-client; \
|
||||
mkdir /root/.tahoe-introducer; \
|
||||
mkdir /root/.tahoe-server;
|
||||
RUN /root/tahoe-lafs/venv/bin/tahoe create-introducer --location=tcp:introducer:3458 --port=tcp:3458 /root/.tahoe-introducer
|
||||
RUN /root/tahoe-lafs/venv/bin/tahoe start /root/.tahoe-introducer
|
||||
RUN /root/tahoe-lafs/venv/bin/tahoe create-node --location=tcp:server:3457 --port=tcp:3457 --introducer=$(cat /root/.tahoe-introducer/private/introducer.furl) /root/.tahoe-server
|
||||
RUN /root/tahoe-lafs/venv/bin/tahoe create-client --webport=3456 --introducer=$(cat /root/.tahoe-introducer/private/introducer.furl) --basedir=/root/.tahoe-client --shares-needed=1 --shares-happy=1 --shares-total=1
|
||||
VOLUME ["/root/.tahoe-client", "/root/.tahoe-server", "/root/.tahoe-introducer"]
|
||||
EXPOSE 3456 3457 3458
|
||||
ENTRYPOINT ["/root/tahoe-lafs/venv/bin/tahoe"]
|
||||
CMD []
|
@ -4,7 +4,7 @@ include relnotes.txt
|
||||
include Dockerfile
|
||||
include tox.ini .appveyor.yml .travis.yml
|
||||
include .coveragerc
|
||||
recursive-include src *.xhtml *.js *.png *.css *.svg *.txt
|
||||
recursive-include src *.xhtml *.js *.png *.css *.svg *.txt *.yaml
|
||||
graft docs
|
||||
graft misc
|
||||
graft static
|
||||
|
95
Makefile
95
Makefile
@ -17,7 +17,7 @@ PYTHON=python
|
||||
export PYTHON
|
||||
PYFLAKES=flake8
|
||||
export PYFLAKES
|
||||
VIRTUAL_ENV=./.tox/py27
|
||||
VIRTUAL_ENV=./.tox/py37
|
||||
SOURCES=src/allmydata static misc setup.py
|
||||
APPNAME=tahoe-lafs
|
||||
TEST_SUITE=allmydata
|
||||
@ -35,7 +35,7 @@ test: .tox/create-venvs.log
|
||||
# Run codechecks first since it takes the least time to report issues early.
|
||||
tox --develop -e codechecks
|
||||
# Run all the test environments in parallel to reduce run-time
|
||||
tox --develop -p auto -e 'py27,py36,pypy27'
|
||||
tox --develop -p auto -e 'py37'
|
||||
.PHONY: test-venv-coverage
|
||||
## Run all tests with coverage collection and reporting.
|
||||
test-venv-coverage:
|
||||
@ -51,7 +51,7 @@ test-venv-coverage:
|
||||
.PHONY: test-py3-all
|
||||
## Run all tests under Python 3
|
||||
test-py3-all: .tox/create-venvs.log
|
||||
tox --develop -e py36 allmydata
|
||||
tox --develop -e py37 allmydata
|
||||
|
||||
# This is necessary only if you want to automatically produce a new
|
||||
# _version.py file from the current git history (without doing a build).
|
||||
@ -136,37 +136,12 @@ count-lines:
|
||||
# Here is a list of testing tools that can be run with 'python' from a
|
||||
# virtualenv in which Tahoe has been installed. There used to be Makefile
|
||||
# targets for each, but the exact path to a suitable python is now up to the
|
||||
# developer. But as a hint, after running 'tox', ./.tox/py27/bin/python will
|
||||
# developer. But as a hint, after running 'tox', ./.tox/py37/bin/python will
|
||||
# probably work.
|
||||
|
||||
# src/allmydata/test/bench_dirnode.py
|
||||
|
||||
|
||||
# The check-speed and check-grid targets are disabled, since they depend upon
|
||||
# the pre-located $(TAHOE) executable that was removed when we switched to
|
||||
# tox. They will eventually be resurrected as dedicated tox environments.
|
||||
|
||||
# The check-speed target uses a pre-established client node to run a canned
|
||||
# set of performance tests against a test network that is also
|
||||
# pre-established (probably on a remote machine). Provide it with the path to
|
||||
# a local directory where this client node has been created (and populated
|
||||
# with the necessary FURLs of the test network). This target will start that
|
||||
# client with the current code and then run the tests. Afterwards it will
|
||||
# stop the client.
|
||||
#
|
||||
# The 'sleep 5' is in there to give the new client a chance to connect to its
|
||||
# storageservers, since check_speed.py has no good way of doing that itself.
|
||||
|
||||
##.PHONY: check-speed
|
||||
##check-speed: .built
|
||||
## if [ -z '$(TESTCLIENTDIR)' ]; then exit 1; fi
|
||||
## @echo "stopping any leftover client code"
|
||||
## -$(TAHOE) stop $(TESTCLIENTDIR)
|
||||
## $(TAHOE) start $(TESTCLIENTDIR)
|
||||
## sleep 5
|
||||
## $(TAHOE) @src/allmydata/test/check_speed.py $(TESTCLIENTDIR)
|
||||
## $(TAHOE) stop $(TESTCLIENTDIR)
|
||||
|
||||
# The check-grid target also uses a pre-established client node, along with a
|
||||
# long-term directory that contains some well-known files. See the docstring
|
||||
# in src/allmydata/test/check_grid.py to see how to set this up.
|
||||
@ -195,12 +170,11 @@ test-clean:
|
||||
# Use 'make distclean' instead to delete all generated files.
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf build _trial_temp _test_memory .built
|
||||
rm -rf build _trial_temp .built
|
||||
rm -f `find src *.egg -name '*.so' -or -name '*.pyc'`
|
||||
rm -rf support dist
|
||||
rm -rf `ls -d *.egg | grep -vEe"setuptools-|setuptools_darcs-|darcsver-"`
|
||||
rm -rf *.pyc
|
||||
rm -f bin/tahoe bin/tahoe.pyscript
|
||||
rm -f *.pkg
|
||||
|
||||
.PHONY: distclean
|
||||
@ -250,3 +224,62 @@ src/allmydata/_version.py:
|
||||
|
||||
.tox/create-venvs.log: tox.ini setup.py
|
||||
tox --notest -p all | tee -a "$(@)"
|
||||
|
||||
|
||||
# to make a new release:
|
||||
# - create a ticket for the release in Trac
|
||||
# - ensure local copy is up-to-date
|
||||
# - create a branch like "XXXX.release" from up-to-date master
|
||||
# - in the branch, run "make release"
|
||||
# - run "make release-test"
|
||||
# - perform any other sanity-checks on the release
|
||||
# - run "make release-upload"
|
||||
# Note that several commands below hard-code "meejah"; if you are
|
||||
# someone else please adjust them.
|
||||
release:
|
||||
@echo "Is checkout clean?"
|
||||
git diff-files --quiet
|
||||
git diff-index --quiet --cached HEAD --
|
||||
|
||||
@echo "Clean docs build area"
|
||||
rm -rf docs/_build/
|
||||
|
||||
@echo "Install required build software"
|
||||
python3 -m pip install --editable .[build]
|
||||
|
||||
@echo "Test README"
|
||||
python3 setup.py check -r -s
|
||||
|
||||
@echo "Update NEWS"
|
||||
python3 -m towncrier build --yes --version `python3 misc/build_helpers/update-version.py --no-tag`
|
||||
git add -u
|
||||
git commit -m "update NEWS for release"
|
||||
|
||||
# note that this always bumps the "middle" number, e.g. from 1.17.1 -> 1.18.0
|
||||
# and produces a tag into the Git repository
|
||||
@echo "Bump version and create tag"
|
||||
python3 misc/build_helpers/update-version.py
|
||||
|
||||
@echo "Build and sign wheel"
|
||||
python3 setup.py bdist_wheel
|
||||
gpg --pinentry=loopback -u meejah@meejah.ca --armor --detach-sign dist/tahoe_lafs-`git describe | cut -b 12-`-py3-none-any.whl
|
||||
ls dist/*`git describe | cut -b 12-`*
|
||||
|
||||
@echo "Build and sign source-dist"
|
||||
python3 setup.py sdist
|
||||
gpg --pinentry=loopback -u meejah@meejah.ca --armor --detach-sign dist/tahoe-lafs-`git describe | cut -b 12-`.tar.gz
|
||||
ls dist/*`git describe | cut -b 12-`*
|
||||
|
||||
# basically just a bare-minimum smoke-test that it installs and runs
|
||||
release-test:
|
||||
gpg --verify dist/tahoe-lafs-`git describe | cut -b 12-`.tar.gz.asc
|
||||
gpg --verify dist/tahoe_lafs-`git describe | cut -b 12-`-py3-none-any.whl.asc
|
||||
virtualenv testmf_venv
|
||||
testmf_venv/bin/pip install dist/tahoe_lafs-`git describe | cut -b 12-`-py3-none-any.whl
|
||||
testmf_venv/bin/tahoe --version
|
||||
rm -rf testmf_venv
|
||||
|
||||
release-upload:
|
||||
scp dist/*`git describe | cut -b 12-`* meejah@tahoe-lafs.org:/home/source/downloads
|
||||
git push origin_push tahoe-lafs-`git describe | cut -b 12-`
|
||||
twine upload dist/tahoe_lafs-`git describe | cut -b 12-`-py3-none-any.whl dist/tahoe_lafs-`git describe | cut -b 12-`-py3-none-any.whl.asc dist/tahoe-lafs-`git describe | cut -b 12-`.tar.gz dist/tahoe-lafs-`git describe | cut -b 12-`.tar.gz.asc
|
||||
|
18
README.rst
18
README.rst
@ -53,12 +53,11 @@ For more detailed instructions, read `Installing Tahoe-LAFS <docs/Installation/i
|
||||
|
||||
Once ``tahoe --version`` works, see `How to Run Tahoe-LAFS <docs/running.rst>`__ to learn how to set up your first Tahoe-LAFS node.
|
||||
|
||||
🐍 Python 3 Support
|
||||
--------------------
|
||||
🐍 Python 2
|
||||
-----------
|
||||
|
||||
Python 3 support has been introduced starting with Tahoe-LAFS 1.16.0, alongside Python 2.
|
||||
System administrators are advised to start running Tahoe on Python 3 and should expect Python 2 support to be dropped in a future version.
|
||||
Please, feel free to file issues if you run into bugs while running Tahoe on Python 3.
|
||||
Python 3.8 or later is required.
|
||||
If you are still using Python 2.7, use Tahoe-LAFS version 1.17.1.
|
||||
|
||||
|
||||
🤖 Issues
|
||||
@ -95,7 +94,14 @@ As a community-driven open source project, Tahoe-LAFS welcomes contributions of
|
||||
|
||||
- `Patch reviews <https://tahoe-lafs.org/trac/tahoe-lafs/wiki/PatchReviewProcess>`__
|
||||
|
||||
Before authoring or reviewing a patch, please familiarize yourself with the `Coding Standard <https://tahoe-lafs.org/trac/tahoe-lafs/wiki/CodingStandards>`__ and the `Contributor Code of Conduct <docs/CODE_OF_CONDUCT.md>`__.
|
||||
Before authoring or reviewing a patch, please familiarize yourself with the `Coding Standard <https://tahoe-lafs.org/trac/tahoe-lafs/wiki/CodingStandards>`__ and the `Contributor Code of Conduct <docs/CODE_OF_CONDUCT.md>`__.
|
||||
|
||||
|
||||
🥳 First Contribution?
|
||||
----------------------
|
||||
|
||||
If you are committing to Tahoe for the very first time, it's required that you add your name to our contributor list in `CREDITS <CREDITS>`__. Please ensure that this addition has it's own commit within your first contribution.
|
||||
|
||||
|
||||
🤝 Supporters
|
||||
--------------
|
||||
|
12
benchmarks/__init__.py
Normal file
12
benchmarks/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
"""
|
||||
pytest-based end-to-end benchmarks of Tahoe-LAFS.
|
||||
|
||||
Usage:
|
||||
|
||||
$ systemd-run --user --scope pytest benchmark --number-of-nodes=3
|
||||
|
||||
It's possible to pass --number-of-nodes multiple times.
|
||||
|
||||
The systemd-run makes sure the tests run in their own cgroup so we get CPU
|
||||
accounting correct.
|
||||
"""
|
150
benchmarks/conftest.py
Normal file
150
benchmarks/conftest.py
Normal file
@ -0,0 +1,150 @@
|
||||
"""
|
||||
pytest infrastructure for benchmarks.
|
||||
|
||||
The number of nodes is parameterized via a --number-of-nodes CLI option added
|
||||
to pytest.
|
||||
"""
|
||||
|
||||
import os
|
||||
from shutil import which, rmtree
|
||||
from tempfile import mkdtemp
|
||||
from contextlib import contextmanager
|
||||
from time import time
|
||||
|
||||
import pytest
|
||||
import pytest_twisted
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.defer import DeferredList, succeed
|
||||
|
||||
from allmydata.util.iputil import allocate_tcp_port
|
||||
|
||||
from integration.grid import Client, create_grid, create_flog_gatherer
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--number-of-nodes",
|
||||
action="append",
|
||||
default=[],
|
||||
type=int,
|
||||
help="list of number_of_nodes to benchmark against",
|
||||
)
|
||||
# Required to be compatible with integration.util code that we indirectly
|
||||
# depend on, but also might be useful.
|
||||
parser.addoption(
|
||||
"--force-foolscap",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="force_foolscap",
|
||||
help=(
|
||||
"If set, force Foolscap only for the storage protocol. "
|
||||
+ "Otherwise HTTP will be used."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
# Make number_of_nodes accessible as a parameterized fixture:
|
||||
if "number_of_nodes" in metafunc.fixturenames:
|
||||
metafunc.parametrize(
|
||||
"number_of_nodes",
|
||||
metafunc.config.getoption("number_of_nodes"),
|
||||
scope="session",
|
||||
)
|
||||
|
||||
|
||||
def port_allocator():
|
||||
port = allocate_tcp_port()
|
||||
return succeed(port)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def grid(request):
|
||||
"""
|
||||
Provides a new Grid with a single Introducer and flog-gathering process.
|
||||
|
||||
Notably does _not_ provide storage servers; use the storage_nodes
|
||||
fixture if your tests need a Grid that can be used for puts / gets.
|
||||
"""
|
||||
tmp_path = mkdtemp(prefix="tahoe-benchmark")
|
||||
request.addfinalizer(lambda: rmtree(tmp_path))
|
||||
flog_binary = which("flogtool")
|
||||
flog_gatherer = pytest_twisted.blockon(
|
||||
create_flog_gatherer(reactor, request, tmp_path, flog_binary)
|
||||
)
|
||||
g = pytest_twisted.blockon(
|
||||
create_grid(reactor, request, tmp_path, flog_gatherer, port_allocator)
|
||||
)
|
||||
return g
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def storage_nodes(grid, number_of_nodes):
|
||||
nodes_d = []
|
||||
for _ in range(number_of_nodes):
|
||||
nodes_d.append(grid.add_storage_node())
|
||||
|
||||
nodes_status = pytest_twisted.blockon(DeferredList(nodes_d))
|
||||
for ok, value in nodes_status:
|
||||
assert ok, "Storage node creation failed: {}".format(value)
|
||||
return grid.storage_servers
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def client_node(request, grid, storage_nodes, number_of_nodes) -> Client:
|
||||
"""
|
||||
Create a grid client node with number of shares matching number of nodes.
|
||||
"""
|
||||
client_node = pytest_twisted.blockon(
|
||||
grid.add_client(
|
||||
"client_node",
|
||||
needed=number_of_nodes,
|
||||
happy=number_of_nodes,
|
||||
total=number_of_nodes + 3, # Make sure FEC does some work
|
||||
)
|
||||
)
|
||||
print(f"Client node pid: {client_node.process.transport.pid}")
|
||||
return client_node
|
||||
|
||||
def get_cpu_time_for_cgroup():
|
||||
"""
|
||||
Get how many CPU seconds have been used in current cgroup so far.
|
||||
|
||||
Assumes we're running in a v2 cgroup.
|
||||
"""
|
||||
with open("/proc/self/cgroup") as f:
|
||||
cgroup = f.read().strip().split(":")[-1]
|
||||
assert cgroup.startswith("/")
|
||||
cgroup = cgroup[1:]
|
||||
cpu_stat = os.path.join("/sys/fs/cgroup", cgroup, "cpu.stat")
|
||||
with open(cpu_stat) as f:
|
||||
for line in f.read().splitlines():
|
||||
if line.startswith("usage_usec"):
|
||||
return int(line.split()[1]) / 1_000_000
|
||||
raise ValueError("Failed to find usage_usec")
|
||||
|
||||
|
||||
class Benchmarker:
|
||||
"""Keep track of benchmarking results."""
|
||||
|
||||
@contextmanager
|
||||
def record(self, capsys: pytest.CaptureFixture[str], name, **parameters):
|
||||
"""Record the timing of running some code, if it succeeds."""
|
||||
start_cpu = get_cpu_time_for_cgroup()
|
||||
start = time()
|
||||
yield
|
||||
elapsed = time() - start
|
||||
end_cpu = get_cpu_time_for_cgroup()
|
||||
elapsed_cpu = end_cpu - start_cpu
|
||||
# FOR now we just print the outcome:
|
||||
parameters = " ".join(f"{k}={v}" for (k, v) in parameters.items())
|
||||
with capsys.disabled():
|
||||
print(
|
||||
f"\nBENCHMARK RESULT: {name} {parameters} elapsed={elapsed:.3} (secs) CPU={elapsed_cpu:.3} (secs)\n"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tahoe_benchmarker():
|
||||
return Benchmarker()
|
66
benchmarks/test_cli.py
Normal file
66
benchmarks/test_cli.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""Benchmarks for minimal `tahoe` CLI interactions."""
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import pytest
|
||||
|
||||
from integration.util import cli
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def cli_alias(client_node):
|
||||
cli(client_node.process, "create-alias", "cli")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("file_size", [1000, 100_000, 1_000_000, 10_000_000])
|
||||
def test_get_put_files_sequentially(
|
||||
file_size,
|
||||
client_node,
|
||||
tahoe_benchmarker,
|
||||
number_of_nodes,
|
||||
capsys,
|
||||
):
|
||||
"""
|
||||
Upload 5 files with ``tahoe put`` and then download them with ``tahoe
|
||||
get``, measuring the latency of both operations. We do multiple uploads
|
||||
and downloads to try to reduce noise.
|
||||
"""
|
||||
DATA = b"0123456789" * (file_size // 10)
|
||||
|
||||
with tahoe_benchmarker.record(
|
||||
capsys, "cli-put-5-file-sequentially", file_size=file_size, number_of_nodes=number_of_nodes
|
||||
):
|
||||
for i in range(5):
|
||||
p = Popen(
|
||||
[
|
||||
"tahoe",
|
||||
"--node-directory",
|
||||
client_node.process.node_dir,
|
||||
"put",
|
||||
"-",
|
||||
f"cli:get_put_files_sequentially{i}",
|
||||
],
|
||||
stdin=PIPE,
|
||||
)
|
||||
p.stdin.write(DATA)
|
||||
p.stdin.write(str(i).encode("ascii"))
|
||||
p.stdin.close()
|
||||
assert p.wait() == 0
|
||||
|
||||
with tahoe_benchmarker.record(
|
||||
capsys, "cli-get-5-files-sequentially", file_size=file_size, number_of_nodes=number_of_nodes
|
||||
):
|
||||
for i in range(5):
|
||||
p = Popen(
|
||||
[
|
||||
"tahoe",
|
||||
"--node-directory",
|
||||
client_node.process.node_dir,
|
||||
"get",
|
||||
f"cli:get_put_files_sequentially{i}",
|
||||
"-",
|
||||
],
|
||||
stdout=PIPE,
|
||||
)
|
||||
assert p.stdout.read() == DATA + str(i).encode("ascii")
|
||||
assert p.wait() == 0
|
13
default.nix
Normal file
13
default.nix
Normal file
@ -0,0 +1,13 @@
|
||||
# This is the flake-compat glue code. It loads the flake and gives us its
|
||||
# outputs. This gives us backwards compatibility with pre-flake consumers.
|
||||
# All of the real action is in flake.nix.
|
||||
(import
|
||||
(
|
||||
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
|
||||
fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
||||
}
|
||||
)
|
||||
{ src = ./.; }
|
||||
).defaultNix.default
|
@ -1,49 +0,0 @@
|
||||
version: '2'
|
||||
services:
|
||||
client:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.dev
|
||||
volumes:
|
||||
- ./misc:/root/tahoe-lafs/misc
|
||||
- ./integration:/root/tahoe-lafs/integration
|
||||
- ./src:/root/tahoe-lafs/static
|
||||
- ./setup.cfg:/root/tahoe-lafs/setup.cfg
|
||||
- ./setup.py:/root/tahoe-lafs/setup.py
|
||||
ports:
|
||||
- "127.0.0.1:3456:3456"
|
||||
depends_on:
|
||||
- "introducer"
|
||||
- "server"
|
||||
entrypoint: /root/tahoe-lafs/venv/bin/tahoe
|
||||
command: ["run", "/root/.tahoe-client"]
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.dev
|
||||
volumes:
|
||||
- ./misc:/root/tahoe-lafs/misc
|
||||
- ./integration:/root/tahoe-lafs/integration
|
||||
- ./src:/root/tahoe-lafs/static
|
||||
- ./setup.cfg:/root/tahoe-lafs/setup.cfg
|
||||
- ./setup.py:/root/tahoe-lafs/setup.py
|
||||
ports:
|
||||
- "127.0.0.1:3457:3457"
|
||||
depends_on:
|
||||
- "introducer"
|
||||
entrypoint: /root/tahoe-lafs/venv/bin/tahoe
|
||||
command: ["run", "/root/.tahoe-server"]
|
||||
introducer:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.dev
|
||||
volumes:
|
||||
- ./misc:/root/tahoe-lafs/misc
|
||||
- ./integration:/root/tahoe-lafs/integration
|
||||
- ./src:/root/tahoe-lafs/static
|
||||
- ./setup.cfg:/root/tahoe-lafs/setup.cfg
|
||||
- ./setup.py:/root/tahoe-lafs/setup.py
|
||||
ports:
|
||||
- "127.0.0.1:3458:3458"
|
||||
entrypoint: /root/tahoe-lafs/venv/bin/tahoe
|
||||
command: ["run", "/root/.tahoe-introducer"]
|
@ -28,15 +28,15 @@ To install Tahoe-LAFS on Windows:
|
||||
3. Open the installer by double-clicking it. Select the **Add Python to PATH** check-box, then click **Install Now**.
|
||||
|
||||
4. Start PowerShell and enter the following command to verify python installation::
|
||||
|
||||
|
||||
python --version
|
||||
|
||||
5. Enter the following command to install Tahoe-LAFS::
|
||||
|
||||
|
||||
pip install tahoe-lafs
|
||||
|
||||
6. Verify installation by checking for the version::
|
||||
|
||||
|
||||
tahoe --version
|
||||
|
||||
If you want to hack on Tahoe's source code, you can install Tahoe in a ``virtualenv`` on your Windows Machine. To learn more, see :doc:`install-on-windows`.
|
||||
@ -56,13 +56,13 @@ If you are working on MacOS or a Linux distribution which does not have Tahoe-LA
|
||||
* **pip**: Most python installations already include `pip`. However, if your installation does not, see `pip installation <https://pip.pypa.io/en/stable/installing/>`_.
|
||||
|
||||
2. Install Tahoe-LAFS using pip::
|
||||
|
||||
|
||||
pip install tahoe-lafs
|
||||
|
||||
3. Verify installation by checking for the version::
|
||||
|
||||
|
||||
tahoe --version
|
||||
|
||||
If you are looking to hack on the source code or run pre-release code, we recommend you install Tahoe-LAFS on a `virtualenv` instance. To learn more, see :doc:`install-on-linux`.
|
||||
If you are looking to hack on the source code or run pre-release code, we recommend you install Tahoe-LAFS on a `virtualenv` instance. To learn more, see :doc:`install-on-linux`.
|
||||
|
||||
You can always write to the `tahoe-dev mailing list <https://lists.tahoe-lafs.org/mailman/listinfo/tahoe-dev>`_ or chat on the `Libera.chat IRC <irc://irc.libera.chat/%23tahoe-lafs>`_ if you are not able to get Tahoe-LAFS up and running on your deployment.
|
||||
|
@ -57,6 +57,18 @@ The key-value store is implemented by a grid of Tahoe-LAFS storage servers --
|
||||
user-space processes. Tahoe-LAFS storage clients communicate with the storage
|
||||
servers over TCP.
|
||||
|
||||
There are two supported protocols:
|
||||
|
||||
* Foolscap, the only supported protocol in release before v1.19.
|
||||
* HTTPS, new in v1.19.
|
||||
|
||||
By default HTTPS is enabled. When HTTPS is enabled on the server, the server
|
||||
transparently listens for both Foolscap and HTTPS on the same port. When it is
|
||||
disabled, the server only supports Foolscap. Clients can use either; by default
|
||||
they will use HTTPS when possible, falling back to I2p, but when configured
|
||||
appropriately they will only use Foolscap. At this time the only limitations of
|
||||
HTTPS is that I2P is not supported, so any usage of I2P only uses Foolscap.
|
||||
|
||||
Storage servers hold data in the form of "shares". Shares are encoded pieces
|
||||
of files. There are a configurable number of shares for each file, 10 by
|
||||
default. Normally, each share is stored on a separate server, but in some
|
||||
|
47
docs/check_running.py
Normal file
47
docs/check_running.py
Normal file
@ -0,0 +1,47 @@
|
||||
|
||||
import psutil
|
||||
import filelock
|
||||
|
||||
|
||||
def can_spawn_tahoe(pidfile):
|
||||
"""
|
||||
Determine if we can spawn a Tahoe-LAFS for the given pidfile. That
|
||||
pidfile may be deleted if it is stale.
|
||||
|
||||
:param pathlib.Path pidfile: the file to check, that is the Path
|
||||
to "running.process" in a Tahoe-LAFS configuration directory
|
||||
|
||||
:returns bool: True if we can spawn `tahoe run` here
|
||||
"""
|
||||
lockpath = pidfile.parent / (pidfile.name + ".lock")
|
||||
with filelock.FileLock(lockpath):
|
||||
try:
|
||||
with pidfile.open("r") as f:
|
||||
pid, create_time = f.read().strip().split(" ", 1)
|
||||
except FileNotFoundError:
|
||||
return True
|
||||
|
||||
# somewhat interesting: we have a pidfile
|
||||
pid = int(pid)
|
||||
create_time = float(create_time)
|
||||
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
# most interesting case: there _is_ a process running at the
|
||||
# recorded PID -- but did it just happen to get that PID, or
|
||||
# is it the very same one that wrote the file?
|
||||
if create_time == proc.create_time():
|
||||
# _not_ stale! another intance is still running against
|
||||
# this configuration
|
||||
return False
|
||||
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
# the file is stale
|
||||
pidfile.unlink()
|
||||
return True
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
print("can spawn?", can_spawn_tahoe(Path("running.process")))
|
@ -12,9 +12,6 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
@ -63,7 +60,7 @@ release = u'1.x'
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
language = "en"
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
|
@ -679,6 +679,13 @@ Client Configuration
|
||||
location to prefer their local servers so that they can maintain access to
|
||||
all of their uploads without using the internet.
|
||||
|
||||
``force_foolscap = (boolean, optional)``
|
||||
|
||||
If this is ``True``, the client will only connect to storage servers via
|
||||
Foolscap, regardless of whether they support HTTPS. If this is ``False``,
|
||||
the client will prefer HTTPS when it is available on the server. The default
|
||||
value is ``False``.
|
||||
|
||||
In addition,
|
||||
see :doc:`accepting-donations` for a convention for donating to storage server operators.
|
||||
|
||||
@ -796,6 +803,13 @@ Storage Server Configuration
|
||||
(i.e. ``BASEDIR/storage``), but it can be placed elsewhere. Relative paths
|
||||
will be interpreted relative to the node's base directory.
|
||||
|
||||
``force_foolscap = (boolean, optional)``
|
||||
|
||||
If this is ``True``, the node will expose the storage server via Foolscap
|
||||
only, with no support for HTTPS. If this is ``False``, the server will
|
||||
support both Foolscap and HTTPS on the same port. The default value is
|
||||
``False``.
|
||||
|
||||
In addition,
|
||||
see :doc:`accepting-donations` for a convention encouraging donations to storage server operators.
|
||||
|
||||
@ -980,6 +994,9 @@ the node will not use an Introducer at all.
|
||||
Such "introducerless" clients must be configured with static servers (described
|
||||
below), or they will not be able to upload and download files.
|
||||
|
||||
|
||||
.. _server_list:
|
||||
|
||||
Static Server Definitions
|
||||
=========================
|
||||
|
||||
|
@ -73,10 +73,15 @@ key on this list.
|
||||
~$1020
|
||||
1DskmM8uCvmvTKjPbeDgfmVsGifZCmxouG
|
||||
|
||||
* Aspiration contract (first phase, 2019)
|
||||
$300k-$350k
|
||||
* Aspiration contract
|
||||
$300k-$350k (first phase, 2019)
|
||||
$800k (second phase, 2020)
|
||||
1gDXYQNH4kCJ8Dk7kgiztfjNUaA1KJcHv
|
||||
|
||||
* OpenCollective development work (2023)
|
||||
~$260k
|
||||
1KZYr8UU2XjuEdSPzn2pF8eRPZZvffByDf
|
||||
|
||||
|
||||
Historical Donation Addresses
|
||||
=============================
|
||||
@ -104,17 +109,17 @@ This document is signed by the Tahoe-LAFS Release-Signing Key (GPG keyid
|
||||
(https://github.com/tahoe-lafs/tahoe-lafs.git) as `docs/donations.rst`.
|
||||
Both actions require access to secrets held closely by Tahoe developers.
|
||||
|
||||
signed: Brian Warner, 27-Dec-2018
|
||||
signed: Brian Warner, 25-Oct-2023
|
||||
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQEzBAEBCAAdFiEE405i0G0Oac/KQXn/veDTHWhmanoFAlwlrdsACgkQveDTHWhm
|
||||
anqEqQf/SdxMvI0+YbsZe+Gr/+lNWrNtfxAkjgLUZYRPmElZG6UKkNuPghXfsYRM
|
||||
71nRbgbn05jrke7AGlulxNplTxYP/5LQVf5K1nvTE7yPI/LBMudIpAbM3wPiLKSD
|
||||
qecrVZiqiIBPHWScyya91qirTHtJTJj39cs/N9937hD+Pm65paHWHDZhMkhStGH7
|
||||
05WtvD0G+fFuAgs04VDBz/XVQlPbngkmdKjIL06jpIAgzC3H9UGFcqe55HKY66jK
|
||||
W769TiRuGLLS07cOPqg8t2hPpE4wv9Gs02hfg1Jc656scsFuEkh5eMMj/MXcFsED
|
||||
8vwn16kjJk1fkeg+UofnXsHeHIJalQ==
|
||||
=/E+V
|
||||
iQEzBAEBCAAdFiEE405i0G0Oac/KQXn/veDTHWhmanoFAmU5YZMACgkQveDTHWhm
|
||||
anqt+ggAo2kulNmjrWA5VhqE8i6ckkxQMRVY4y0LAfiI0ho/505ZBZvpoh/Ze31x
|
||||
ZJj4DczHmZM+m3L+fZyubT4ldagYEojtwkYmxHAQz2DIV4PrdjsUQWyvkNcTBZWu
|
||||
y5mR5ATk3EYRa19xGEosWK1OzW2kgRbpAbznuWsdxxw9vNENBrolGRsyJqRQHCiV
|
||||
/4UkrGiOegaJSFMKy2dCyDF3ExD6wT9+fdqC5xDJZjhD+SUDJnD4oWLYLroj//v1
|
||||
sy4J+/ElNU9oaC0jDb9fx1ECk+u6B+YiaYlW/MrZNqzKCM/76yZ8sA2+ynsOHGtL
|
||||
bPFpLJjX6gBwHkMqvkWhsJEojxkFVQ==
|
||||
=gxlb
|
||||
-----END PGP SIGNATURE-----
|
||||
|
@ -131,3 +131,54 @@ developer summit.
|
||||
* acdfc299c35eed3bb27f7463ad8cdfcdcd4dcfd5184f290f87530c2be999de3e
|
||||
1.41401086 (@$714.16) = $1009.83, plus 0.000133 tx-fee
|
||||
|
||||
|
||||
Aspiration Contract
|
||||
-------------------
|
||||
|
||||
In December 2018, we entered into an agreement with a non-profit named
|
||||
Aspiration (https://aspirationtech.org/) to fund contractors for development
|
||||
work. They handle payroll, taxes, and oversight, in exchange for an 8%
|
||||
management fee. The first phase of work will extend through most of 2019.
|
||||
|
||||
* Recipient: Aspiration
|
||||
* Address: 1gDXYQNH4kCJ8Dk7kgiztfjNUaA1KJcHv
|
||||
|
||||
These txids record the transfers from the primary 1Pxi address to the
|
||||
Aspiration-specific 1gDXY subaddress. In some cases, leftover funds
|
||||
were swept back into the main 1Pxi address after the transfers were
|
||||
complete.
|
||||
|
||||
First phase, transfers performed 28-Dec-2018 - 31-Dec-2018, total 89
|
||||
BTC, about $350K.
|
||||
|
||||
* 95c68d488bd92e8c164195370aaa516dff05aa4d8c543d3fb8cfafae2b811e7a
|
||||
1.0 BTC plus 0.00002705 tx-fee
|
||||
* c0a5b8e3a63c56c4365d4c3ded0821bc1170f6351502849168bc34e30a0582d7
|
||||
89.0 BTC plus 0.00000633 tx-fee
|
||||
* 421cff5f398509aaf48951520738e0e63dfddf1157920c15bdc72c34e24cf1cf
|
||||
return 0.00005245 BTC to 1Pxi, less 0.00000211 tx-fee
|
||||
|
||||
In November 2020, we funded a second phase of the work: 51.38094 BTC,
|
||||
about $800K.
|
||||
|
||||
* 7558cbf3b24e8d835809d2d6f01a8ba229190102efdf36280d0639abaa488721
|
||||
1.0 BTC plus 0.00230766 tx-fee
|
||||
* 9c78ae6bb7db62cbd6be82fd52d50a2f015285b562f05de0ebfb0e5afc6fd285
|
||||
56.0 BTC plus 0.00057400 tx-fee
|
||||
* fbee4332e8c7ffbc9c1bcaee773f063550e589e58d350d14f6daaa473966c368
|
||||
returning 5.61906 BTC to 1Pxi, less 0.00012000 tx-fee
|
||||
|
||||
|
||||
Open Collective
|
||||
---------------
|
||||
|
||||
In August 2023, we started working with Open Collective to fund a
|
||||
grant covering development work performed over the last year.
|
||||
|
||||
* Recipient: Open Collective (US)
|
||||
* Address: 1KZYr8UU2XjuEdSPzn2pF8eRPZZvffByDf
|
||||
|
||||
The first phase transferred 7.5 BTC (about $260K).
|
||||
|
||||
* (txid)
|
||||
(amount)
|
||||
|
@ -47,8 +47,8 @@ servers must be configured with a way to first authenticate a user (confirm
|
||||
that a prospective client has a legitimate claim to whatever authorities we
|
||||
might grant a particular user), and second to decide what directory cap
|
||||
should be used as the root directory for a log-in by the authenticated user.
|
||||
A username and password can be used; as of Tahoe-LAFS v1.11, RSA or DSA
|
||||
public key authentication is also supported.
|
||||
As of Tahoe-LAFS v1.17,
|
||||
RSA/DSA public key authentication is the only supported mechanism.
|
||||
|
||||
Tahoe-LAFS provides two mechanisms to perform this user-to-cap mapping.
|
||||
The first (recommended) is a simple flat file with one account per line.
|
||||
@ -59,20 +59,14 @@ Creating an Account File
|
||||
|
||||
To use the first form, create a file (for example ``BASEDIR/private/accounts``)
|
||||
in which each non-comment/non-blank line is a space-separated line of
|
||||
(USERNAME, PASSWORD, ROOTCAP), like so::
|
||||
(USERNAME, KEY-TYPE, PUBLIC-KEY, ROOTCAP), like so::
|
||||
|
||||
% cat BASEDIR/private/accounts
|
||||
# This is a password line: username password cap
|
||||
alice password URI:DIR2:ioej8xmzrwilg772gzj4fhdg7a:wtiizszzz2rgmczv4wl6bqvbv33ag4kvbr6prz3u6w3geixa6m6a
|
||||
bob sekrit URI:DIR2:6bdmeitystckbl9yqlw7g56f4e:serp5ioqxnh34mlbmzwvkp3odehsyrr7eytt5f64we3k9hhcrcja
|
||||
|
||||
# This is a public key line: username keytype pubkey cap
|
||||
# (Tahoe-LAFS v1.11 or later)
|
||||
carol ssh-rsa AAAA... URI:DIR2:ovjy4yhylqlfoqg2vcze36dhde:4d4f47qko2xm5g7osgo2yyidi5m4muyo2vjjy53q4vjju2u55mfa
|
||||
|
||||
For public key authentication, the keytype may be either "ssh-rsa" or "ssh-dsa".
|
||||
To avoid ambiguity between passwords and public key types, a password cannot
|
||||
start with "ssh-".
|
||||
The key type may be either "ssh-rsa" or "ssh-dsa".
|
||||
|
||||
Now add an ``accounts.file`` directive to your ``tahoe.cfg`` file, as described in
|
||||
the next sections.
|
||||
|
@ -446,6 +446,21 @@ Creating a New Directory
|
||||
given, the directory's format is determined by the default mutable file
|
||||
format, as configured on the Tahoe-LAFS node responding to the request.
|
||||
|
||||
In addition, an optional "private-key=" argument is supported which, if given,
|
||||
specifies the underlying signing key to be used when creating the directory.
|
||||
This value must be a DER-encoded 2048-bit RSA private key in urlsafe base64
|
||||
encoding. (To convert an existing PEM-encoded RSA key file into the format
|
||||
required, the following commands may be used -- assuming a modern UNIX-like
|
||||
environment with common tools already installed:
|
||||
``openssl rsa -in key.pem -outform der | base64 -w 0 -i - | tr '+/' '-_'``)
|
||||
|
||||
Because this key can be used to derive the write capability for the
|
||||
associated directory, additional care should be taken to ensure that the key is
|
||||
unique, that it is kept confidential, and that it was derived from an
|
||||
appropriate (high-entropy) source of randomness. If this argument is omitted
|
||||
(the default behavior), Tahoe-LAFS will generate an appropriate signing key
|
||||
using the underlying operating system's source of entropy.
|
||||
|
||||
``POST /uri?t=mkdir-with-children``
|
||||
|
||||
Create a new directory, populated with a set of child nodes, and return its
|
||||
@ -453,7 +468,8 @@ Creating a New Directory
|
||||
any other directory: the returned write-cap is the only reference to it.
|
||||
|
||||
The format of the directory can be controlled with the format= argument in
|
||||
the query string, as described above.
|
||||
the query string and a signing key can be specified with the private-key=
|
||||
argument, as described above.
|
||||
|
||||
Initial children are provided as the body of the POST form (this is more
|
||||
efficient than doing separate mkdir and set_children operations). If the
|
||||
|
18
docs/gpg-setup.rst
Normal file
18
docs/gpg-setup.rst
Normal file
@ -0,0 +1,18 @@
|
||||
Preparing to Authenticate Release (Setting up GPG)
|
||||
--------------------------------------------------
|
||||
|
||||
In other to keep releases authentic it's required that releases are signed before being
|
||||
published. This ensure's that users of Tahoe are able to verify that the version of Tahoe
|
||||
they are using is coming from a trusted or at the very least known source.
|
||||
|
||||
The authentication is done using the ``GPG`` implementation of ``OpenGPG`` to be able to complete
|
||||
the release steps you would have to download the ``GPG`` software and setup a key(identity).
|
||||
|
||||
- `Download <https://www.gnupg.org/download/>`__ and install GPG for your operating system.
|
||||
- Generate a key pair using ``gpg --gen-key``. *Some questions would be asked to personalize your key configuration.*
|
||||
|
||||
You might take additional steps including:
|
||||
|
||||
- Setting up a revocation certificate (Incase you lose your secret key)
|
||||
- Backing up your key pair
|
||||
- Upload your fingerprint to a keyserver such as `openpgp.org <https://keys.openpgp.org/>`__
|
@ -40,6 +40,9 @@ preserving your privacy and security.
|
||||
:caption: Tahoe-LAFS in Depth
|
||||
|
||||
architecture
|
||||
gpg-setup
|
||||
servers
|
||||
managed-grid
|
||||
helper
|
||||
convergence-secret
|
||||
garbage-collection
|
||||
|
342
docs/managed-grid.rst
Normal file
342
docs/managed-grid.rst
Normal file
@ -0,0 +1,342 @@
|
||||
|
||||
|
||||
Managed Grid
|
||||
============
|
||||
|
||||
This document explains the "Grid Manager" concept and the
|
||||
`grid-manager` command. Someone operating a grid may choose to use a
|
||||
Grid Manager. Operators of storage-servers and clients will then be
|
||||
given additional configuration in this case.
|
||||
|
||||
|
||||
Overview and Motivation
|
||||
-----------------------
|
||||
|
||||
In a grid using an Introducer, a client will use any storage-server
|
||||
the Introducer announces (and the Introducer will announce any
|
||||
storage-server that connects to it). This means that anyone with the
|
||||
Introducer fURL can connect storage to the grid.
|
||||
|
||||
Sometimes, this is just what you want!
|
||||
|
||||
For some use-cases, though, you want to have clients only use certain
|
||||
servers. One case might be a "managed" grid, where some entity runs
|
||||
the grid; clients of this grid don't want their uploads to go to
|
||||
"unmanaged" storage if some other client decides to provide storage.
|
||||
|
||||
One way to limit which storage servers a client connects to is via the
|
||||
"server list" (:ref:`server_list`) (aka "Introducerless"
|
||||
mode). Clients are given static lists of storage-servers, and connect
|
||||
only to those. This means manually updating these lists if the storage
|
||||
servers change, however.
|
||||
|
||||
Another method is for clients to use `[client] peers.preferred=`
|
||||
configuration option (:ref:`Client Configuration`), which suffers
|
||||
from a similar disadvantage.
|
||||
|
||||
|
||||
Grid Manager
|
||||
------------
|
||||
|
||||
A "grid-manager" consists of some data defining a keypair (along with
|
||||
some other details) and Tahoe sub-commands to manipulate the data and
|
||||
produce certificates to give to storage-servers. Certificates assert
|
||||
the statement: "Grid Manager X suggests you use storage-server Y to
|
||||
upload shares to" (X and Y are public-keys). Such a certificate
|
||||
consists of:
|
||||
|
||||
- the version of the format the certificate conforms to (`1`)
|
||||
- the public-key of a storage-server
|
||||
- an expiry timestamp
|
||||
- a signature of the above
|
||||
|
||||
A client will always use any storage-server for downloads (expired
|
||||
certificate, or no certificate) because clients check the ciphertext
|
||||
and re-assembled plaintext against the keys in the capability;
|
||||
"grid-manager" certificates only control uploads.
|
||||
|
||||
Clients make use of this functionality by configuring one or more Grid Manager public keys.
|
||||
This tells the client to only upload to storage-servers that have a currently-valid certificate from any of the Grid Managers their client allows.
|
||||
In case none are configured, the default behavior (of using any storage server) prevails.
|
||||
|
||||
|
||||
Grid Manager Data Storage
|
||||
-------------------------
|
||||
|
||||
The data defining the grid-manager is stored in an arbitrary
|
||||
directory, which you indicate with the ``--config`` option (in the
|
||||
future, we may add the ability to store the data directly in a grid,
|
||||
at which time you may be able to pass a directory-capability to this
|
||||
option).
|
||||
|
||||
If you don't want to store the configuration on disk at all, you may
|
||||
use ``--config -`` (the last character is a dash) and write a valid
|
||||
JSON configuration to stdin.
|
||||
|
||||
All commands require the ``--config`` option and they all behave
|
||||
similarly for "data from stdin" versus "data from disk". A directory
|
||||
(and not a file) is used on disk because in that mode, each
|
||||
certificate issued is also stored alongside the configuration
|
||||
document; in "stdin / stdout" mode, an issued certificate is only
|
||||
ever available on stdout.
|
||||
|
||||
The configuration is a JSON document. It is subject to change as Grid
|
||||
Manager evolves. It contains a version number in the
|
||||
`grid_manager_config_version` key which will increment whenever the
|
||||
document schema changes.
|
||||
|
||||
|
||||
grid-manager create
|
||||
```````````````````
|
||||
|
||||
Create a new grid-manager.
|
||||
|
||||
If you specify ``--config -`` then a new grid-manager configuration is
|
||||
written to stdout. Otherwise, a new grid-manager is created in the
|
||||
directory specified by the ``--config`` option. It is an error if the
|
||||
directory already exists.
|
||||
|
||||
|
||||
grid-manager public-identity
|
||||
````````````````````````````
|
||||
|
||||
Print out a grid-manager's public key. This key is derived from the
|
||||
private-key of the grid-manager, so a valid grid-manager config must
|
||||
be given via ``--config``
|
||||
|
||||
This public key is what is put in clients' configuration to actually
|
||||
validate and use grid-manager certificates.
|
||||
|
||||
|
||||
grid-manager add
|
||||
````````````````
|
||||
|
||||
Takes two args: ``name pubkey``. The ``name`` is an arbitrary local
|
||||
identifier for the new storage node (also sometimes called "a petname"
|
||||
or "nickname"). The pubkey is the tahoe-encoded key from a ``node.pubkey``
|
||||
file in the storage-server's node directory (minus any
|
||||
whitespace). For example, if ``~/storage0`` contains a storage-node,
|
||||
you might do something like this::
|
||||
|
||||
grid-manager --config ./gm0 add storage0 $(cat ~/storage0/node.pubkey)
|
||||
|
||||
This adds a new storage-server to a Grid Manager's
|
||||
configuration. (Since it mutates the configuration, if you used
|
||||
``--config -`` the new configuration will be printed to stdout). The
|
||||
usefulness of the ``name`` is solely for reference within this Grid
|
||||
Manager.
|
||||
|
||||
|
||||
grid-manager list
|
||||
`````````````````
|
||||
|
||||
Lists all storage-servers that have previously been added using
|
||||
``grid-manager add``.
|
||||
|
||||
|
||||
grid-manager sign
|
||||
`````````````````
|
||||
|
||||
Takes two args: ``name expiry_days``. The ``name`` is a nickname used
|
||||
previously in a ``grid-manager add`` command and ``expiry_days`` is
|
||||
the number of days in the future when the certificate should expire.
|
||||
|
||||
Note that this mutates the state of the grid-manager if it is on disk,
|
||||
by adding this certificate to our collection of issued
|
||||
certificates. If you used ``--config -``, the certificate isn't
|
||||
persisted anywhere except to stdout (so if you wish to keep it
|
||||
somewhere, that is up to you).
|
||||
|
||||
This command creates a new "version 1" certificate for a
|
||||
storage-server (identified by its public key). The new certificate is
|
||||
printed to stdout. If you stored the config on disk, the new
|
||||
certificate will (also) be in a file named like ``alice.cert.0``.
|
||||
|
||||
|
||||
Enrolling a Storage Server: CLI
|
||||
-------------------------------
|
||||
|
||||
|
||||
tahoe admin add-grid-manager-cert
|
||||
`````````````````````````````````
|
||||
|
||||
- `--filename`: the file to read the cert from
|
||||
- `--name`: the name of this certificate
|
||||
|
||||
Import a "version 1" storage-certificate produced by a grid-manager A
|
||||
storage server may have zero or more such certificates installed; for
|
||||
now just one is sufficient. You will have to re-start your node after
|
||||
this. Subsequent announcements to the Introducer will include this
|
||||
certificate.
|
||||
|
||||
.. note::
|
||||
|
||||
This command will simply edit the `tahoe.cfg` file and direct you
|
||||
to re-start. In the Future(tm), we should consider (in exarkun's
|
||||
words):
|
||||
|
||||
"A python program you run as a new process" might not be the
|
||||
best abstraction to layer on top of the configuration
|
||||
persistence system, though. It's a nice abstraction for users
|
||||
(although most users would probably rather have a GUI) but it's
|
||||
not a great abstraction for automation. So at some point it
|
||||
may be better if there is CLI -> public API -> configuration
|
||||
persistence system. And maybe "public API" is even a network
|
||||
API for the storage server so it's equally easy to access from
|
||||
an agent implemented in essentially any language and maybe if
|
||||
the API is exposed by the storage node itself then this also
|
||||
gives you live-configuration-updates, avoiding the need for
|
||||
node restarts (not that this is the only way to accomplish
|
||||
this, but I think it's a good way because it avoids the need
|
||||
for messes like inotify and it supports the notion that the
|
||||
storage node process is in charge of its own configuration
|
||||
persistence system, not just one consumer among many ... which
|
||||
has some nice things going for it ... though how this interacts
|
||||
exactly with further node management automation might bear
|
||||
closer scrutiny).
|
||||
|
||||
|
||||
Enrolling a Storage Server: Config
|
||||
----------------------------------
|
||||
|
||||
You may edit the ``[storage]`` section of the ``tahoe.cfg`` file to
|
||||
turn on grid-management with ``grid_management = true``. You then must
|
||||
also provide a ``[grid_management_certificates]`` section in the
|
||||
config-file which lists ``name = path/to/certificate`` pairs.
|
||||
|
||||
These certificate files are issued by the ``grid-manager sign``
|
||||
command; these should be transmitted to the storage server operator
|
||||
who includes them in the config for the storage server. Relative paths
|
||||
are based from the node directory. Example::
|
||||
|
||||
[storage]
|
||||
grid_management = true
|
||||
|
||||
[grid_management_certificates]
|
||||
default = example_grid.cert
|
||||
|
||||
This will cause us to give this certificate to any Introducers we
|
||||
connect to (and subsequently, the Introducer will give the certificate
|
||||
out to clients).
|
||||
|
||||
|
||||
Enrolling a Client: Config
|
||||
--------------------------
|
||||
|
||||
You may instruct a Tahoe client to use only storage servers from given
|
||||
Grid Managers. If there are no such keys, any servers are used
|
||||
(but see https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3979). If
|
||||
there are one or more keys, the client will only upload to a storage
|
||||
server that has a valid certificate (from any of the keys).
|
||||
|
||||
To specify public-keys, add a ``[grid_managers]`` section to the
|
||||
config. This consists of ``name = value`` pairs where ``name`` is an
|
||||
arbitrary name and ``value`` is a public-key of a Grid
|
||||
Manager. Example::
|
||||
|
||||
[grid_managers]
|
||||
example_grid = pub-v0-vqimc4s5eflwajttsofisp5st566dbq36xnpp4siz57ufdavpvlq
|
||||
|
||||
See also https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3507 which
|
||||
proposes a command to edit the config.
|
||||
|
||||
|
||||
Example Setup of a New Managed Grid
|
||||
-----------------------------------
|
||||
|
||||
This example creates an actual grid, but it's all just on one machine
|
||||
with different "node directories" and a separate tahoe process for
|
||||
each node. Usually of course each storage server would be on a
|
||||
separate computer.
|
||||
|
||||
Note that we use the ``daemonize`` command in the following but that's
|
||||
only one way to handle "running a command in the background". You
|
||||
could instead run commands that start with ``daemonize ...`` in their
|
||||
own shell/terminal window or via something like ``systemd``
|
||||
|
||||
We'll store our Grid Manager configuration on disk, in
|
||||
``./gm0``. To initialize this directory::
|
||||
|
||||
grid-manager --config ./gm0 create
|
||||
|
||||
(If you already have a grid, you can :ref:`skip ahead <skip_ahead>`.)
|
||||
|
||||
First of all, create an Introducer. Note that we actually have to run
|
||||
it briefly before it creates the "Introducer fURL" we want for the
|
||||
next steps::
|
||||
|
||||
tahoe create-introducer --listen=tcp --port=5555 --location=tcp:localhost:5555 ./introducer
|
||||
daemonize tahoe -d introducer run
|
||||
|
||||
Next, we attach a couple of storage nodes::
|
||||
|
||||
tahoe create-node --introducer $(cat introducer/private/introducer.furl) --nickname storage0 --webport 6001 --location tcp:localhost:6003 --port 6003 ./storage0
|
||||
tahoe create-node --introducer $(cat introducer/private/introducer.furl) --nickname storage1 --webport 6101 --location tcp:localhost:6103 --port 6103 ./storage1
|
||||
daemonize tahoe -d storage0 run
|
||||
daemonize tahoe -d storage1 run
|
||||
|
||||
.. _skip_ahead:
|
||||
|
||||
We can now tell the Grid Manager about our new storage servers::
|
||||
|
||||
grid-manager --config ./gm0 add storage0 $(cat storage0/node.pubkey)
|
||||
grid-manager --config ./gm0 add storage1 $(cat storage1/node.pubkey)
|
||||
|
||||
To produce a new certificate for each node, we do this::
|
||||
|
||||
grid-manager --config ./gm0 sign storage0 > ./storage0/gridmanager.cert
|
||||
grid-manager --config ./gm0 sign storage1 > ./storage1/gridmanager.cert
|
||||
|
||||
Now, we want our storage servers to actually announce these
|
||||
certificates into the grid. We do this by adding some configuration
|
||||
(in ``tahoe.cfg``)::
|
||||
|
||||
[storage]
|
||||
grid_management = true
|
||||
|
||||
[grid_manager_certificates]
|
||||
default = gridmanager.cert
|
||||
|
||||
Add the above bit to each node's ``tahoe.cfg`` and re-start the
|
||||
storage nodes. (Alternatively, use the ``tahoe add-grid-manager``
|
||||
command).
|
||||
|
||||
Now try adding a new storage server ``storage2``. This client can join
|
||||
the grid just fine, and announce itself to the Introducer as providing
|
||||
storage::
|
||||
|
||||
tahoe create-node --introducer $(cat introducer/private/introducer.furl) --nickname storage2 --webport 6301 --location tcp:localhost:6303 --port 6303 ./storage2
|
||||
daemonize tahoe -d storage2 run
|
||||
|
||||
At this point any client will upload to any of these three
|
||||
storage-servers. Make a client "alice" and try!
|
||||
|
||||
::
|
||||
|
||||
tahoe create-client --introducer $(cat introducer/private/introducer.furl) --nickname alice --webport 6401 --shares-total=3 --shares-needed=2 --shares-happy=3 ./alice
|
||||
daemonize tahoe -d alice run
|
||||
tahoe -d alice put README.rst # prints out a read-cap
|
||||
find storage2/storage/shares # confirm storage2 has a share
|
||||
|
||||
Now we want to make Alice only upload to the storage servers that the
|
||||
grid-manager has given certificates to (``storage0`` and
|
||||
``storage1``). We need the grid-manager's public key to put in Alice's
|
||||
configuration::
|
||||
|
||||
grid-manager --config ./gm0 public-identity
|
||||
|
||||
Put the key printed out above into Alice's ``tahoe.cfg`` in section
|
||||
``client``::
|
||||
|
||||
[grid_managers]
|
||||
example_name = pub-v0-vqimc4s5eflwajttsofisp5st566dbq36xnpp4siz57ufdavpvlq
|
||||
|
||||
|
||||
Now, re-start the "alice" client. Since we made Alice's parameters
|
||||
require 3 storage servers to be reachable (``--happy=3``), all their
|
||||
uploads should now fail (so ``tahoe put`` will fail) because they
|
||||
won't use storage2 and thus can't "achieve happiness".
|
||||
|
||||
A proposal to expose more information about Grid Manager and
|
||||
certificate status in the Welcome page is discussed in
|
||||
https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3506
|
@ -82,8 +82,9 @@ network: A
|
||||
|
||||
memory footprint: N/K*A
|
||||
|
||||
notes: Tahoe-LAFS generates a new RSA keypair for each mutable file that it
|
||||
publishes to a grid. This takes up to 1 or 2 seconds on a typical desktop PC.
|
||||
notes:
|
||||
Tahoe-LAFS generates a new RSA keypair for each mutable file that it publishes to a grid.
|
||||
This takes around 100 milliseconds on a relatively high-end laptop from 2021.
|
||||
|
||||
Part of the process of encrypting, encoding, and uploading a mutable file to a
|
||||
Tahoe-LAFS grid requires that the entire file be in memory at once. For larger
|
||||
|
@ -14,4 +14,3 @@ index only lists the files that are in .rst format.
|
||||
:maxdepth: 2
|
||||
|
||||
leasedb
|
||||
http-storage-node-protocol
|
||||
|
@ -3,9 +3,8 @@
|
||||
Release Checklist
|
||||
=================
|
||||
|
||||
These instructions were produced while making the 1.15.0 release. They
|
||||
are based on the original instructions (in old revisions in the file
|
||||
`docs/how_to_make_a_tahoe-lafs_release.org`).
|
||||
This release checklist specifies a series of checks that anyone engaged in
|
||||
releasing a version of Tahoe should follow.
|
||||
|
||||
Any contributor can do the first part of the release preparation. Only
|
||||
certain contributors can perform other parts. These are the two main
|
||||
@ -13,9 +12,12 @@ sections of this checklist (and could be done by different people).
|
||||
|
||||
A final section describes how to announce the release.
|
||||
|
||||
This checklist is based on the original instructions (in old revisions in the file
|
||||
`docs/how_to_make_a_tahoe-lafs_release.org`).
|
||||
|
||||
|
||||
Any Contributor
|
||||
---------------
|
||||
===============
|
||||
|
||||
Anyone who can create normal PRs should be able to complete this
|
||||
portion of the release process.
|
||||
@ -32,13 +34,35 @@ Tuesday if you want to get anything in").
|
||||
|
||||
- Create a ticket for the release in Trac
|
||||
- Ticket number needed in next section
|
||||
- Making first release? See `GPG Setup Instructions <gpg-setup.rst>`__ to make sure you can sign releases. [One time setup]
|
||||
|
||||
Get a clean checkout
|
||||
````````````````````
|
||||
|
||||
The release proccess involves compressing source files and putting them in formats
|
||||
suitable for distribution such as ``.tar.gz`` and ``zip``. That said, it's neccesary to
|
||||
the release process begins with a clean checkout to avoid making a release with
|
||||
previously generated files.
|
||||
|
||||
- Inside the tahoe root dir run ``git clone . ../tahoe-release-x.x.x`` where (x.x.x is the release number such as 1.16.0).
|
||||
|
||||
.. note::
|
||||
The above command would create a new directory at the same level as your original clone named ``tahoe-release-x.x.x``. You can name this folder however you want but it would be a good
|
||||
practice to give it the release name. You MAY also discard this directory once the release
|
||||
process is complete.
|
||||
|
||||
Get into the release directory and install dependencies by running
|
||||
|
||||
- cd ../tahoe-release-x.x.x (assuming you are still in your original clone)
|
||||
- python -m venv venv
|
||||
- ./venv/bin/pip install --editable .[test]
|
||||
|
||||
|
||||
Create Branch and Apply Updates
|
||||
```````````````````````````````
|
||||
|
||||
- Create a branch for release-candidates (e.g. `XXXX.release-1.15.0.rc0`)
|
||||
- run `tox -e news` to produce a new NEWS.txt file (this does a commit)
|
||||
- Create a branch for the release/candidate (e.g. ``XXXX.release-1.16.0``)
|
||||
- run tox -e news to produce a new NEWS.txt file (this does a commit)
|
||||
- create the news for the release
|
||||
|
||||
- newsfragments/<ticket number>.minor
|
||||
@ -46,7 +70,7 @@ Create Branch and Apply Updates
|
||||
|
||||
- manually fix NEWS.txt
|
||||
|
||||
- proper title for latest release ("Release 1.15.0" instead of "Release ...post1432")
|
||||
- proper title for latest release ("Release 1.16.0" instead of "Release ...post1432")
|
||||
- double-check date (maybe release will be in the future)
|
||||
- spot-check the release notes (these come from the newsfragments
|
||||
files though so don't do heavy editing)
|
||||
@ -54,7 +78,7 @@ Create Branch and Apply Updates
|
||||
|
||||
- update "relnotes.txt"
|
||||
|
||||
- update all mentions of 1.14.0 -> 1.15.0
|
||||
- update all mentions of ``1.16.0`` to new and higher release version for example ``1.16.1``
|
||||
- update "previous release" statement and date
|
||||
- summarize major changes
|
||||
- commit it
|
||||
@ -63,14 +87,7 @@ Create Branch and Apply Updates
|
||||
|
||||
- change the value given for `version` from `OLD.post1` to `NEW.post1`
|
||||
|
||||
- update "CREDITS"
|
||||
|
||||
- are there any new contributors in this release?
|
||||
- one way: git log release-1.14.0.. | grep Author | sort | uniq
|
||||
- commit it
|
||||
|
||||
- update "docs/known_issues.rst" if appropriate
|
||||
- update "docs/Installation/install-tahoe.rst" references to the new release
|
||||
- Push the branch to github
|
||||
- Create a (draft) PR; this should trigger CI (note that github
|
||||
doesn't let you create a PR without some changes on the branch so
|
||||
@ -95,23 +112,33 @@ they will need to evaluate which contributors' signatures they trust.
|
||||
- (all steps above are completed)
|
||||
- sign the release
|
||||
|
||||
- git tag -s -u 0xE34E62D06D0E69CFCA4179FFBDE0D31D68666A7A -m "release Tahoe-LAFS-1.15.0rc0" tahoe-lafs-1.15.0rc0
|
||||
- (replace the key-id above with your own)
|
||||
- git tag -s -u 0xE34E62D06D0E69CFCA4179FFBDE0D31D68666A7A -m "release Tahoe-LAFS-1.16.0rc0" tahoe-lafs-1.16.0rc0
|
||||
|
||||
.. note::
|
||||
- Replace the key-id above with your own, which can simply be your email if it's attached to your fingerprint.
|
||||
- Don't forget to put the correct tag message and name. In this example, the tag message is "release Tahoe-LAFS-1.16.0rc0" and the tag name is ``tahoe-lafs-1.16.0rc0``
|
||||
|
||||
- build all code locally
|
||||
|
||||
- these should all pass:
|
||||
|
||||
- tox -e py27,codechecks,docs,integration
|
||||
- tox -e py37,codechecks,docs,integration
|
||||
|
||||
- these can fail (ideally they should not of course):
|
||||
|
||||
- tox -e deprecations,upcoming-deprecations
|
||||
|
||||
- clone to a clean, local checkout (to avoid extra files being included in the release)
|
||||
|
||||
- cd /tmp
|
||||
- git clone /home/meejah/src/tahoe-lafs
|
||||
|
||||
- build tarballs
|
||||
|
||||
- tox -e tarballs
|
||||
- confirm it at least exists:
|
||||
- ls dist/ | grep 1.15.0rc0
|
||||
- Confirm that release tarballs exist by runnig:
|
||||
|
||||
- ls dist/ | grep 1.16.0rc0
|
||||
|
||||
- inspect and test the tarballs
|
||||
|
||||
@ -120,14 +147,12 @@ they will need to evaluate which contributors' signatures they trust.
|
||||
|
||||
- when satisfied, sign the tarballs:
|
||||
|
||||
- gpg --pinentry=loopback --armor --detach-sign dist/tahoe_lafs-1.15.0rc0-py2-none-any.whl
|
||||
- gpg --pinentry=loopback --armor --detach-sign dist/tahoe_lafs-1.15.0rc0.tar.bz2
|
||||
- gpg --pinentry=loopback --armor --detach-sign dist/tahoe_lafs-1.15.0rc0.tar.gz
|
||||
- gpg --pinentry=loopback --armor --detach-sign dist/tahoe_lafs-1.15.0rc0.zip
|
||||
- gpg --pinentry=loopback --armor -u 0xE34E62D06D0E69CFCA4179FFBDE0D31D68666A7A --detach-sign dist/tahoe_lafs-1.16.0rc0-py2.py3-none-any.whl
|
||||
- gpg --pinentry=loopback --armor --detach-sign dist/tahoe_lafs-1.16.0rc0.tar.gz
|
||||
|
||||
|
||||
Privileged Contributor
|
||||
-----------------------
|
||||
======================
|
||||
|
||||
Steps in this portion require special access to keys or
|
||||
infrastructure. For example, **access to tahoe-lafs.org** to upload
|
||||
@ -155,24 +180,32 @@ need to be uploaded to https://tahoe-lafs.org in `~source/downloads`
|
||||
|
||||
- secure-copy all release artifacts to the download area on the
|
||||
tahoe-lafs.org host machine. `~source/downloads` on there maps to
|
||||
https://tahoe-lafs.org/downloads/ on the Web.
|
||||
- scp dist/*1.15.0* username@tahoe-lafs.org:/home/source/downloads
|
||||
https://tahoe-lafs.org/downloads/ on the Web:
|
||||
|
||||
- scp dist/*1.15.0* username@tahoe-lafs.org:/home/source/downloads
|
||||
|
||||
- the following developers have access to do this:
|
||||
|
||||
- exarkun
|
||||
- meejah
|
||||
- warner
|
||||
|
||||
Push the signed tag to the main repository:
|
||||
|
||||
- git push origin tahoe-lafs-1.17.1
|
||||
|
||||
For the actual release, the tarball and signature files need to be
|
||||
uploaded to PyPI as well.
|
||||
|
||||
- how to do this?
|
||||
- (original guide says only `twine upload dist/*`)
|
||||
- the following developers have access to do this:
|
||||
- ls dist/*1.19.0*
|
||||
- twine upload --username __token__ --password `cat SECRET-pypi-tahoe-publish-token` dist/*1.19.0*
|
||||
|
||||
The following developers have access to do this:
|
||||
|
||||
- warner
|
||||
- meejah
|
||||
- exarkun (partial?)
|
||||
- meejah (partial?)
|
||||
|
||||
|
||||
Announcing the Release Candidate
|
||||
````````````````````````````````
|
||||
|
4
docs/requirements.txt
Normal file
4
docs/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
sphinx
|
||||
docutils<0.18 # https://github.com/sphinx-doc/sphinx/issues/9788
|
||||
recommonmark
|
||||
sphinx_rtd_theme
|
@ -124,6 +124,35 @@ Tahoe-LAFS.
|
||||
.. _magic wormhole: https://magic-wormhole.io/
|
||||
|
||||
|
||||
Multiple Instances
|
||||
------------------
|
||||
|
||||
Running multiple instances against the same configuration directory isn't supported.
|
||||
This will lead to undefined behavior and could corrupt the configuration or state.
|
||||
|
||||
We attempt to avoid this situation with a "pidfile"-style file in the config directory called ``running.process``.
|
||||
There may be a parallel file called ``running.process.lock`` in existence.
|
||||
|
||||
The ``.lock`` file exists to make sure only one process modifies ``running.process`` at once.
|
||||
The lock file is managed by the `lockfile <https://pypi.org/project/lockfile/>`_ library.
|
||||
If you wish to make use of ``running.process`` for any reason you should also lock it and follow the semantics of lockfile.
|
||||
|
||||
If ``running.process`` exists then it contains the PID and the creation-time of the process.
|
||||
When no such file exists, there is no other process running on this configuration.
|
||||
If there is a ``running.process`` file, it may be a leftover file or it may indicate that another process is running against this config.
|
||||
To tell the difference, determine if the PID in the file exists currently.
|
||||
If it does, check the creation-time of the process versus the one in the file.
|
||||
If these match, there is another process currently running and using this config.
|
||||
Otherwise, the file is stale -- it should be removed before starting Tahoe-LAFS.
|
||||
|
||||
Some example Python code to check the above situations:
|
||||
|
||||
.. literalinclude:: check_running.py
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
A note about small grids
|
||||
------------------------
|
||||
|
||||
|
@ -267,7 +267,7 @@ How well does this design meet the goals?
|
||||
value, so there are no opportunities for staleness
|
||||
9. monotonicity: VERY: the single point of access also protects against
|
||||
retrograde motion
|
||||
|
||||
|
||||
|
||||
|
||||
Confidentiality leaks in the storage servers
|
||||
@ -332,8 +332,9 @@ MDMF design rules allow for efficient random-access reads from the middle of
|
||||
the file, which would give the index something useful to point at.
|
||||
|
||||
The current SDMF design generates a new RSA public/private keypair for each
|
||||
directory. This takes considerable time and CPU effort, generally one or two
|
||||
seconds per directory. We have designed (but not yet built) a DSA-based
|
||||
directory. This takes some time and CPU effort (around 100 milliseconds on a
|
||||
relatively high-end 2021 laptop) per directory.
|
||||
We have designed (but not yet built) a DSA-based
|
||||
mutable file scheme which will use shared parameters to reduce the
|
||||
directory-creation effort to a bare minimum (picking a random number instead
|
||||
of generating two random primes).
|
||||
@ -363,7 +364,7 @@ single child, looking up a single child) would require pulling or pushing a
|
||||
lot of unrelated data, increasing network overhead (and necessitating
|
||||
test-and-set semantics for the modification side, which increases the chances
|
||||
that a user operation will fail, making it more challenging to provide
|
||||
promises of atomicity to the user).
|
||||
promises of atomicity to the user).
|
||||
|
||||
It would also make it much more difficult to enable the delegation
|
||||
("sharing") of specific directories. Since each aggregate "realm" provides
|
||||
@ -469,4 +470,3 @@ Preventing delegation between communication parties is just as pointless as
|
||||
asking Bob to forget previously accessed files. However, there may be value
|
||||
to configuring the UI to ask Carol to not share files with Bob, or to
|
||||
removing all files from Bob's view at the same time his access is revoked.
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
Storage Node Protocol ("Great Black Swamp", "GBS")
|
||||
==================================================
|
||||
|
||||
The target audience for this document is Tahoe-LAFS developers.
|
||||
The target audience for this document is developers working on Tahoe-LAFS or on an alternate implementation intended to be interoperable.
|
||||
After reading this document,
|
||||
one should expect to understand how Tahoe-LAFS clients interact over the network with Tahoe-LAFS storage nodes.
|
||||
|
||||
@ -30,15 +30,15 @@ Glossary
|
||||
introducer
|
||||
a Tahoe-LAFS process at a known location configured to re-publish announcements about the location of storage servers
|
||||
|
||||
fURL
|
||||
:ref:`fURLs <fURLs>`
|
||||
a self-authenticating URL-like string which can be used to locate a remote object using the Foolscap protocol
|
||||
(the storage service is an example of such an object)
|
||||
|
||||
NURL
|
||||
:ref:`NURLs <NURLs>`
|
||||
a self-authenticating URL-like string almost exactly like a fURL but without being tied to Foolscap
|
||||
|
||||
swissnum
|
||||
a short random string which is part of a fURL and which acts as a shared secret to authorize clients to use a storage service
|
||||
a short random string which is part of a fURL/NURL and which acts as a shared secret to authorize clients to use a storage service
|
||||
|
||||
lease
|
||||
state associated with a share informing a storage server of the duration of storage desired by a client
|
||||
@ -64,6 +64,10 @@ Glossary
|
||||
lease renew secret
|
||||
a short secret string which storage servers required to be presented before allowing a particular lease to be renewed
|
||||
|
||||
The key words
|
||||
"MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL"
|
||||
in this document are to be interpreted as described in RFC 2119.
|
||||
|
||||
Motivation
|
||||
----------
|
||||
|
||||
@ -119,8 +123,8 @@ An HTTP-based protocol can make use of TLS in largely the same way to provide th
|
||||
Provision of these properties *is* dependant on implementers following Great Black Swamp's rules for x509 certificate validation
|
||||
(rather than the standard "web" rules for validation).
|
||||
|
||||
Requirements
|
||||
------------
|
||||
Design Requirements
|
||||
-------------------
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
@ -189,6 +193,9 @@ Solutions
|
||||
An HTTP-based protocol, dubbed "Great Black Swamp" (or "GBS"), is described below.
|
||||
This protocol aims to satisfy the above requirements at a lower level of complexity than the current Foolscap-based protocol.
|
||||
|
||||
Summary (Non-normative)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Communication with the storage node will take place using TLS.
|
||||
The TLS version and configuration will be dictated by an ongoing understanding of best practices.
|
||||
The storage node will present an x509 certificate during the TLS handshake.
|
||||
@ -211,15 +218,15 @@ To further clarify, consider this example.
|
||||
Alice operates a storage node.
|
||||
Alice generates a key pair and secures it properly.
|
||||
Alice generates a self-signed storage node certificate with the key pair.
|
||||
Alice's storage node announces (to an introducer) a fURL containing (among other information) the SPKI hash.
|
||||
Alice's storage node announces (to an introducer) a NURL containing (among other information) the SPKI hash.
|
||||
Imagine the SPKI hash is ``i5xb...``.
|
||||
This results in a fURL of ``pb://i5xb...@example.com:443/g3m5...#v=1``.
|
||||
This results in a NURL of ``pb://i5xb...@example.com:443/g3m5...#v=1``.
|
||||
Bob creates a client node pointed at the same introducer.
|
||||
Bob's client node receives the announcement from Alice's storage node
|
||||
(indirected through the introducer).
|
||||
|
||||
Bob's client node recognizes the fURL as referring to an HTTP-dialect server due to the ``v=1`` fragment.
|
||||
Bob's client node can now perform a TLS handshake with a server at the address in the fURL location hints
|
||||
Bob's client node recognizes the NURL as referring to an HTTP-dialect server due to the ``v=1`` fragment.
|
||||
Bob's client node can now perform a TLS handshake with a server at the address in the NURL location hints
|
||||
(``example.com:443`` in this example).
|
||||
Following the above described validation procedures,
|
||||
Bob's client node can determine whether it has reached Alice's storage node or not.
|
||||
@ -230,17 +237,17 @@ Additionally,
|
||||
by continuing to interact using TLS,
|
||||
Bob's client and Alice's storage node are assured of both **message authentication** and **message confidentiality**.
|
||||
|
||||
Bob's client further inspects the fURL for the *swissnum*.
|
||||
Bob's client further inspects the NURL for the *swissnum*.
|
||||
When Bob's client issues HTTP requests to Alice's storage node it includes the *swissnum* in its requests.
|
||||
**Storage authorization** has been achieved.
|
||||
|
||||
.. note::
|
||||
|
||||
Foolscap TubIDs are 20 bytes (SHA1 digest of the certificate).
|
||||
They are encoded with Base32 for a length of 32 bytes.
|
||||
They are encoded with `Base32`_ for a length of 32 bytes.
|
||||
SPKI information discussed here is 32 bytes (SHA256 digest).
|
||||
They would be encoded in Base32 for a length of 52 bytes.
|
||||
`base64url`_ provides a more compact encoding of the information while remaining URL-compatible.
|
||||
They would be encoded in `Base32`_ for a length of 52 bytes.
|
||||
`unpadded base64url`_ provides a more compact encoding of the information while remaining URL-compatible.
|
||||
This would encode the SPKI information for a length of merely 43 bytes.
|
||||
SHA1,
|
||||
the current Foolscap hash function,
|
||||
@ -266,13 +273,13 @@ Generation of a new certificate allows for certain non-optimal conditions to be
|
||||
* The ``commonName`` of ``newpb_thingy`` may be changed to a more descriptive value.
|
||||
* A ``notValidAfter`` field with a timestamp in the past may be updated.
|
||||
|
||||
Storage nodes will announce a new fURL for this new HTTP-based server.
|
||||
This fURL will be announced alongside their existing Foolscap-based server's fURL.
|
||||
Storage nodes will announce a new NURL for this new HTTP-based server.
|
||||
This NURL will be announced alongside their existing Foolscap-based server's fURL.
|
||||
Such an announcement will resemble this::
|
||||
|
||||
{
|
||||
"anonymous-storage-FURL": "pb://...", # The old key
|
||||
"gbs-anonymous-storage-url": "pb://...#v=1" # The new key
|
||||
"anonymous-storage-FURL": "pb://...", # The old entry
|
||||
"anonymous-storage-NURLs": ["pb://...#v=1"] # The new, additional entry
|
||||
}
|
||||
|
||||
The transition process will proceed in three stages:
|
||||
@ -312,13 +319,8 @@ The follow sequence of events is likely:
|
||||
#. The client uses the information in its cache to open a Foolscap connection to the storage server.
|
||||
|
||||
Ideally,
|
||||
the client would not rely on an update from the introducer to give it the GBS fURL for the updated storage server.
|
||||
Therefore,
|
||||
when an updated client connects to a storage server using Foolscap,
|
||||
it should request the server's version information.
|
||||
If this information indicates that GBS is supported then the client should cache this GBS information.
|
||||
On subsequent connection attempts,
|
||||
it should make use of this GBS information.
|
||||
the client would not rely on an update from the introducer to give it the GBS NURL for the updated storage server.
|
||||
In practice, we have decided not to implement this functionality.
|
||||
|
||||
Server Details
|
||||
--------------
|
||||
@ -329,15 +331,117 @@ and shares.
|
||||
A particular resource is addressed by the HTTP request path.
|
||||
Details about the interface are encoded in the HTTP message body.
|
||||
|
||||
String Encoding
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. _Base32:
|
||||
|
||||
Base32
|
||||
!!!!!!
|
||||
|
||||
Where the specification refers to Base32 the meaning is *unpadded* Base32 encoding as specified by `RFC 4648`_ using a *lowercase variation* of the alphabet from Section 6.
|
||||
|
||||
That is, the alphabet is:
|
||||
|
||||
.. list-table:: Base32 Alphabet
|
||||
:header-rows: 1
|
||||
|
||||
* - Value
|
||||
- Encoding
|
||||
- Value
|
||||
- Encoding
|
||||
- Value
|
||||
- Encoding
|
||||
- Value
|
||||
- Encoding
|
||||
|
||||
* - 0
|
||||
- a
|
||||
- 9
|
||||
- j
|
||||
- 18
|
||||
- s
|
||||
- 27
|
||||
- 3
|
||||
* - 1
|
||||
- b
|
||||
- 10
|
||||
- k
|
||||
- 19
|
||||
- t
|
||||
- 28
|
||||
- 4
|
||||
* - 2
|
||||
- c
|
||||
- 11
|
||||
- l
|
||||
- 20
|
||||
- u
|
||||
- 29
|
||||
- 5
|
||||
* - 3
|
||||
- d
|
||||
- 12
|
||||
- m
|
||||
- 21
|
||||
- v
|
||||
- 30
|
||||
- 6
|
||||
* - 4
|
||||
- e
|
||||
- 13
|
||||
- n
|
||||
- 22
|
||||
- w
|
||||
- 31
|
||||
- 7
|
||||
* - 5
|
||||
- f
|
||||
- 14
|
||||
- o
|
||||
- 23
|
||||
- x
|
||||
-
|
||||
-
|
||||
* - 6
|
||||
- g
|
||||
- 15
|
||||
- p
|
||||
- 24
|
||||
- y
|
||||
-
|
||||
-
|
||||
* - 7
|
||||
- h
|
||||
- 16
|
||||
- q
|
||||
- 25
|
||||
- z
|
||||
-
|
||||
-
|
||||
* - 8
|
||||
- i
|
||||
- 17
|
||||
- r
|
||||
- 26
|
||||
- 2
|
||||
-
|
||||
-
|
||||
|
||||
Message Encoding
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The preferred encoding for HTTP message bodies is `CBOR`_.
|
||||
A request may be submitted using an alternate encoding by declaring this in the ``Content-Type`` header.
|
||||
A request may indicate its preference for an alternate encoding in the response using the ``Accept`` header.
|
||||
These two headers are used in the typical way for an HTTP application.
|
||||
Clients and servers MUST use the ``Content-Type`` and ``Accept`` header fields as specified in `RFC 9110`_ for message body negotiation.
|
||||
|
||||
The only other encoding support for which is currently recommended is JSON.
|
||||
The encoding for HTTP message bodies SHOULD be `CBOR`_.
|
||||
Clients submitting requests using this encoding MUST include a ``Content-Type: application/cbor`` request header field.
|
||||
A request MAY be submitted using an alternate encoding by declaring this in the ``Content-Type`` header field.
|
||||
A request MAY indicate its preference for an alternate encoding in the response using the ``Accept`` header field.
|
||||
A request which includes no ``Accept`` header field MUST be interpreted in the same way as a request including a ``Accept: application/cbor`` header field.
|
||||
|
||||
Clients and servers MAY support additional request and response message body encodings.
|
||||
|
||||
Clients and servers SHOULD support ``application/json`` request and response message body encoding.
|
||||
For HTTP messages carrying binary share data,
|
||||
this is expected to be a particularly poor encoding.
|
||||
However,
|
||||
@ -350,6 +454,24 @@ Because of the simple types used throughout
|
||||
and the equivalence described in `RFC 7049`_
|
||||
these examples should be representative regardless of which of these two encodings is chosen.
|
||||
|
||||
There are two exceptions to this rule.
|
||||
|
||||
1. Sets
|
||||
!!!!!!!
|
||||
|
||||
For CBOR messages,
|
||||
any sequence that is semantically a set (i.e. no repeated values allowed, order doesn't matter, and elements are hashable in Python) should be sent as a set.
|
||||
Tag 6.258 is used to indicate sets in CBOR;
|
||||
see `the CBOR registry <https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml>`_ for more details.
|
||||
The JSON encoding does not support sets.
|
||||
Sets MUST be represented as arrays in JSON-encoded messages.
|
||||
|
||||
2. Bytes
|
||||
!!!!!!!!
|
||||
|
||||
The CBOR encoding natively supports a bytes type while the JSON encoding does not.
|
||||
Bytes MUST be represented as strings giving the `Base64`_ representation of the original bytes value.
|
||||
|
||||
HTTP Design
|
||||
~~~~~~~~~~~
|
||||
|
||||
@ -363,50 +485,96 @@ one branch contains all of the share data;
|
||||
another branch contains all of the lease data;
|
||||
etc.
|
||||
|
||||
Authorization is required for all endpoints.
|
||||
The standard HTTP authorization protocol is used.
|
||||
The authentication *type* used is ``Tahoe-LAFS``.
|
||||
The swissnum from the NURL used to locate the storage service is used as the *credentials*.
|
||||
If credentials are not presented or the swissnum is not associated with a storage service then no storage processing is performed and the request receives an ``UNAUTHORIZED`` response.
|
||||
Clients and servers MUST use the ``Authorization`` header field,
|
||||
as specified in `RFC 9110`_,
|
||||
for authorization of all requests to all endpoints specified here.
|
||||
The authentication *type* MUST be ``Tahoe-LAFS``.
|
||||
Clients MUST present the `Base64`_-encoded representation of the swissnum from the NURL used to locate the storage service as the *credentials*.
|
||||
|
||||
If credentials are not presented or the swissnum is not associated with a storage service then the server MUST issue a ``401 UNAUTHORIZED`` response and perform no other processing of the message.
|
||||
|
||||
Requests to certain endpoints MUST include additional secrets in the ``X-Tahoe-Authorization`` headers field.
|
||||
The endpoints which require these secrets are:
|
||||
|
||||
* ``PUT /storage/v1/lease/:storage_index``:
|
||||
The secrets included MUST be ``lease-renew-secret`` and ``lease-cancel-secret``.
|
||||
|
||||
* ``POST /storage/v1/immutable/:storage_index``:
|
||||
The secrets included MUST be ``lease-renew-secret``, ``lease-cancel-secret``, and ``upload-secret``.
|
||||
|
||||
* ``PATCH /storage/v1/immutable/:storage_index/:share_number``:
|
||||
The secrets included MUST be ``upload-secret``.
|
||||
|
||||
* ``PUT /storage/v1/immutable/:storage_index/:share_number/abort``:
|
||||
The secrets included MUST be ``upload-secret``.
|
||||
|
||||
* ``POST /storage/v1/mutable/:storage_index/read-test-write``:
|
||||
The secrets included MUST be ``lease-renew-secret``, ``lease-cancel-secret``, and ``write-enabler``.
|
||||
|
||||
If these secrets are:
|
||||
|
||||
1. Missing.
|
||||
2. The wrong length.
|
||||
3. Not the expected kind of secret.
|
||||
4. They are otherwise unparseable before they are actually semantically used.
|
||||
|
||||
the server MUST respond with ``400 BAD REQUEST`` and perform no other processing of the message.
|
||||
401 is not used because this isn't an authorization problem, this is a "you sent garbage and should know better" bug.
|
||||
|
||||
If authorization using the secret fails,
|
||||
then the server MUST send a ``401 UNAUTHORIZED`` response and perform no other processing of the message.
|
||||
|
||||
Encoding
|
||||
~~~~~~~~
|
||||
|
||||
* ``storage_index`` MUST be `Base32`_ encoded in URLs.
|
||||
* ``share_number`` MUST be a decimal representation
|
||||
|
||||
General
|
||||
~~~~~~~
|
||||
|
||||
``GET /v1/version``
|
||||
!!!!!!!!!!!!!!!!!!!
|
||||
``GET /storage/v1/version``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
Retrieve information about the version of the storage server.
|
||||
Information is returned as an encoded mapping.
|
||||
For example::
|
||||
This endpoint allows clients to retrieve some basic metadata about a storage server from the storage service.
|
||||
The response MUST validate against this CDDL schema::
|
||||
|
||||
{ "http://allmydata.org/tahoe/protocols/storage/v1" :
|
||||
{ "maximum-immutable-share-size": 1234,
|
||||
"maximum-mutable-share-size": 1235,
|
||||
"available-space": 123456,
|
||||
"tolerates-immutable-read-overrun": true,
|
||||
"delete-mutable-shares-with-zero-length-writev": true,
|
||||
"fills-holes-with-zero-bytes": true,
|
||||
"prevents-read-past-end-of-share-data": true,
|
||||
"gbs-anonymous-storage-url": "pb://...#v=1"
|
||||
},
|
||||
"application-version": "1.13.0"
|
||||
}
|
||||
{'http://allmydata.org/tahoe/protocols/storage/v1' => {
|
||||
'maximum-immutable-share-size' => uint
|
||||
'maximum-mutable-share-size' => uint
|
||||
'available-space' => uint
|
||||
}
|
||||
'application-version' => bstr
|
||||
}
|
||||
|
||||
``PUT /v1/lease/:storage_index``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
The server SHOULD populate as many fields as possible with accurate information about its behavior.
|
||||
|
||||
For fields which relate to a specific API
|
||||
the semantics are documented below in the section for that API.
|
||||
For fields that are more general than a single API the semantics are as follows:
|
||||
|
||||
* available-space:
|
||||
The server SHOULD use this field to advertise the amount of space that it currently considers unused and is willing to allocate for client requests.
|
||||
The value is a number of bytes.
|
||||
|
||||
|
||||
``PUT /storage/v1/lease/:storage_index``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
Either renew or create a new lease on the bucket addressed by ``storage_index``.
|
||||
The details of the lease are encoded in the request body.
|
||||
|
||||
The renew secret and cancellation secret should be included as ``X-Tahoe-Authorization`` headers.
|
||||
For example::
|
||||
|
||||
{"renew-secret": "abcd", "cancel-secret": "efgh"}
|
||||
X-Tahoe-Authorization: lease-renew-secret <base64-lease-renew-secret>
|
||||
X-Tahoe-Authorization: lease-cancel-secret <base64-lease-cancel-secret>
|
||||
|
||||
If the ``renew-secret`` value matches an existing lease
|
||||
If the ``lease-renew-secret`` value matches an existing lease
|
||||
then the expiration time of that lease will be changed to 31 days after the time of this operation.
|
||||
If it does not match an existing lease
|
||||
then a new lease will be created with this ``renew-secret`` which expires 31 days after the time of this operation.
|
||||
then a new lease will be created with this ``lease-renew-secret`` which expires 31 days after the time of this operation.
|
||||
|
||||
``renew-secret`` and ``cancel-secret`` values must be 32 bytes long.
|
||||
``lease-renew-secret`` and ``lease-cancel-secret`` values must be 32 bytes long.
|
||||
The server treats them as opaque values.
|
||||
:ref:`Share Leases` gives details about how the Tahoe-LAFS storage client constructs these values.
|
||||
|
||||
@ -423,8 +591,10 @@ In these cases the server takes no action and returns ``NOT FOUND``.
|
||||
Discussion
|
||||
``````````
|
||||
|
||||
We considered an alternative where ``renew-secret`` and ``cancel-secret`` are placed in query arguments on the request path.
|
||||
We chose to put these values into the request body to make the URL simpler.
|
||||
We considered an alternative where ``lease-renew-secret`` and ``lease-cancel-secret`` are placed in query arguments on the request path.
|
||||
This increases chances of leaking secrets in logs.
|
||||
Putting the secrets in the body reduces the chances of leaking secrets,
|
||||
but eventually we chose headers as the least likely information to be logged.
|
||||
|
||||
Several behaviors here are blindly copied from the Foolscap-based storage server protocol.
|
||||
|
||||
@ -441,27 +611,59 @@ Immutable
|
||||
Writing
|
||||
~~~~~~~
|
||||
|
||||
``POST /v1/immutable/:storage_index``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
``POST /storage/v1/immutable/:storage_index``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
Initialize an immutable storage index with some buckets.
|
||||
The buckets may have share data written to them once.
|
||||
A lease is also created for the shares.
|
||||
The server MUST allow share data to be written to the buckets at most one time.
|
||||
The server MAY create a lease for the buckets.
|
||||
Details of the buckets to create are encoded in the request body.
|
||||
The request body MUST validate against this CDDL schema::
|
||||
|
||||
{
|
||||
share-numbers: #6.258([0*256 uint])
|
||||
allocated-size: uint
|
||||
}
|
||||
|
||||
For example::
|
||||
|
||||
{"renew-secret": "efgh", "cancel-secret": "ijkl",
|
||||
"share-numbers": [1, 7, ...], "allocated-size": 12345}
|
||||
{"share-numbers": [1, 7, ...], "allocated-size": 12345}
|
||||
|
||||
The server SHOULD accept a value for **allocated-size** that is less than or equal to the lesser of the values of the server's version message's **maximum-immutable-share-size** or **available-space** values.
|
||||
|
||||
The request MUST include ``X-Tahoe-Authorization`` HTTP headers that set the various secrets—upload, lease renewal, lease cancellation—that will be later used to authorize various operations.
|
||||
For example::
|
||||
|
||||
X-Tahoe-Authorization: lease-renew-secret <base64-lease-renew-secret>
|
||||
X-Tahoe-Authorization: lease-cancel-secret <base64-lease-cancel-secret>
|
||||
X-Tahoe-Authorization: upload-secret <base64-upload-secret>
|
||||
|
||||
The response body MUST include encoded information about the created buckets.
|
||||
The response body MUST validate against this CDDL schema::
|
||||
|
||||
{
|
||||
already-have: #6.258([0*256 uint])
|
||||
allocated: #6.258([0*256 uint])
|
||||
}
|
||||
|
||||
The response body includes encoded information about the created buckets.
|
||||
For example::
|
||||
|
||||
{"already-have": [1, ...], "allocated": [7, ...]}
|
||||
|
||||
The upload secret is an opaque _byte_ string.
|
||||
|
||||
Handling repeat calls:
|
||||
|
||||
* If the same API call is repeated with the same upload secret, the response is the same and no change is made to server state.
|
||||
This is necessary to ensure retries work in the face of lost responses from the server.
|
||||
* If the API calls is with a different upload secret, this implies a new client, perhaps because the old client died.
|
||||
Or it may happen because the client wants to upload a different share number than a previous client.
|
||||
New shares will be created, existing shares will be unchanged, regardless of whether the upload secret matches or not.
|
||||
|
||||
Discussion
|
||||
``````````
|
||||
|
||||
We considered making this ``POST /v1/immutable`` instead.
|
||||
We considered making this ``POST /storage/v1/immutable`` instead.
|
||||
The motivation was to keep *storage index* out of the request URL.
|
||||
Request URLs have an elevated chance of being logged by something.
|
||||
We were concerned that having the *storage index* logged may increase some risks.
|
||||
@ -482,25 +684,53 @@ The response includes ``already-have`` and ``allocated`` for two reasons:
|
||||
This might be because a server has become unavailable and a remaining server needs to store more shares for the upload.
|
||||
It could also just be that the client's preferred servers have changed.
|
||||
|
||||
``PATCH /v1/immutable/:storage_index/:share_number``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
Regarding upload secrets,
|
||||
the goal is for uploading and aborting (see next sections) to be authenticated by more than just the storage index.
|
||||
In the future, we may want to generate them in a way that allows resuming/canceling when the client has issues.
|
||||
In the short term, they can just be a random byte string.
|
||||
The primary security constraint is that each upload to each server has its own unique upload key,
|
||||
tied to uploading that particular storage index to this particular server.
|
||||
|
||||
Rejected designs for upload secrets:
|
||||
|
||||
* Upload secret per share number.
|
||||
In order to make the secret unguessable by attackers, which includes other servers,
|
||||
it must contain randomness.
|
||||
Randomness means there is no need to have a secret per share, since adding share-specific content to randomness doesn't actually make the secret any better.
|
||||
|
||||
``PATCH /storage/v1/immutable/:storage_index/:share_number``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
Write data for the indicated share.
|
||||
The share number must belong to the storage index.
|
||||
The request body is the raw share data (i.e., ``application/octet-stream``).
|
||||
*Content-Range* requests are encouraged for large transfers to allow partially complete uploads to be resumed.
|
||||
The share number MUST belong to the storage index.
|
||||
The request body MUST be the raw share data (i.e., ``application/octet-stream``).
|
||||
The request MUST include a *Content-Range* header field;
|
||||
for large transfers this allows partially complete uploads to be resumed.
|
||||
|
||||
For example,
|
||||
a 1MiB share can be divided in to eight separate 128KiB chunks.
|
||||
Each chunk can be uploaded in a separate request.
|
||||
Each request can include a *Content-Range* value indicating its placement within the complete share.
|
||||
If any one of these requests fails then at most 128KiB of upload work needs to be retried.
|
||||
|
||||
The server must recognize when all of the data has been received and mark the share as complete
|
||||
The server MUST recognize when all of the data has been received and mark the share as complete
|
||||
(which it can do because it was informed of the size when the storage index was initialized).
|
||||
|
||||
* When a chunk that does not complete the share is successfully uploaded the response is ``OK``.
|
||||
The response body indicates the range of share data that has yet to be uploaded.
|
||||
That is::
|
||||
The request MUST include a ``X-Tahoe-Authorization`` header that includes the upload secret::
|
||||
|
||||
X-Tahoe-Authorization: upload-secret <base64-upload-secret>
|
||||
|
||||
Responses:
|
||||
|
||||
* When a chunk that does not complete the share is successfully uploaded the response MUST be ``OK``.
|
||||
The response body MUST indicate the range of share data that has yet to be uploaded.
|
||||
The response body MUST validate against this CDDL schema::
|
||||
|
||||
{
|
||||
required: [0* {begin: uint, end: uint}]
|
||||
}
|
||||
|
||||
For example::
|
||||
|
||||
{ "required":
|
||||
[ { "begin": <byte position, inclusive>
|
||||
@ -511,25 +741,12 @@ The server must recognize when all of the data has been received and mark the sh
|
||||
]
|
||||
}
|
||||
|
||||
* When the chunk that completes the share is successfully uploaded the response is ``CREATED``.
|
||||
* When the chunk that completes the share is successfully uploaded the response MUST be ``CREATED``.
|
||||
* If the *Content-Range* for a request covers part of the share that has already,
|
||||
and the data does not match already written data,
|
||||
the response is ``CONFLICT``.
|
||||
At this point the only thing to do is abort the upload and start from scratch (see below).
|
||||
|
||||
``PUT /v1/immutable/:storage_index/:share_number/abort``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
This cancels an *in-progress* upload.
|
||||
|
||||
The response code:
|
||||
|
||||
* When the upload is still in progress and therefore the abort has succeeded,
|
||||
the response is ``OK``.
|
||||
Future uploads can start from scratch with no pre-existing upload state stored on the server.
|
||||
* If the uploaded has already finished, the response is 405 (Method Not Allowed)
|
||||
and no change is made.
|
||||
|
||||
the response MUST be ``CONFLICT``.
|
||||
In this case the client MUST abort the upload.
|
||||
The client MAY then restart the upload from scratch.
|
||||
|
||||
Discussion
|
||||
``````````
|
||||
@ -549,38 +766,85 @@ From RFC 7231::
|
||||
PATCH method defined in [RFC5789]).
|
||||
|
||||
|
||||
``POST /v1/immutable/:storage_index/:share_number/corrupt``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
``PUT /storage/v1/immutable/:storage_index/:share_number/abort``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
This cancels an *in-progress* upload.
|
||||
|
||||
The request MUST include a ``X-Tahoe-Authorization`` header that includes the upload secret::
|
||||
|
||||
X-Tahoe-Authorization: upload-secret <base64-upload-secret>
|
||||
|
||||
If there is an incomplete upload with a matching upload-secret then the server MUST consider the abort to have succeeded.
|
||||
In this case the response MUST be ``OK``.
|
||||
The server MUST respond to all future requests as if the operations related to this upload did not take place.
|
||||
|
||||
If there is no incomplete upload with a matching upload-secret then the server MUST respond with ``Method Not Allowed`` (405).
|
||||
The server MUST make no client-visible changes to its state in this case.
|
||||
|
||||
``POST /storage/v1/immutable/:storage_index/:share_number/corrupt``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
Advise the server the data read from the indicated share was corrupt.
|
||||
The request body includes an human-meaningful string with details about the corruption.
|
||||
The request body includes an human-meaningful text string with details about the corruption.
|
||||
It also includes potentially important details about the share.
|
||||
The request body MUST validate against this CDDL schema::
|
||||
|
||||
{
|
||||
reason: tstr .size (1..32765)
|
||||
}
|
||||
|
||||
For example::
|
||||
|
||||
{"reason": "expected hash abcd, got hash efgh"}
|
||||
|
||||
.. share-type, storage-index, and share-number are inferred from the URL
|
||||
The report pertains to the immutable share with a **storage index** and **share number** given in the request path.
|
||||
If the identified **storage index** and **share number** are known to the server then the response SHOULD be accepted and made available to server administrators.
|
||||
In this case the response SHOULD be ``OK``.
|
||||
If the response is not accepted then the response SHOULD be ``Not Found`` (404).
|
||||
|
||||
Discussion
|
||||
``````````
|
||||
|
||||
The seemingly odd length limit on ``reason`` is chosen so that the *encoded* representation of the message is limited to 32768.
|
||||
|
||||
Reading
|
||||
~~~~~~~
|
||||
|
||||
``GET /v1/immutable/:storage_index/shares``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
``GET /storage/v1/immutable/:storage_index/shares``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
Retrieve a list (semantically, a set) indicating all shares available for the indicated storage index.
|
||||
The response body MUST validate against this CDDL schema::
|
||||
|
||||
#6.258([0*256 uint])
|
||||
|
||||
Retrieve a list indicating all shares available for the indicated storage index.
|
||||
For example::
|
||||
|
||||
[1, 5]
|
||||
|
||||
``GET /v1/immutable/:storage_index/:share_number``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
If the **storage index** in the request path is not known to the server then the response MUST include an empty list.
|
||||
|
||||
``GET /storage/v1/immutable/:storage_index/:share_number``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
Read a contiguous sequence of bytes from one share in one bucket.
|
||||
The response body is the raw share data (i.e., ``application/octet-stream``).
|
||||
The ``Range`` header may be used to request exactly one ``bytes`` range.
|
||||
Interpretation and response behavior is as specified in RFC 7233 § 4.1.
|
||||
Multiple ranges in a single request are *not* supported.
|
||||
The response body MUST be the raw share data (i.e., ``application/octet-stream``).
|
||||
The ``Range`` header MAY be used to request exactly one ``bytes`` range,
|
||||
in which case the response code MUST be ``Partial Content`` (206).
|
||||
Interpretation and response behavior MUST be as specified in RFC 7233 § 4.1.
|
||||
Multiple ranges in a single request are *not* supported;
|
||||
open-ended ranges are also not supported.
|
||||
Clients MUST NOT send requests using these features.
|
||||
|
||||
If the response reads beyond the end of the data,
|
||||
the response MUST be shorter than the requested range.
|
||||
It MUST contain all data up to the end of the share and then end.
|
||||
The resulting ``Content-Range`` header MUST be consistent with the returned data.
|
||||
|
||||
If the response to a query is an empty range,
|
||||
the server MUST send a ``No Content`` (204) response.
|
||||
|
||||
Discussion
|
||||
``````````
|
||||
@ -609,8 +873,8 @@ Mutable
|
||||
Writing
|
||||
~~~~~~~
|
||||
|
||||
``POST /v1/mutable/:storage_index/read-test-write``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
``POST /storage/v1/mutable/:storage_index/read-test-write``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
General purpose read-test-and-write operation for mutable storage indexes.
|
||||
A mutable storage index is also called a "slot"
|
||||
@ -619,16 +883,30 @@ The first write operation on a mutable storage index creates it
|
||||
(that is,
|
||||
there is no separate "create this storage index" operation as there is for the immutable storage index type).
|
||||
|
||||
The request body includes the secrets necessary to rewrite to the shares
|
||||
along with test, read, and write vectors for the operation.
|
||||
The request MUST include ``X-Tahoe-Authorization`` headers with write enabler and lease secrets::
|
||||
|
||||
X-Tahoe-Authorization: write-enabler <base64-write-enabler-secret>
|
||||
X-Tahoe-Authorization: lease-cancel-secret <base64-lease-cancel-secret>
|
||||
X-Tahoe-Authorization: lease-renew-secret <base64-lease-renew-secret>
|
||||
|
||||
The request body MUST include test, read, and write vectors for the operation.
|
||||
The request body MUST validate against this CDDL schema::
|
||||
|
||||
{
|
||||
"test-write-vectors": {
|
||||
0*256 share_number : {
|
||||
"test": [0*30 {"offset": uint, "size": uint, "specimen": bstr}]
|
||||
"write": [* {"offset": uint, "data": bstr}]
|
||||
"new-length": uint / null
|
||||
}
|
||||
}
|
||||
"read-vector": [0*30 {"offset": uint, "size": uint}]
|
||||
}
|
||||
share_number = uint
|
||||
|
||||
For example::
|
||||
|
||||
{
|
||||
"secrets": {
|
||||
"write-enabler": "abcd",
|
||||
"lease-renew": "efgh",
|
||||
"lease-cancel": "ijkl"
|
||||
},
|
||||
"test-write-vectors": {
|
||||
0: {
|
||||
"test": [{
|
||||
@ -648,6 +926,14 @@ For example::
|
||||
|
||||
The response body contains a boolean indicating whether the tests all succeed
|
||||
(and writes were applied) and a mapping giving read data (pre-write).
|
||||
The response body MUST validate against this CDDL schema::
|
||||
|
||||
{
|
||||
"success": bool,
|
||||
"data": {0*256 share_number: [0* bstr]}
|
||||
}
|
||||
share_number = uint
|
||||
|
||||
For example::
|
||||
|
||||
{
|
||||
@ -659,28 +945,57 @@ For example::
|
||||
}
|
||||
}
|
||||
|
||||
A test vector or read vector that read beyond the boundaries of existing data will return nothing for any bytes past the end.
|
||||
As a result, if there is no data at all, an empty bytestring is returned no matter what the offset or length.
|
||||
A client MAY send a test vector or read vector to bytes beyond the end of existing data.
|
||||
In this case a server MUST behave as if the test or read vector referred to exactly as much data exists.
|
||||
|
||||
For example,
|
||||
consider the case where the server has 5 bytes of data for a particular share.
|
||||
If a client sends a read vector with an ``offset`` of 1 and a ``size`` of 4 then the server MUST respond with all of the data except the first byte.
|
||||
If a client sends a read vector with the same ``offset`` and a ``size`` of 5 (or any larger value) then the server MUST respond in the same way.
|
||||
|
||||
Similarly,
|
||||
if there is no data at all,
|
||||
an empty byte string is returned no matter what the offset or length.
|
||||
|
||||
Reading
|
||||
~~~~~~~
|
||||
|
||||
``GET /v1/mutable/:storage_index/shares``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
``GET /storage/v1/mutable/:storage_index/shares``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
Retrieve a set indicating all shares available for the indicated storage index.
|
||||
The response body MUST validate against this CDDL schema::
|
||||
|
||||
#6.258([0*256 uint])
|
||||
|
||||
Retrieve a list indicating all shares available for the indicated storage index.
|
||||
For example::
|
||||
|
||||
[1, 5]
|
||||
|
||||
``GET /v1/mutable/:storage_index?share=:s0&share=:sN&offset=:o1&size=:z0&offset=:oN&size=:zN``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
``GET /storage/v1/mutable/:storage_index/:share_number``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
Read data from the indicated mutable shares.
|
||||
Just like ``GET /v1/mutable/:storage_index``.
|
||||
Read data from the indicated mutable shares, just like ``GET /storage/v1/immutable/:storage_index``.
|
||||
|
||||
``POST /v1/mutable/:storage_index/:share_number/corrupt``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
The response body MUST be the raw share data (i.e., ``application/octet-stream``).
|
||||
The ``Range`` header MAY be used to request exactly one ``bytes`` range,
|
||||
in which case the response code MUST be ``Partial Content`` (206).
|
||||
Interpretation and response behavior MUST be specified in RFC 7233 § 4.1.
|
||||
Multiple ranges in a single request are *not* supported;
|
||||
open-ended ranges are also not supported.
|
||||
Clients MUST NOT send requests using these features.
|
||||
|
||||
If the response reads beyond the end of the data,
|
||||
the response MUST be shorter than the requested range.
|
||||
It MUST contain all data up to the end of the share and then end.
|
||||
The resulting ``Content-Range`` header MUST be consistent with the returned data.
|
||||
|
||||
If the response to a query is an empty range,
|
||||
the server MUST send a ``No Content`` (204) response.
|
||||
|
||||
|
||||
``POST /storage/v1/mutable/:storage_index/:share_number/corrupt``
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
Advise the server the data read from the indicated share was corrupt.
|
||||
Just like the immutable version.
|
||||
@ -688,49 +1003,69 @@ Just like the immutable version.
|
||||
Sample Interactions
|
||||
-------------------
|
||||
|
||||
This section contains examples of client/server interactions to help illuminate the above specification.
|
||||
This section is non-normative.
|
||||
|
||||
Immutable Data
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
1. Create a bucket for storage index ``AAAAAAAAAAAAAAAA`` to hold two immutable shares, discovering that share ``1`` was already uploaded::
|
||||
|
||||
POST /v1/immutable/AAAAAAAAAAAAAAAA
|
||||
{"renew-secret": "efgh", "cancel-secret": "ijkl",
|
||||
"share-numbers": [1, 7], "allocated-size": 48}
|
||||
POST /storage/v1/immutable/AAAAAAAAAAAAAAAA
|
||||
Authorization: Tahoe-LAFS nurl-swissnum
|
||||
X-Tahoe-Authorization: lease-renew-secret efgh
|
||||
X-Tahoe-Authorization: lease-cancel-secret jjkl
|
||||
X-Tahoe-Authorization: upload-secret xyzf
|
||||
|
||||
{"share-numbers": [1, 7], "allocated-size": 48}
|
||||
|
||||
200 OK
|
||||
{"already-have": [1], "allocated": [7]}
|
||||
|
||||
#. Upload the content for immutable share ``7``::
|
||||
|
||||
PATCH /v1/immutable/AAAAAAAAAAAAAAAA/7
|
||||
PATCH /storage/v1/immutable/AAAAAAAAAAAAAAAA/7
|
||||
Authorization: Tahoe-LAFS nurl-swissnum
|
||||
Content-Range: bytes 0-15/48
|
||||
X-Tahoe-Authorization: upload-secret xyzf
|
||||
<first 16 bytes of share data>
|
||||
|
||||
200 OK
|
||||
{ "required": [ {"begin": 16, "end": 48 } ] }
|
||||
|
||||
PATCH /v1/immutable/AAAAAAAAAAAAAAAA/7
|
||||
PATCH /storage/v1/immutable/AAAAAAAAAAAAAAAA/7
|
||||
Authorization: Tahoe-LAFS nurl-swissnum
|
||||
Content-Range: bytes 16-31/48
|
||||
X-Tahoe-Authorization: upload-secret xyzf
|
||||
<second 16 bytes of share data>
|
||||
|
||||
200 OK
|
||||
{ "required": [ {"begin": 32, "end": 48 } ] }
|
||||
|
||||
PATCH /v1/immutable/AAAAAAAAAAAAAAAA/7
|
||||
PATCH /storage/v1/immutable/AAAAAAAAAAAAAAAA/7
|
||||
Authorization: Tahoe-LAFS nurl-swissnum
|
||||
Content-Range: bytes 32-47/48
|
||||
X-Tahoe-Authorization: upload-secret xyzf
|
||||
<final 16 bytes of share data>
|
||||
|
||||
201 CREATED
|
||||
|
||||
#. Download the content of the previously uploaded immutable share ``7``::
|
||||
|
||||
GET /v1/immutable/AAAAAAAAAAAAAAAA?share=7&offset=0&size=48
|
||||
GET /storage/v1/immutable/AAAAAAAAAAAAAAAA?share=7
|
||||
Authorization: Tahoe-LAFS nurl-swissnum
|
||||
Range: bytes=0-47
|
||||
|
||||
200 OK
|
||||
Content-Range: bytes 0-47/48
|
||||
<complete 48 bytes of previously uploaded data>
|
||||
|
||||
#. Renew the lease on all immutable shares in bucket ``AAAAAAAAAAAAAAAA``::
|
||||
|
||||
PUT /v1/lease/AAAAAAAAAAAAAAAA
|
||||
{"renew-secret": "efgh", "cancel-secret": "ijkl"}
|
||||
PUT /storage/v1/lease/AAAAAAAAAAAAAAAA
|
||||
Authorization: Tahoe-LAFS nurl-swissnum
|
||||
X-Tahoe-Authorization: lease-cancel-secret jjkl
|
||||
X-Tahoe-Authorization: lease-renew-secret efgh
|
||||
|
||||
204 NO CONTENT
|
||||
|
||||
@ -742,13 +1077,13 @@ The special test vector of size 1 but empty bytes will only pass
|
||||
if there is no existing share,
|
||||
otherwise it will read a byte which won't match `b""`::
|
||||
|
||||
POST /v1/mutable/BBBBBBBBBBBBBBBB/read-test-write
|
||||
POST /storage/v1/mutable/BBBBBBBBBBBBBBBB/read-test-write
|
||||
Authorization: Tahoe-LAFS nurl-swissnum
|
||||
X-Tahoe-Authorization: write-enabler abcd
|
||||
X-Tahoe-Authorization: lease-cancel-secret efgh
|
||||
X-Tahoe-Authorization: lease-renew-secret ijkl
|
||||
|
||||
{
|
||||
"secrets": {
|
||||
"write-enabler": "abcd",
|
||||
"lease-renew": "efgh",
|
||||
"lease-cancel": "ijkl"
|
||||
},
|
||||
"test-write-vectors": {
|
||||
3: {
|
||||
"test": [{
|
||||
@ -774,13 +1109,13 @@ otherwise it will read a byte which won't match `b""`::
|
||||
|
||||
#. Safely rewrite the contents of a known version of mutable share number ``3`` (or fail)::
|
||||
|
||||
POST /v1/mutable/BBBBBBBBBBBBBBBB/read-test-write
|
||||
POST /storage/v1/mutable/BBBBBBBBBBBBBBBB/read-test-write
|
||||
Authorization: Tahoe-LAFS nurl-swissnum
|
||||
X-Tahoe-Authorization: write-enabler abcd
|
||||
X-Tahoe-Authorization: lease-cancel-secret efgh
|
||||
X-Tahoe-Authorization: lease-renew-secret ijkl
|
||||
|
||||
{
|
||||
"secrets": {
|
||||
"write-enabler": "abcd",
|
||||
"lease-renew": "efgh",
|
||||
"lease-cancel": "ijkl"
|
||||
},
|
||||
"test-write-vectors": {
|
||||
3: {
|
||||
"test": [{
|
||||
@ -806,20 +1141,33 @@ otherwise it will read a byte which won't match `b""`::
|
||||
|
||||
#. Download the contents of share number ``3``::
|
||||
|
||||
GET /v1/mutable/BBBBBBBBBBBBBBBB?share=3&offset=0&size=10
|
||||
GET /storage/v1/mutable/BBBBBBBBBBBBBBBB?share=3
|
||||
Authorization: Tahoe-LAFS nurl-swissnum
|
||||
Range: bytes=0-16
|
||||
|
||||
200 OK
|
||||
Content-Range: bytes 0-15/16
|
||||
<complete 16 bytes of previously uploaded data>
|
||||
|
||||
#. Renew the lease on previously uploaded mutable share in slot ``BBBBBBBBBBBBBBBB``::
|
||||
|
||||
PUT /v1/lease/BBBBBBBBBBBBBBBB
|
||||
{"renew-secret": "efgh", "cancel-secret": "ijkl"}
|
||||
PUT /storage/v1/lease/BBBBBBBBBBBBBBBB
|
||||
Authorization: Tahoe-LAFS nurl-swissnum
|
||||
X-Tahoe-Authorization: lease-cancel-secret efgh
|
||||
X-Tahoe-Authorization: lease-renew-secret ijkl
|
||||
|
||||
204 NO CONTENT
|
||||
|
||||
.. _Base64: https://www.rfc-editor.org/rfc/rfc4648#section-4
|
||||
|
||||
.. _RFC 4648: https://tools.ietf.org/html/rfc4648
|
||||
|
||||
.. _RFC 7469: https://tools.ietf.org/html/rfc7469#section-2.4
|
||||
|
||||
.. _RFC 7049: https://tools.ietf.org/html/rfc7049#section-4
|
||||
|
||||
.. _RFC 9110: https://tools.ietf.org/html/rfc9110
|
||||
|
||||
.. _CBOR: http://cbor.io/
|
||||
|
||||
.. [#]
|
||||
@ -864,7 +1212,7 @@ otherwise it will read a byte which won't match `b""`::
|
||||
spki_encoded = urlsafe_b64encode(spki_sha256)
|
||||
assert spki_encoded == tub_id
|
||||
|
||||
Note we use `base64url`_ rather than the Foolscap- and Tahoe-LAFS-preferred Base32.
|
||||
Note we use `unpadded base64url`_ rather than the Foolscap- and Tahoe-LAFS-preferred Base32.
|
||||
|
||||
.. [#]
|
||||
https://www.cvedetails.com/cve/CVE-2017-5638/
|
||||
@ -875,6 +1223,6 @@ otherwise it will read a byte which won't match `b""`::
|
||||
.. [#]
|
||||
https://efail.de/
|
||||
|
||||
.. _base64url: https://tools.ietf.org/html/rfc7515#appendix-C
|
||||
.. _unpadded base64url: https://tools.ietf.org/html/rfc7515#appendix-C
|
||||
|
||||
.. _attacking SHA1: https://en.wikipedia.org/wiki/SHA-1#Attacks
|
@ -17,3 +17,4 @@ the data formats used by Tahoe.
|
||||
lease
|
||||
servers-of-happiness
|
||||
backends/raic
|
||||
http-storage-node-protocol
|
||||
|
@ -7,6 +7,8 @@ These are not to be confused with the URI-like capabilities Tahoe-LAFS uses to r
|
||||
An attempt is also made to outline the rationale for certain choices about these URLs.
|
||||
The intended audience for this document is Tahoe-LAFS maintainers and other developers interested in interoperating with Tahoe-LAFS or these URLs.
|
||||
|
||||
.. _furls:
|
||||
|
||||
Background
|
||||
----------
|
||||
|
||||
@ -31,12 +33,14 @@ The client's use of the swissnum is what allows the server to authorize the clie
|
||||
|
||||
.. _`swiss number`: http://wiki.erights.org/wiki/Swiss_number
|
||||
|
||||
.. _NURLs:
|
||||
|
||||
NURLs
|
||||
-----
|
||||
|
||||
The authentication and authorization properties of fURLs are a good fit for Tahoe-LAFS' requirements.
|
||||
These are not inherently tied to the Foolscap protocol itself.
|
||||
In particular they are beneficial to :doc:`../proposed/http-storage-node-protocol` which uses HTTP instead of Foolscap.
|
||||
In particular they are beneficial to :doc:`http-storage-node-protocol` which uses HTTP instead of Foolscap.
|
||||
It is conceivable they will also be used with WebSockets at some point as well.
|
||||
|
||||
Continuing to refer to these URLs as fURLs when they are being used for other protocols may cause confusion.
|
||||
@ -47,27 +51,27 @@ This can be considered to expand to "**N**\ ew URLs" or "Authe\ **N**\ ticating
|
||||
The anticipated use for a **NURL** will still be to establish a TLS connection to a peer.
|
||||
The protocol run over that TLS connection could be Foolscap though it is more likely to be an HTTP-based protocol (such as GBS).
|
||||
|
||||
Unlike fURLs, only a single net-loc is included, for consistency with other forms of URLs.
|
||||
As a result, multiple NURLs may be available for a single server.
|
||||
|
||||
Syntax
|
||||
------
|
||||
|
||||
The EBNF for a NURL is as follows::
|
||||
|
||||
nurl = scheme, hash, "@", net-loc-list, "/", swiss-number, [ version1 ]
|
||||
|
||||
scheme = "pb://"
|
||||
nurl = tcp-nurl | tor-nurl | i2p-nurl
|
||||
tcp-nurl = "pb://", hash, "@", tcp-loc, "/", swiss-number, [ version1 ]
|
||||
tor-nurl = "pb+tor://", hash, "@", tcp-loc, "/", swiss-number, [ version1 ]
|
||||
i2p-nurl = "pb+i2p://", hash, "@", i2p-loc, "/", swiss-number, [ version1 ]
|
||||
|
||||
hash = unreserved
|
||||
|
||||
net-loc-list = net-loc, [ { ",", net-loc } ]
|
||||
net-loc = tcp-loc | tor-loc | i2p-loc
|
||||
|
||||
tcp-loc = [ "tcp:" ], hostname, [ ":" port ]
|
||||
tor-loc = "tor:", hostname, [ ":" port ]
|
||||
i2p-loc = "i2p:", i2p-addr, [ ":" port ]
|
||||
|
||||
i2p-addr = { unreserved }, ".i2p"
|
||||
tcp-loc = hostname, [ ":" port ]
|
||||
hostname = domain | IPv4address | IPv6address
|
||||
|
||||
i2p-loc = i2p-addr, [ ":" port ]
|
||||
i2p-addr = { unreserved }, ".i2p"
|
||||
|
||||
swiss-number = segment
|
||||
|
||||
version1 = "#v=1"
|
||||
@ -87,11 +91,13 @@ These differences are separated into distinct versions.
|
||||
Version 0
|
||||
---------
|
||||
|
||||
A Foolscap fURL is considered the canonical definition of a version 0 NURL.
|
||||
In theory, a Foolscap fURL with a single netloc is considered the canonical definition of a version 0 NURL.
|
||||
Notably,
|
||||
the hash component is defined as the base32-encoded SHA1 hash of the DER form of an x509v3 certificate.
|
||||
A version 0 NURL is identified by the absence of the ``v=1`` fragment.
|
||||
|
||||
In practice, real world fURLs may have more than one netloc, so lack of version fragment will likely just involve dispatching the fURL to a different parser.
|
||||
|
||||
Examples
|
||||
~~~~~~~~
|
||||
|
||||
@ -103,11 +109,8 @@ Version 1
|
||||
|
||||
The hash component of a version 1 NURL differs in three ways from the prior version.
|
||||
|
||||
1. The hash function used is SHA3-224 instead of SHA1.
|
||||
The security of SHA1 `continues to be eroded`_.
|
||||
Contrariwise SHA3 is currently the most recent addition to the SHA family by NIST.
|
||||
The 224 bit instance is chosen to keep the output short and because it offers greater collision resistance than SHA1 was thought to offer even at its inception
|
||||
(prior to security research showing actual collision resistance is lower).
|
||||
1. The hash function used is SHA-256, to match RFC 7469.
|
||||
The security of SHA1 `continues to be eroded`_; Latacora `SHA-2`_.
|
||||
2. The hash is computed over the certificate's SPKI instead of the whole certificate.
|
||||
This allows certificate re-generation so long as the public key remains the same.
|
||||
This is useful to allow contact information to be updated or extension of validity period.
|
||||
@ -122,7 +125,7 @@ The hash component of a version 1 NURL differs in three ways from the prior vers
|
||||
*all* certificate fields should be considered within the context of the relationship identified by the SPKI hash.
|
||||
|
||||
3. The hash is encoded using urlsafe-base64 (without padding) instead of base32.
|
||||
This provides a more compact representation and minimizes the usability impacts of switching from a 160 bit hash to a 224 bit hash.
|
||||
This provides a more compact representation and minimizes the usability impacts of switching from a 160 bit hash to a 256 bit hash.
|
||||
|
||||
A version 1 NURL is identified by the presence of the ``v=1`` fragment.
|
||||
Though the length of the hash string (38 bytes) could also be used to differentiate it from a version 0 NURL,
|
||||
@ -140,7 +143,8 @@ Examples
|
||||
* ``pb://azEu8vlRpnEeYm0DySQDeNY3Z2iJXHC_bsbaAw@localhost:47877/64i4aokv4ej#v=1``
|
||||
|
||||
.. _`continues to be eroded`: https://en.wikipedia.org/wiki/SHA-1#Cryptanalysis_and_validation
|
||||
.. _`explored by the web community`: https://www.imperialviolet.org/2011/05/04/pinning.html
|
||||
.. _`SHA-2`: https://latacora.micro.blog/2018/04/03/cryptographic-right-answers.html
|
||||
.. _`explored by the web community`: https://www.rfc-editor.org/rfc/rfc7469
|
||||
.. _Foolscap: https://github.com/warner/foolscap
|
||||
|
||||
.. [1] ``foolscap.furl.decode_furl`` is taken as the canonical definition of the syntax of a fURL.
|
||||
|
@ -264,3 +264,18 @@ the "tahoe-conf" file for notes about configuration and installing these
|
||||
plugins into a Munin environment.
|
||||
|
||||
.. _Munin: http://munin-monitoring.org/
|
||||
|
||||
|
||||
Scraping Stats Values in OpenMetrics Format
|
||||
===========================================
|
||||
|
||||
Time Series DataBase (TSDB) software like Prometheus_ and VictoriaMetrics_ can
|
||||
parse statistics from the e.g. http://localhost:3456/statistics?t=openmetrics
|
||||
URL in OpenMetrics_ format. Software like Grafana_ can then be used to graph
|
||||
and alert on these numbers. You can find a pre-configured dashboard for
|
||||
Grafana at https://grafana.com/grafana/dashboards/16894-tahoe-lafs/.
|
||||
|
||||
.. _OpenMetrics: https://openmetrics.io/
|
||||
.. _Prometheus: https://prometheus.io/
|
||||
.. _VictoriaMetrics: https://victoriametrics.com/
|
||||
.. _Grafana: https://grafana.com/
|
||||
|
115
flake.lock
generated
Normal file
115
flake.lock
generated
Normal file
@ -0,0 +1,115 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1687709756,
|
||||
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-22_11": {
|
||||
"locked": {
|
||||
"lastModified": 1688392541,
|
||||
"narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-22.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-23_05": {
|
||||
"locked": {
|
||||
"lastModified": 1689885880,
|
||||
"narHash": "sha256-2ikAcvHKkKh8J/eUrwMA+wy1poscC+oL1RkN1V3RmT8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fa793b06f56896b7d1909e4b69977c7bf842b2f0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-23.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1689791806,
|
||||
"narHash": "sha256-QpXjfiyBFwa7MV/J6nM5FoBreks9O7j9cAZxV22MR8A=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "439ba0789ff84dddea64eb2d47a4a0d4887dbb1f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "pull/244135/head",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs-unstable"
|
||||
],
|
||||
"nixpkgs-22_11": "nixpkgs-22_11",
|
||||
"nixpkgs-23_05": "nixpkgs-23_05",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
240
flake.nix
Normal file
240
flake.nix
Normal file
@ -0,0 +1,240 @@
|
||||
{
|
||||
description = "Tahoe-LAFS, free and open decentralized data store";
|
||||
|
||||
nixConfig = {
|
||||
# Supply configuration for the build cache updated by our CI system. This
|
||||
# should allow most users to avoid having to build a large number of
|
||||
# packages (otherwise necessary due to our Python package overrides).
|
||||
substituters = ["https://tahoe-lafs-opensource.cachix.org"];
|
||||
trusted-public-keys = ["tahoe-lafs-opensource.cachix.org-1:eIKCHOPJYceJ2gb74l6e0mayuSdXqiavxYeAio0LFGo="];
|
||||
};
|
||||
|
||||
inputs = {
|
||||
# A couple possible nixpkgs pins. Ideally these could be selected easily
|
||||
# from the command line but there seems to be no syntax/support for that.
|
||||
# However, these at least cause certain revisions to be pinned in our lock
|
||||
# file where you *can* dig them out - and the CI configuration does.
|
||||
#
|
||||
# These are really just examples for the time being since neither of these
|
||||
# releases contains a package set that is completely compatible with our
|
||||
# requirements. We could decide in the future that supporting multiple
|
||||
# releases of NixOS at a time is worthwhile and then pins like these will
|
||||
# help us test each of those releases.
|
||||
"nixpkgs-22_11" = {
|
||||
url = github:NixOS/nixpkgs?ref=nixos-22.11;
|
||||
};
|
||||
"nixpkgs-23_05" = {
|
||||
url = github:NixOS/nixpkgs?ref=nixos-23.05;
|
||||
};
|
||||
|
||||
# We depend on a very new python-cryptography which is not yet available
|
||||
# from any release branch of nixpkgs. However, it is contained in a PR
|
||||
# currently up for review. Point our nixpkgs at that for now.
|
||||
"nixpkgs-unstable" = {
|
||||
url = github:NixOS/nixpkgs?ref=pull/244135/head;
|
||||
};
|
||||
|
||||
# Point the default nixpkgs at one of those. This avoids having getting a
|
||||
# _third_ package set involved and gives a way to provide what should be a
|
||||
# working experience by default (that is, if nixpkgs doesn't get
|
||||
# overridden).
|
||||
nixpkgs.follows = "nixpkgs-unstable";
|
||||
|
||||
# Also get flake-utils for simplified multi-system definitions.
|
||||
flake-utils = {
|
||||
url = github:numtide/flake-utils;
|
||||
};
|
||||
|
||||
# And get a helper that lets us easily continue to provide a default.nix.
|
||||
flake-compat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, ... }:
|
||||
{
|
||||
# Expose an overlay which adds our version of Tahoe-LAFS to the Python
|
||||
# package sets we specify, as well as all of the correct versions of its
|
||||
# dependencies.
|
||||
#
|
||||
# We will also use this to define some other outputs since it gives us
|
||||
# the most succinct way to get a working Tahoe-LAFS package.
|
||||
overlays.default = import ./nix/overlay.nix;
|
||||
|
||||
} // (flake-utils.lib.eachDefaultSystem (system: let
|
||||
|
||||
# The package set for this system architecture.
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
# And include our Tahoe-LAFS package in that package set.
|
||||
overlays = [ self.overlays.default ];
|
||||
};
|
||||
|
||||
# pythonVersions :: [string]
|
||||
#
|
||||
# The version strings for the Python runtimes we'll work with.
|
||||
pythonVersions =
|
||||
let
|
||||
# Match attribute names that look like a Python derivation - CPython
|
||||
# or PyPy. We take care to avoid things like "python-foo" and
|
||||
# "python3Full-unittest" though. We only want things like "pypy38"
|
||||
# or "python311".
|
||||
nameMatches = name: null != builtins.match "(python|pypy)3[[:digit:]]{0,2}" name;
|
||||
|
||||
# Sometimes an old version is left in the package set as an error
|
||||
# saying something like "we remove this". Make sure we whatever we
|
||||
# found by name evaluates without error, too.
|
||||
notError = drv: (builtins.tryEval drv).success;
|
||||
in
|
||||
# Discover all of the Python runtime derivations by inspecting names
|
||||
# and filtering out derivations with errors.
|
||||
builtins.attrNames (
|
||||
pkgs.lib.attrsets.filterAttrs
|
||||
(name: drv: nameMatches name && notError drv)
|
||||
pkgs
|
||||
);
|
||||
|
||||
# defaultPyVersion :: string
|
||||
#
|
||||
# An element of pythonVersions which we'll use for the default package.
|
||||
defaultPyVersion = "python3";
|
||||
|
||||
# pythons :: [derivation]
|
||||
#
|
||||
# Retrieve the actual Python package for each configured version. We
|
||||
# already applied our overlay to pkgs so our packages will already be
|
||||
# available.
|
||||
pythons = builtins.map (pyVer: pkgs.${pyVer}) pythonVersions;
|
||||
|
||||
# packageName :: string -> string
|
||||
#
|
||||
# Construct the Tahoe-LAFS package name for the given Python runtime.
|
||||
packageName = pyVersion: "${pyVersion}-tahoe-lafs";
|
||||
|
||||
# string -> string
|
||||
#
|
||||
# Construct the unit test application name for the given Python runtime.
|
||||
unitTestName = pyVersion: "${pyVersion}-unittest";
|
||||
|
||||
# (string -> a) -> (string -> b) -> string -> attrset a b
|
||||
#
|
||||
# Make a singleton attribute set from the result of two functions.
|
||||
singletonOf = f: g: x: { ${f x} = g x; };
|
||||
|
||||
# [attrset] -> attrset
|
||||
#
|
||||
# Merge a list of attrset into a single attrset with overlap preferring
|
||||
# rightmost values.
|
||||
mergeAttrs = pkgs.lib.foldr pkgs.lib.mergeAttrs {};
|
||||
|
||||
# makeRuntimeEnv :: string -> derivation
|
||||
#
|
||||
# Create a derivation that includes a Python runtime, Tahoe-LAFS, and
|
||||
# all of its dependencies.
|
||||
makeRuntimeEnv = singletonOf packageName makeRuntimeEnv';
|
||||
makeRuntimeEnv' = pyVersion: (pkgs.${pyVersion}.withPackages (ps: with ps;
|
||||
[ tahoe-lafs ] ++
|
||||
tahoe-lafs.passthru.extras.i2p ++
|
||||
tahoe-lafs.passthru.extras.tor
|
||||
)).overrideAttrs (old: {
|
||||
# By default, withPackages gives us a derivation with a fairly generic
|
||||
# name (like "python-env"). Put our name in there for legibility.
|
||||
# See the similar override in makeTestEnv.
|
||||
name = packageName pyVersion;
|
||||
});
|
||||
|
||||
# makeTestEnv :: string -> derivation
|
||||
#
|
||||
# Create a derivation that includes a Python runtime and all of the
|
||||
# Tahoe-LAFS dependencies, but not Tahoe-LAFS itself, which we'll get
|
||||
# from the working directory.
|
||||
makeTestEnv = pyVersion: (pkgs.${pyVersion}.withPackages (ps: with ps;
|
||||
[ tahoe-lafs ] ++
|
||||
tahoe-lafs.passthru.extras.i2p ++
|
||||
tahoe-lafs.passthru.extras.tor ++
|
||||
tahoe-lafs.passthru.extras.unittest
|
||||
)).overrideAttrs (old: {
|
||||
# See the similar override in makeRuntimeEnv'.
|
||||
name = packageName pyVersion;
|
||||
});
|
||||
in {
|
||||
# Include a package set with out overlay on it in our own output. This
|
||||
# is mainly a development/debugging convenience as it will expose all of
|
||||
# our Python package overrides beneath it. The magic name
|
||||
# "legacyPackages" is copied from nixpkgs and has special support in the
|
||||
# nix command line tool.
|
||||
legacyPackages = pkgs;
|
||||
|
||||
# The flake's package outputs. We'll define one version of the package
|
||||
# for each version of Python we could find. We'll also point the
|
||||
# flake's "default" package at the derivation corresponding to the
|
||||
# default Python version we defined above. The package consists of a
|
||||
# Python environment with Tahoe-LAFS available to it.
|
||||
packages =
|
||||
mergeAttrs (
|
||||
[ { default = self.packages.${system}.${packageName defaultPyVersion}; } ]
|
||||
++ (builtins.map makeRuntimeEnv pythonVersions)
|
||||
++ (builtins.map (singletonOf unitTestName makeTestEnv) pythonVersions)
|
||||
);
|
||||
|
||||
# The flake's app outputs. We'll define a version of an app for running
|
||||
# the test suite for each version of Python we could find. We'll also
|
||||
# define a version of an app for running the "tahoe" command-line
|
||||
# entrypoint for each version of Python we could find.
|
||||
apps =
|
||||
let
|
||||
# writeScript :: string -> string -> path
|
||||
#
|
||||
# Write a shell program to a file so it can be run later.
|
||||
#
|
||||
# We avoid writeShellApplication here because it has ghc as a
|
||||
# dependency but ghc has Python as a dependency and our Python
|
||||
# package override triggers a rebuild of ghc and many Haskell
|
||||
# packages which takes a looong time.
|
||||
writeScript = name: text: "${pkgs.writeShellScript name text}";
|
||||
|
||||
# makeTahoeApp :: string -> attrset
|
||||
#
|
||||
# A helper function to define the Tahoe-LAFS runtime entrypoint for
|
||||
# a certain Python runtime.
|
||||
makeTahoeApp = pyVersion: {
|
||||
"tahoe-${pyVersion}" = {
|
||||
type = "app";
|
||||
program =
|
||||
writeScript "tahoe"
|
||||
''
|
||||
${makeRuntimeEnv' pyVersion}/bin/tahoe "$@"
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# makeUnitTestsApp :: string -> attrset
|
||||
#
|
||||
# A helper function to define the Tahoe-LAFS unit test entrypoint
|
||||
# for a certain Python runtime.
|
||||
makeUnitTestsApp = pyVersion: {
|
||||
"${unitTestName pyVersion}" = {
|
||||
type = "app";
|
||||
program =
|
||||
let
|
||||
python = "${makeTestEnv pyVersion}/bin/python";
|
||||
in
|
||||
writeScript "unit-tests"
|
||||
''
|
||||
${python} setup.py update_version
|
||||
export TAHOE_LAFS_HYPOTHESIS_PROFILE=ci
|
||||
export PYTHONPATH=$PWD/src
|
||||
${python} -m twisted.trial "$@"
|
||||
'';
|
||||
};
|
||||
};
|
||||
in
|
||||
# Merge a default app definition with the rest of the apps.
|
||||
mergeAttrs (
|
||||
[ { default = self.apps.${system}."tahoe-python3"; } ]
|
||||
++ (builtins.map makeUnitTestsApp pythonVersions)
|
||||
++ (builtins.map makeTahoeApp pythonVersions)
|
||||
);
|
||||
}));
|
||||
}
|
@ -1,35 +1,26 @@
|
||||
"""
|
||||
Ported to Python 3.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
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 __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from attr import frozen
|
||||
from time import sleep
|
||||
from os import mkdir, listdir, environ
|
||||
from os import mkdir, environ
|
||||
from os.path import join, exists
|
||||
from tempfile import mkdtemp, mktemp
|
||||
from functools import partial
|
||||
from json import loads
|
||||
|
||||
from foolscap.furl import (
|
||||
decode_furl,
|
||||
)
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from eliot import (
|
||||
to_file,
|
||||
log_call,
|
||||
)
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.procutils import which
|
||||
from twisted.internet.defer import DeferredList
|
||||
from twisted.internet.defer import DeferredList, succeed
|
||||
from twisted.internet.error import (
|
||||
ProcessExitedAlready,
|
||||
ProcessTerminated,
|
||||
@ -37,23 +28,32 @@ from twisted.internet.error import (
|
||||
|
||||
import pytest
|
||||
import pytest_twisted
|
||||
from typing import Mapping
|
||||
|
||||
from .util import (
|
||||
_CollectOutputProtocol,
|
||||
_MagicTextProtocol,
|
||||
_DumpOutputProtocol,
|
||||
_ProcessExitedProtocol,
|
||||
_create_node,
|
||||
_cleanup_tahoe_process,
|
||||
_tahoe_runner_optional_coverage,
|
||||
await_client_ready,
|
||||
TahoeProcess,
|
||||
cli,
|
||||
_run_node,
|
||||
generate_ssh_key,
|
||||
block_with_timeout,
|
||||
)
|
||||
from .grid import (
|
||||
create_flog_gatherer,
|
||||
create_grid,
|
||||
)
|
||||
from allmydata.node import read_config
|
||||
from allmydata.util.iputil import allocate_tcp_port
|
||||
|
||||
# No reason for HTTP requests to take longer than four minutes in the
|
||||
# integration tests. See allmydata/scripts/common_http.py for usage.
|
||||
os.environ["__TAHOE_CLI_HTTP_TIMEOUT"] = "240"
|
||||
|
||||
# Make Foolscap logging go into Twisted logging, so that integration test logs
|
||||
# include extra information
|
||||
# (https://github.com/warner/foolscap/blob/latest-release/doc/logging.rst):
|
||||
os.environ["FLOGTOTWISTED"] = "1"
|
||||
|
||||
# pytest customization hooks
|
||||
|
||||
@ -66,6 +66,29 @@ def pytest_addoption(parser):
|
||||
"--coverage", action="store_true", dest="coverage",
|
||||
help="Collect coverage statistics",
|
||||
)
|
||||
parser.addoption(
|
||||
"--force-foolscap", action="store_true", default=False,
|
||||
dest="force_foolscap",
|
||||
help=("If set, force Foolscap only for the storage protocol. " +
|
||||
"Otherwise HTTP will be used.")
|
||||
)
|
||||
parser.addoption(
|
||||
"--runslow", action="store_true", default=False,
|
||||
dest="runslow",
|
||||
help="If set, run tests marked as slow.",
|
||||
)
|
||||
|
||||
def pytest_collection_modifyitems(session, config, items):
|
||||
if not config.option.runslow:
|
||||
# The --runslow option was not given; keep only collected items not
|
||||
# marked as slow.
|
||||
items[:] = [
|
||||
item
|
||||
for item
|
||||
in items
|
||||
if item.get_closest_marker("slow") is None
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope='session')
|
||||
def eliot_logging():
|
||||
@ -89,9 +112,21 @@ def reactor():
|
||||
return _reactor
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@log_call(action_type=u"integration:port_allocator", include_result=False)
|
||||
def port_allocator(reactor):
|
||||
# these will appear basically random, which can make especially
|
||||
# manual debugging harder but we're re-using code instead of
|
||||
# writing our own...so, win?
|
||||
def allocate():
|
||||
port = allocate_tcp_port()
|
||||
return succeed(port)
|
||||
return allocate
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@log_call(action_type=u"integration:temp_dir", include_args=[])
|
||||
def temp_dir(request):
|
||||
def temp_dir(request) -> str:
|
||||
"""
|
||||
Invoke like 'py.test --keep-tempdir ...' to avoid deleting the temp-dir
|
||||
"""
|
||||
@ -123,154 +158,48 @@ def flog_binary():
|
||||
@pytest.fixture(scope='session')
|
||||
@log_call(action_type=u"integration:flog_gatherer", include_args=[])
|
||||
def flog_gatherer(reactor, temp_dir, flog_binary, request):
|
||||
out_protocol = _CollectOutputProtocol()
|
||||
gather_dir = join(temp_dir, 'flog_gather')
|
||||
reactor.spawnProcess(
|
||||
out_protocol,
|
||||
flog_binary,
|
||||
(
|
||||
'flogtool', 'create-gatherer',
|
||||
'--location', 'tcp:localhost:3117',
|
||||
'--port', '3117',
|
||||
gather_dir,
|
||||
)
|
||||
fg = pytest_twisted.blockon(
|
||||
create_flog_gatherer(reactor, request, temp_dir, flog_binary)
|
||||
)
|
||||
pytest_twisted.blockon(out_protocol.done)
|
||||
|
||||
twistd_protocol = _MagicTextProtocol("Gatherer waiting at")
|
||||
twistd_process = reactor.spawnProcess(
|
||||
twistd_protocol,
|
||||
which('twistd')[0],
|
||||
(
|
||||
'twistd', '--nodaemon', '--python',
|
||||
join(gather_dir, 'gatherer.tac'),
|
||||
),
|
||||
path=gather_dir,
|
||||
)
|
||||
pytest_twisted.blockon(twistd_protocol.magic_seen)
|
||||
|
||||
def cleanup():
|
||||
_cleanup_tahoe_process(twistd_process, twistd_protocol.exited)
|
||||
|
||||
flog_file = mktemp('.flog_dump')
|
||||
flog_protocol = _DumpOutputProtocol(open(flog_file, 'w'))
|
||||
flog_dir = join(temp_dir, 'flog_gather')
|
||||
flogs = [x for x in listdir(flog_dir) if x.endswith('.flog')]
|
||||
|
||||
print("Dumping {} flogtool logfiles to '{}'".format(len(flogs), flog_file))
|
||||
reactor.spawnProcess(
|
||||
flog_protocol,
|
||||
flog_binary,
|
||||
(
|
||||
'flogtool', 'dump', join(temp_dir, 'flog_gather', flogs[0])
|
||||
),
|
||||
)
|
||||
print("Waiting for flogtool to complete")
|
||||
try:
|
||||
block_with_timeout(flog_protocol.done, reactor)
|
||||
except ProcessTerminated as e:
|
||||
print("flogtool exited unexpectedly: {}".format(str(e)))
|
||||
print("Flogtool completed")
|
||||
|
||||
request.addfinalizer(cleanup)
|
||||
|
||||
with open(join(gather_dir, 'log_gatherer.furl'), 'r') as f:
|
||||
furl = f.read().strip()
|
||||
return furl
|
||||
return fg
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@log_call(
|
||||
action_type=u"integration:introducer",
|
||||
include_args=["temp_dir", "flog_gatherer"],
|
||||
include_result=False,
|
||||
)
|
||||
def introducer(reactor, temp_dir, flog_gatherer, request):
|
||||
config = '''
|
||||
[node]
|
||||
nickname = introducer0
|
||||
web.port = 4560
|
||||
log_gatherer.furl = {log_furl}
|
||||
'''.format(log_furl=flog_gatherer)
|
||||
@log_call(action_type=u"integration:grid", include_args=[])
|
||||
def grid(reactor, request, temp_dir, flog_gatherer, port_allocator):
|
||||
"""
|
||||
Provides a new Grid with a single Introducer and flog-gathering process.
|
||||
|
||||
intro_dir = join(temp_dir, 'introducer')
|
||||
print("making introducer", intro_dir)
|
||||
|
||||
if not exists(intro_dir):
|
||||
mkdir(intro_dir)
|
||||
done_proto = _ProcessExitedProtocol()
|
||||
_tahoe_runner_optional_coverage(
|
||||
done_proto,
|
||||
reactor,
|
||||
request,
|
||||
(
|
||||
'create-introducer',
|
||||
'--listen=tcp',
|
||||
'--hostname=localhost',
|
||||
intro_dir,
|
||||
),
|
||||
)
|
||||
pytest_twisted.blockon(done_proto.done)
|
||||
|
||||
# over-write the config file with our stuff
|
||||
with open(join(intro_dir, 'tahoe.cfg'), 'w') as f:
|
||||
f.write(config)
|
||||
|
||||
# "tahoe run" is consistent across Linux/macOS/Windows, unlike the old
|
||||
# "start" command.
|
||||
protocol = _MagicTextProtocol('introducer running')
|
||||
transport = _tahoe_runner_optional_coverage(
|
||||
protocol,
|
||||
reactor,
|
||||
request,
|
||||
(
|
||||
'run',
|
||||
intro_dir,
|
||||
),
|
||||
Notably does _not_ provide storage servers; use the storage_nodes
|
||||
fixture if your tests need a Grid that can be used for puts / gets.
|
||||
"""
|
||||
g = pytest_twisted.blockon(
|
||||
create_grid(reactor, request, temp_dir, flog_gatherer, port_allocator)
|
||||
)
|
||||
request.addfinalizer(partial(_cleanup_tahoe_process, transport, protocol.exited))
|
||||
return g
|
||||
|
||||
pytest_twisted.blockon(protocol.magic_seen)
|
||||
return TahoeProcess(transport, intro_dir)
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def introducer(grid):
|
||||
return grid.introducer
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@log_call(action_type=u"integration:introducer:furl", include_args=["temp_dir"])
|
||||
def introducer_furl(introducer, temp_dir):
|
||||
furl_fname = join(temp_dir, 'introducer', 'private', 'introducer.furl')
|
||||
while not exists(furl_fname):
|
||||
print("Don't see {} yet".format(furl_fname))
|
||||
sleep(.1)
|
||||
furl = open(furl_fname, 'r').read()
|
||||
tubID, location_hints, name = decode_furl(furl)
|
||||
if not location_hints:
|
||||
# If there are no location hints then nothing can ever possibly
|
||||
# connect to it and the only thing that can happen next is something
|
||||
# will hang or time out. So just give up right now.
|
||||
raise ValueError(
|
||||
"Introducer ({!r}) fURL has no location hints!".format(
|
||||
introducer_furl,
|
||||
),
|
||||
)
|
||||
return furl
|
||||
return introducer.furl
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
@log_call(
|
||||
action_type=u"integration:tor:introducer",
|
||||
include_args=["temp_dir", "flog_gatherer"],
|
||||
include_result=False,
|
||||
)
|
||||
def tor_introducer(reactor, temp_dir, flog_gatherer, request):
|
||||
config = '''
|
||||
[node]
|
||||
nickname = introducer_tor
|
||||
web.port = 4561
|
||||
log_gatherer.furl = {log_furl}
|
||||
'''.format(log_furl=flog_gatherer)
|
||||
|
||||
def tor_introducer(reactor, temp_dir, flog_gatherer, request, tor_network):
|
||||
intro_dir = join(temp_dir, 'introducer_tor')
|
||||
print("making introducer", intro_dir)
|
||||
print("making Tor introducer in {}".format(intro_dir))
|
||||
print("(this can take tens of seconds to allocate Onion address)")
|
||||
|
||||
if not exists(intro_dir):
|
||||
mkdir(intro_dir)
|
||||
@ -281,20 +210,23 @@ log_gatherer.furl = {log_furl}
|
||||
request,
|
||||
(
|
||||
'create-introducer',
|
||||
'--tor-control-port', 'tcp:localhost:8010',
|
||||
'--tor-control-port', tor_network.client_control_endpoint,
|
||||
'--hide-ip',
|
||||
'--listen=tor',
|
||||
intro_dir,
|
||||
),
|
||||
)
|
||||
pytest_twisted.blockon(done_proto.done)
|
||||
|
||||
# over-write the config file with our stuff
|
||||
with open(join(intro_dir, 'tahoe.cfg'), 'w') as f:
|
||||
f.write(config)
|
||||
# adjust a few settings
|
||||
config = read_config(intro_dir, "tub.port")
|
||||
config.set_config("node", "nickname", "introducer-tor")
|
||||
config.set_config("node", "web.port", "4561")
|
||||
config.set_config("node", "log_gatherer.furl", flog_gatherer.furl)
|
||||
|
||||
# "tahoe run" is consistent across Linux/macOS/Windows, unlike the old
|
||||
# "start" command.
|
||||
protocol = _MagicTextProtocol('introducer running')
|
||||
protocol = _MagicTextProtocol('introducer running', "tor_introducer")
|
||||
transport = _tahoe_runner_optional_coverage(
|
||||
protocol,
|
||||
reactor,
|
||||
@ -313,101 +245,51 @@ log_gatherer.furl = {log_furl}
|
||||
pass
|
||||
request.addfinalizer(cleanup)
|
||||
|
||||
print("Waiting for introducer to be ready...")
|
||||
pytest_twisted.blockon(protocol.magic_seen)
|
||||
print("Introducer ready.")
|
||||
return transport
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def tor_introducer_furl(tor_introducer, temp_dir):
|
||||
furl_fname = join(temp_dir, 'introducer_tor', 'private', 'introducer.furl')
|
||||
while not exists(furl_fname):
|
||||
print("Don't see {} yet".format(furl_fname))
|
||||
sleep(.1)
|
||||
furl = open(furl_fname, 'r').read()
|
||||
print(f"Found Tor introducer furl: {furl} in {furl_fname}")
|
||||
return furl
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@log_call(
|
||||
action_type=u"integration:storage_nodes",
|
||||
include_args=["temp_dir", "introducer_furl", "flog_gatherer"],
|
||||
include_args=["grid"],
|
||||
include_result=False,
|
||||
)
|
||||
def storage_nodes(reactor, temp_dir, introducer, introducer_furl, flog_gatherer, request):
|
||||
def storage_nodes(grid):
|
||||
nodes_d = []
|
||||
# start all 5 nodes in parallel
|
||||
for x in range(5):
|
||||
name = 'node{}'.format(x)
|
||||
web_port= 9990 + x
|
||||
nodes_d.append(
|
||||
_create_node(
|
||||
reactor, request, temp_dir, introducer_furl, flog_gatherer, name,
|
||||
web_port="tcp:{}:interface=localhost".format(web_port),
|
||||
storage=True,
|
||||
)
|
||||
)
|
||||
nodes_d.append(grid.add_storage_node())
|
||||
|
||||
nodes_status = pytest_twisted.blockon(DeferredList(nodes_d))
|
||||
nodes = []
|
||||
for ok, process in nodes_status:
|
||||
assert ok, "Storage node creation failed: {}".format(process)
|
||||
nodes.append(process)
|
||||
return nodes
|
||||
for ok, value in nodes_status:
|
||||
assert ok, "Storage node creation failed: {}".format(value)
|
||||
return grid.storage_servers
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@log_call(action_type=u"integration:alice", include_args=[], include_result=False)
|
||||
def alice(reactor, temp_dir, introducer_furl, flog_gatherer, storage_nodes, request):
|
||||
process = pytest_twisted.blockon(
|
||||
_create_node(
|
||||
reactor, request, temp_dir, introducer_furl, flog_gatherer, "alice",
|
||||
web_port="tcp:9980:interface=localhost",
|
||||
storage=False,
|
||||
# We're going to kill this ourselves, so no need for finalizer to
|
||||
# do it:
|
||||
finalize=False,
|
||||
)
|
||||
)
|
||||
await_client_ready(process)
|
||||
|
||||
# 1. Create a new RW directory cap:
|
||||
cli(process, "create-alias", "test")
|
||||
rwcap = loads(cli(process, "list-aliases", "--json"))["test"]["readwrite"]
|
||||
|
||||
# 2. Enable SFTP on the node:
|
||||
host_ssh_key_path = join(process.node_dir, "private", "ssh_host_rsa_key")
|
||||
accounts_path = join(process.node_dir, "private", "accounts")
|
||||
with open(join(process.node_dir, "tahoe.cfg"), "a") as f:
|
||||
f.write("""\
|
||||
[sftpd]
|
||||
enabled = true
|
||||
port = tcp:8022:interface=127.0.0.1
|
||||
host_pubkey_file = {ssh_key_path}.pub
|
||||
host_privkey_file = {ssh_key_path}
|
||||
accounts.file = {accounts_path}
|
||||
""".format(ssh_key_path=host_ssh_key_path, accounts_path=accounts_path))
|
||||
generate_ssh_key(host_ssh_key_path)
|
||||
|
||||
# 3. Add a SFTP access file with username/password and SSH key auth.
|
||||
|
||||
# The client SSH key path is typically going to be somewhere else (~/.ssh,
|
||||
# typically), but for convenience sake for testing we'll put it inside node.
|
||||
client_ssh_key_path = join(process.node_dir, "private", "ssh_client_rsa_key")
|
||||
generate_ssh_key(client_ssh_key_path)
|
||||
# Pub key format is "ssh-rsa <thekey> <username>". We want the key.
|
||||
ssh_public_key = open(client_ssh_key_path + ".pub").read().strip().split()[1]
|
||||
with open(accounts_path, "w") as f:
|
||||
f.write("""\
|
||||
alice password {rwcap}
|
||||
|
||||
alice2 ssh-rsa {ssh_public_key} {rwcap}
|
||||
""".format(rwcap=rwcap, ssh_public_key=ssh_public_key))
|
||||
|
||||
# 4. Restart the node with new SFTP config.
|
||||
process.kill()
|
||||
pytest_twisted.blockon(_run_node(reactor, process.node_dir, request, None))
|
||||
|
||||
await_client_ready(process)
|
||||
return process
|
||||
def alice(reactor, request, grid, storage_nodes):
|
||||
"""
|
||||
:returns grid.Client: the associated instance for Alice
|
||||
"""
|
||||
alice = pytest_twisted.blockon(grid.add_client("alice"))
|
||||
pytest_twisted.blockon(alice.add_sftp(reactor, request))
|
||||
print(f"Alice pid: {alice.process.transport.pid}")
|
||||
return alice
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@ -420,22 +302,43 @@ def bob(reactor, temp_dir, introducer_furl, flog_gatherer, storage_nodes, reques
|
||||
storage=False,
|
||||
)
|
||||
)
|
||||
await_client_ready(process)
|
||||
pytest_twisted.blockon(await_client_ready(process))
|
||||
return process
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.mark.skipif(sys.platform.startswith('win'),
|
||||
'Tor tests are unstable on Windows')
|
||||
def chutney(reactor, temp_dir):
|
||||
def chutney(reactor, temp_dir: str) -> tuple[str, dict[str, str]]:
|
||||
"""
|
||||
Install the Chutney software that is required to run a small local Tor grid.
|
||||
|
||||
(Chutney lacks the normal "python stuff" so we can't just declare
|
||||
it in Tox or similar dependencies)
|
||||
"""
|
||||
# Try to find Chutney already installed in the environment.
|
||||
try:
|
||||
import chutney
|
||||
except ImportError:
|
||||
# Nope, we'll get our own in a moment.
|
||||
pass
|
||||
else:
|
||||
# We already have one, just use it.
|
||||
return (
|
||||
# from `checkout/lib/chutney/__init__.py` we want to get back to
|
||||
# `checkout` because that's the parent of the directory with all
|
||||
# of the network definitions. So, great-grand-parent.
|
||||
FilePath(chutney.__file__).parent().parent().parent().path,
|
||||
# There's nothing to add to the environment.
|
||||
{},
|
||||
)
|
||||
|
||||
chutney_dir = join(temp_dir, 'chutney')
|
||||
mkdir(chutney_dir)
|
||||
|
||||
# TODO:
|
||||
|
||||
# check for 'tor' binary explicitly and emit a "skip" if we can't
|
||||
# find it
|
||||
missing = [exe for exe in ["tor", "tor-gencert"] if not which(exe)]
|
||||
if missing:
|
||||
pytest.skip(f"Some command-line tools not found: {missing}")
|
||||
|
||||
# XXX yuck! should add a setup.py to chutney so we can at least
|
||||
# "pip install <path to tarball>" and/or depend on chutney in "pip
|
||||
@ -448,17 +351,15 @@ def chutney(reactor, temp_dir):
|
||||
'git',
|
||||
(
|
||||
'git', 'clone',
|
||||
'https://git.torproject.org/chutney.git',
|
||||
'https://gitlab.torproject.org/tpo/core/chutney.git',
|
||||
chutney_dir,
|
||||
),
|
||||
env=environ,
|
||||
)
|
||||
pytest_twisted.blockon(proto.done)
|
||||
|
||||
# XXX: Here we reset Chutney to the last revision known to work
|
||||
# with Python 2, as a workaround for Chutney moving to Python 3.
|
||||
# When this is no longer necessary, we will have to drop this and
|
||||
# add '--depth=1' back to the above 'git clone' subprocess.
|
||||
# XXX: Here we reset Chutney to a specific revision known to work,
|
||||
# since there are no stability guarantees or releases yet.
|
||||
proto = _DumpOutputProtocol(None)
|
||||
reactor.spawnProcess(
|
||||
proto,
|
||||
@ -466,94 +367,131 @@ def chutney(reactor, temp_dir):
|
||||
(
|
||||
'git', '-C', chutney_dir,
|
||||
'reset', '--hard',
|
||||
'99bd06c7554b9113af8c0877b6eca4ceb95dcbaa'
|
||||
'c4f6789ad2558dcbfeb7d024c6481d8112bfb6c2'
|
||||
),
|
||||
env=environ,
|
||||
)
|
||||
pytest_twisted.blockon(proto.done)
|
||||
|
||||
return chutney_dir
|
||||
return chutney_dir, {"PYTHONPATH": join(chutney_dir, "lib")}
|
||||
|
||||
|
||||
@frozen
|
||||
class ChutneyTorNetwork:
|
||||
"""
|
||||
Represents a running Chutney (tor) network. Returned by the
|
||||
"tor_network" fixture.
|
||||
"""
|
||||
dir: FilePath
|
||||
environ: Mapping[str, str]
|
||||
client_control_port: int
|
||||
|
||||
@property
|
||||
def client_control_endpoint(self) -> str:
|
||||
return "tcp:localhost:{}".format(self.client_control_port)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.mark.skipif(sys.platform.startswith('win'),
|
||||
reason='Tor tests are unstable on Windows')
|
||||
def tor_network(reactor, temp_dir, chutney, request):
|
||||
"""
|
||||
Build a basic Tor network.
|
||||
|
||||
# this is the actual "chutney" script at the root of a chutney checkout
|
||||
chutney_dir = chutney
|
||||
chut = join(chutney_dir, 'chutney')
|
||||
Instantiate the "networks/basic" Chutney configuration for a local
|
||||
Tor network.
|
||||
|
||||
# now, as per Chutney's README, we have to create the network
|
||||
# ./chutney configure networks/basic
|
||||
# ./chutney start networks/basic
|
||||
This provides a small, local Tor network that can run v3 Onion
|
||||
Services. It has 3 authorities, 5 relays and 2 clients.
|
||||
|
||||
The 'chutney' fixture pins a Chutney git qrevision, so things
|
||||
shouldn't change. This network has two clients which are the only
|
||||
nodes with valid SocksPort configuration ("008c" and "009c" 9008
|
||||
and 9009)
|
||||
|
||||
The control ports start at 8000 (so the ControlPort for the client
|
||||
nodes are 8008 and 8009).
|
||||
|
||||
:param chutney: The root directory of a Chutney checkout and a dict of
|
||||
additional environment variables to set so a Python process can use
|
||||
it.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
chutney_root, chutney_env = chutney
|
||||
basic_network = join(chutney_root, 'networks', 'basic')
|
||||
|
||||
env = environ.copy()
|
||||
env.update({"PYTHONPATH": join(chutney_dir, "lib")})
|
||||
proto = _DumpOutputProtocol(None)
|
||||
reactor.spawnProcess(
|
||||
proto,
|
||||
sys.executable,
|
||||
(
|
||||
sys.executable, '-m', 'chutney.TorNet', 'configure',
|
||||
join(chutney_dir, 'networks', 'basic'),
|
||||
),
|
||||
path=join(chutney_dir),
|
||||
env=env,
|
||||
)
|
||||
pytest_twisted.blockon(proto.done)
|
||||
|
||||
proto = _DumpOutputProtocol(None)
|
||||
reactor.spawnProcess(
|
||||
proto,
|
||||
sys.executable,
|
||||
(
|
||||
sys.executable, '-m', 'chutney.TorNet', 'start',
|
||||
join(chutney_dir, 'networks', 'basic'),
|
||||
),
|
||||
path=join(chutney_dir),
|
||||
env=env,
|
||||
)
|
||||
pytest_twisted.blockon(proto.done)
|
||||
|
||||
# print some useful stuff
|
||||
proto = _CollectOutputProtocol()
|
||||
reactor.spawnProcess(
|
||||
proto,
|
||||
sys.executable,
|
||||
(
|
||||
sys.executable, '-m', 'chutney.TorNet', 'status',
|
||||
join(chutney_dir, 'networks', 'basic'),
|
||||
),
|
||||
path=join(chutney_dir),
|
||||
env=env,
|
||||
)
|
||||
try:
|
||||
pytest_twisted.blockon(proto.done)
|
||||
except ProcessTerminated:
|
||||
print("Chutney.TorNet status failed (continuing):")
|
||||
print(proto.output.getvalue())
|
||||
|
||||
def cleanup():
|
||||
print("Tearing down Chutney Tor network")
|
||||
proto = _CollectOutputProtocol()
|
||||
env.update(chutney_env)
|
||||
env.update({
|
||||
# default is 60, probably too short for reliable automated use.
|
||||
"CHUTNEY_START_TIME": "600",
|
||||
})
|
||||
chutney_argv = (sys.executable, '-m', 'chutney.TorNet')
|
||||
def chutney(argv):
|
||||
proto = _DumpOutputProtocol(None)
|
||||
reactor.spawnProcess(
|
||||
proto,
|
||||
sys.executable,
|
||||
(
|
||||
sys.executable, '-m', 'chutney.TorNet', 'stop',
|
||||
join(chutney_dir, 'networks', 'basic'),
|
||||
),
|
||||
path=join(chutney_dir),
|
||||
chutney_argv + argv,
|
||||
path=join(chutney_root),
|
||||
env=env,
|
||||
)
|
||||
return proto.done
|
||||
|
||||
# now, as per Chutney's README, we have to create the network
|
||||
pytest_twisted.blockon(chutney(("configure", basic_network)))
|
||||
|
||||
# before we start the network, ensure we will tear down at the end
|
||||
def cleanup():
|
||||
print("Tearing down Chutney Tor network")
|
||||
try:
|
||||
block_with_timeout(proto.done, reactor)
|
||||
block_with_timeout(chutney(("stop", basic_network)), reactor)
|
||||
except ProcessTerminated:
|
||||
# If this doesn't exit cleanly, that's fine, that shouldn't fail
|
||||
# the test suite.
|
||||
pass
|
||||
|
||||
request.addfinalizer(cleanup)
|
||||
|
||||
return chut
|
||||
pytest_twisted.blockon(chutney(("start", basic_network)))
|
||||
|
||||
# Wait for the nodes to "bootstrap" - ie, form a network among themselves.
|
||||
# Successful bootstrap is reported with a message something like:
|
||||
#
|
||||
# Everything bootstrapped after 151 sec
|
||||
# Bootstrap finished: 151 seconds
|
||||
# Node status:
|
||||
# test000a : 100, done , Done
|
||||
# test001a : 100, done , Done
|
||||
# test002a : 100, done , Done
|
||||
# test003r : 100, done , Done
|
||||
# test004r : 100, done , Done
|
||||
# test005r : 100, done , Done
|
||||
# test006r : 100, done , Done
|
||||
# test007r : 100, done , Done
|
||||
# test008c : 100, done , Done
|
||||
# test009c : 100, done , Done
|
||||
# Published dir info:
|
||||
# test000a : 100, all nodes , desc md md_cons ns_cons , Dir info cached
|
||||
# test001a : 100, all nodes , desc md md_cons ns_cons , Dir info cached
|
||||
# test002a : 100, all nodes , desc md md_cons ns_cons , Dir info cached
|
||||
# test003r : 100, all nodes , desc md md_cons ns_cons , Dir info cached
|
||||
# test004r : 100, all nodes , desc md md_cons ns_cons , Dir info cached
|
||||
# test005r : 100, all nodes , desc md md_cons ns_cons , Dir info cached
|
||||
# test006r : 100, all nodes , desc md md_cons ns_cons , Dir info cached
|
||||
# test007r : 100, all nodes , desc md md_cons ns_cons , Dir info cached
|
||||
pytest_twisted.blockon(chutney(("wait_for_bootstrap", basic_network)))
|
||||
|
||||
# print some useful stuff
|
||||
try:
|
||||
pytest_twisted.blockon(chutney(("status", basic_network)))
|
||||
except ProcessTerminated:
|
||||
print("Chutney.TorNet status failed (continuing)")
|
||||
|
||||
# the "8008" comes from configuring "networks/basic" in chutney
|
||||
# and then examining "net/nodes/008c/torrc" for ControlPort value
|
||||
return ChutneyTorNetwork(
|
||||
chutney_root,
|
||||
chutney_env,
|
||||
8008,
|
||||
)
|
||||
|
529
integration/grid.py
Normal file
529
integration/grid.py
Normal file
@ -0,0 +1,529 @@
|
||||
"""
|
||||
Classes which directly represent various kinds of Tahoe processes
|
||||
that co-operate to for "a Grid".
|
||||
|
||||
These methods and objects are used by conftest.py fixtures but may
|
||||
also be used as direct helpers for tests that don't want to (or can't)
|
||||
rely on 'the' global grid as provided by fixtures like 'alice' or
|
||||
'storage_servers'.
|
||||
"""
|
||||
|
||||
from os import mkdir, listdir
|
||||
from os.path import join, exists
|
||||
from json import loads
|
||||
from tempfile import mktemp
|
||||
from time import sleep
|
||||
|
||||
from eliot import (
|
||||
log_call,
|
||||
)
|
||||
|
||||
from foolscap.furl import (
|
||||
decode_furl,
|
||||
)
|
||||
|
||||
from twisted.python.procutils import which
|
||||
from twisted.internet.defer import (
|
||||
inlineCallbacks,
|
||||
returnValue,
|
||||
Deferred,
|
||||
)
|
||||
from twisted.internet.task import (
|
||||
deferLater,
|
||||
)
|
||||
from twisted.internet.interfaces import (
|
||||
IProcessTransport,
|
||||
IProcessProtocol,
|
||||
)
|
||||
from twisted.internet.error import ProcessTerminated
|
||||
|
||||
from allmydata.util.attrs_provides import (
|
||||
provides,
|
||||
)
|
||||
from allmydata.node import read_config
|
||||
from .util import (
|
||||
_CollectOutputProtocol,
|
||||
_MagicTextProtocol,
|
||||
_DumpOutputProtocol,
|
||||
_ProcessExitedProtocol,
|
||||
_run_node,
|
||||
_cleanup_tahoe_process,
|
||||
_tahoe_runner_optional_coverage,
|
||||
TahoeProcess,
|
||||
await_client_ready,
|
||||
generate_ssh_key,
|
||||
cli,
|
||||
reconfigure,
|
||||
_create_node,
|
||||
)
|
||||
|
||||
import attr
|
||||
import pytest_twisted
|
||||
|
||||
|
||||
# currently, we pass a "request" around a bunch but it seems to only
|
||||
# be for addfinalizer() calls.
|
||||
# - is "keeping" a request like that okay? What if it's a session-scoped one?
|
||||
# (i.e. in Grid etc)
|
||||
# - maybe limit to "a callback to hang your cleanup off of" (instead of request)?
|
||||
|
||||
|
||||
@attr.s
|
||||
class FlogGatherer(object):
|
||||
"""
|
||||
Flog Gatherer process.
|
||||
"""
|
||||
process = attr.ib(
|
||||
validator=provides(IProcessTransport)
|
||||
)
|
||||
protocol = attr.ib(
|
||||
validator=provides(IProcessProtocol)
|
||||
)
|
||||
furl = attr.ib()
|
||||
|
||||
|
||||
@inlineCallbacks
|
||||
def create_flog_gatherer(reactor, request, temp_dir, flog_binary):
|
||||
out_protocol = _CollectOutputProtocol()
|
||||
gather_dir = join(temp_dir, 'flog_gather')
|
||||
reactor.spawnProcess(
|
||||
out_protocol,
|
||||
flog_binary,
|
||||
(
|
||||
'flogtool', 'create-gatherer',
|
||||
'--location', 'tcp:localhost:3117',
|
||||
'--port', '3117',
|
||||
gather_dir,
|
||||
)
|
||||
)
|
||||
yield out_protocol.done
|
||||
|
||||
twistd_protocol = _MagicTextProtocol("Gatherer waiting at", "gatherer")
|
||||
twistd_process = reactor.spawnProcess(
|
||||
twistd_protocol,
|
||||
which('twistd')[0],
|
||||
(
|
||||
'twistd', '--nodaemon', '--python',
|
||||
join(gather_dir, 'gatherer.tac'),
|
||||
),
|
||||
path=gather_dir,
|
||||
)
|
||||
yield twistd_protocol.magic_seen
|
||||
|
||||
def cleanup():
|
||||
_cleanup_tahoe_process(twistd_process, twistd_protocol.exited)
|
||||
|
||||
flog_file = mktemp('.flog_dump')
|
||||
flog_protocol = _DumpOutputProtocol(open(flog_file, 'w'))
|
||||
flog_dir = join(temp_dir, 'flog_gather')
|
||||
flogs = [x for x in listdir(flog_dir) if x.endswith('.flog')]
|
||||
|
||||
print("Dumping {} flogtool logfiles to '{}'".format(len(flogs), flog_file))
|
||||
for flog_path in flogs:
|
||||
reactor.spawnProcess(
|
||||
flog_protocol,
|
||||
flog_binary,
|
||||
(
|
||||
'flogtool', 'dump', join(temp_dir, 'flog_gather', flog_path)
|
||||
),
|
||||
)
|
||||
print("Waiting for flogtool to complete")
|
||||
try:
|
||||
pytest_twisted.blockon(flog_protocol.done)
|
||||
except ProcessTerminated as e:
|
||||
print("flogtool exited unexpectedly: {}".format(str(e)))
|
||||
print("Flogtool completed")
|
||||
|
||||
request.addfinalizer(cleanup)
|
||||
|
||||
with open(join(gather_dir, 'log_gatherer.furl'), 'r') as f:
|
||||
furl = f.read().strip()
|
||||
returnValue(
|
||||
FlogGatherer(
|
||||
protocol=twistd_protocol,
|
||||
process=twistd_process,
|
||||
furl=furl,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class StorageServer(object):
|
||||
"""
|
||||
Represents a Tahoe Storage Server
|
||||
"""
|
||||
|
||||
process = attr.ib(
|
||||
validator=attr.validators.instance_of(TahoeProcess)
|
||||
)
|
||||
protocol = attr.ib(
|
||||
validator=provides(IProcessProtocol)
|
||||
)
|
||||
|
||||
@inlineCallbacks
|
||||
def restart(self, reactor, request):
|
||||
"""
|
||||
re-start our underlying process by issuing a TERM, waiting and
|
||||
then running again. await_client_ready() will be done as well
|
||||
|
||||
Note that self.process and self.protocol will be new instances
|
||||
after this.
|
||||
"""
|
||||
self.process.transport.signalProcess('TERM')
|
||||
yield self.protocol.exited
|
||||
self.process = yield _run_node(
|
||||
reactor, self.process.node_dir, request, None,
|
||||
)
|
||||
self.protocol = self.process.transport.proto
|
||||
yield await_client_ready(self.process)
|
||||
|
||||
|
||||
@inlineCallbacks
|
||||
def create_storage_server(reactor, request, temp_dir, introducer, flog_gatherer, name, web_port,
|
||||
needed=2, happy=3, total=4):
|
||||
"""
|
||||
Create a new storage server
|
||||
"""
|
||||
node_process = yield _create_node(
|
||||
reactor, request, temp_dir, introducer.furl, flog_gatherer,
|
||||
name, web_port, storage=True, needed=needed, happy=happy, total=total,
|
||||
)
|
||||
storage = StorageServer(
|
||||
process=node_process,
|
||||
# node_process is a TahoeProcess. its transport is an
|
||||
# IProcessTransport. in practice, this means it is a
|
||||
# twisted.internet._baseprocess.BaseProcess. BaseProcess records the
|
||||
# process protocol as its proto attribute.
|
||||
protocol=node_process.transport.proto,
|
||||
)
|
||||
returnValue(storage)
|
||||
|
||||
|
||||
@attr.s
|
||||
class Client(object):
|
||||
"""
|
||||
Represents a Tahoe client
|
||||
"""
|
||||
|
||||
process = attr.ib(
|
||||
validator=attr.validators.instance_of(TahoeProcess)
|
||||
)
|
||||
protocol = attr.ib(
|
||||
validator=provides(IProcessProtocol)
|
||||
)
|
||||
request = attr.ib() # original request, for addfinalizer()
|
||||
|
||||
## XXX convenience? or confusion?
|
||||
# @property
|
||||
# def node_dir(self):
|
||||
# return self.process.node_dir
|
||||
|
||||
@inlineCallbacks
|
||||
def reconfigure_zfec(self, reactor, zfec_params, convergence=None, max_segment_size=None):
|
||||
"""
|
||||
Reconfigure the ZFEC parameters for this node
|
||||
"""
|
||||
# XXX this is a stop-gap to keep tests running "as is"
|
||||
# -> we should fix the tests so that they create a new client
|
||||
# in the grid with the required parameters, instead of
|
||||
# re-configuring Alice (or whomever)
|
||||
|
||||
rtn = yield Deferred.fromCoroutine(
|
||||
reconfigure(reactor, self.request, self.process, zfec_params, convergence, max_segment_size)
|
||||
)
|
||||
return rtn
|
||||
|
||||
@inlineCallbacks
|
||||
def restart(self, reactor, request, servers=1):
|
||||
"""
|
||||
re-start our underlying process by issuing a TERM, waiting and
|
||||
then running again.
|
||||
|
||||
:param int servers: number of server connections we will wait
|
||||
for before being 'ready'
|
||||
|
||||
Note that self.process and self.protocol will be new instances
|
||||
after this.
|
||||
"""
|
||||
# XXX similar to above, can we make this return a new instance
|
||||
# instead of mutating?
|
||||
self.process.transport.signalProcess('TERM')
|
||||
yield self.protocol.exited
|
||||
process = yield _run_node(
|
||||
reactor, self.process.node_dir, request, None,
|
||||
)
|
||||
self.process = process
|
||||
self.protocol = self.process.transport.proto
|
||||
yield await_client_ready(self.process, minimum_number_of_servers=servers)
|
||||
|
||||
@inlineCallbacks
|
||||
def add_sftp(self, reactor, request):
|
||||
"""
|
||||
"""
|
||||
# if other things need to add or change configuration, further
|
||||
# refactoring could be useful here (i.e. move reconfigure
|
||||
# parts to their own functions)
|
||||
|
||||
# XXX why do we need an alias?
|
||||
# 1. Create a new RW directory cap:
|
||||
cli(self.process, "create-alias", "test")
|
||||
rwcap = loads(cli(self.process, "list-aliases", "--json"))["test"]["readwrite"]
|
||||
|
||||
# 2. Enable SFTP on the node:
|
||||
host_ssh_key_path = join(self.process.node_dir, "private", "ssh_host_rsa_key")
|
||||
sftp_client_key_path = join(self.process.node_dir, "private", "ssh_client_rsa_key")
|
||||
accounts_path = join(self.process.node_dir, "private", "accounts")
|
||||
with open(join(self.process.node_dir, "tahoe.cfg"), "a") as f:
|
||||
f.write(
|
||||
("\n\n[sftpd]\n"
|
||||
"enabled = true\n"
|
||||
"port = tcp:8022:interface=127.0.0.1\n"
|
||||
"host_pubkey_file = {ssh_key_path}.pub\n"
|
||||
"host_privkey_file = {ssh_key_path}\n"
|
||||
"accounts.file = {accounts_path}\n").format(
|
||||
ssh_key_path=host_ssh_key_path,
|
||||
accounts_path=accounts_path,
|
||||
)
|
||||
)
|
||||
generate_ssh_key(host_ssh_key_path)
|
||||
|
||||
# 3. Add a SFTP access file with an SSH key for auth.
|
||||
generate_ssh_key(sftp_client_key_path)
|
||||
# Pub key format is "ssh-rsa <thekey> <username>". We want the key.
|
||||
with open(sftp_client_key_path + ".pub") as pubkey_file:
|
||||
ssh_public_key = pubkey_file.read().strip().split()[1]
|
||||
with open(accounts_path, "w") as f:
|
||||
f.write(
|
||||
"alice-key ssh-rsa {ssh_public_key} {rwcap}\n".format(
|
||||
rwcap=rwcap,
|
||||
ssh_public_key=ssh_public_key,
|
||||
)
|
||||
)
|
||||
|
||||
# 4. Restart the node with new SFTP config.
|
||||
print("restarting for SFTP")
|
||||
yield self.restart(reactor, request)
|
||||
print("restart done")
|
||||
# XXX i think this is broken because we're "waiting for ready" during first bootstrap? or something?
|
||||
|
||||
|
||||
@inlineCallbacks
|
||||
def create_client(reactor, request, temp_dir, introducer, flog_gatherer, name, web_port,
|
||||
needed=2, happy=3, total=4):
|
||||
"""
|
||||
Create a new storage server
|
||||
"""
|
||||
from .util import _create_node
|
||||
node_process = yield _create_node(
|
||||
reactor, request, temp_dir, introducer.furl, flog_gatherer,
|
||||
name, web_port, storage=False, needed=needed, happy=happy, total=total,
|
||||
)
|
||||
returnValue(
|
||||
Client(
|
||||
process=node_process,
|
||||
protocol=node_process.transport.proto,
|
||||
request=request,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class Introducer(object):
|
||||
"""
|
||||
Reprsents a running introducer
|
||||
"""
|
||||
|
||||
process = attr.ib(
|
||||
validator=attr.validators.instance_of(TahoeProcess)
|
||||
)
|
||||
protocol = attr.ib(
|
||||
validator=provides(IProcessProtocol)
|
||||
)
|
||||
furl = attr.ib()
|
||||
|
||||
|
||||
def _validate_furl(furl_fname):
|
||||
"""
|
||||
Opens and validates a fURL, ensuring location hints.
|
||||
:returns: the furl
|
||||
:raises: ValueError if no location hints
|
||||
"""
|
||||
while not exists(furl_fname):
|
||||
print("Don't see {} yet".format(furl_fname))
|
||||
sleep(.1)
|
||||
furl = open(furl_fname, 'r').read()
|
||||
tubID, location_hints, name = decode_furl(furl)
|
||||
if not location_hints:
|
||||
# If there are no location hints then nothing can ever possibly
|
||||
# connect to it and the only thing that can happen next is something
|
||||
# will hang or time out. So just give up right now.
|
||||
raise ValueError(
|
||||
"Introducer ({!r}) fURL has no location hints!".format(
|
||||
furl,
|
||||
),
|
||||
)
|
||||
return furl
|
||||
|
||||
|
||||
@inlineCallbacks
|
||||
@log_call(
|
||||
action_type=u"integration:introducer",
|
||||
include_args=["temp_dir", "flog_gatherer"],
|
||||
include_result=False,
|
||||
)
|
||||
def create_introducer(reactor, request, temp_dir, flog_gatherer, port):
|
||||
"""
|
||||
Run a new Introducer and return an Introducer instance.
|
||||
"""
|
||||
intro_dir = join(temp_dir, 'introducer{}'.format(port))
|
||||
|
||||
if not exists(intro_dir):
|
||||
mkdir(intro_dir)
|
||||
done_proto = _ProcessExitedProtocol()
|
||||
_tahoe_runner_optional_coverage(
|
||||
done_proto,
|
||||
reactor,
|
||||
request,
|
||||
(
|
||||
'create-introducer',
|
||||
'--listen=tcp',
|
||||
'--hostname=localhost',
|
||||
intro_dir,
|
||||
),
|
||||
)
|
||||
yield done_proto.done
|
||||
|
||||
config = read_config(intro_dir, "tub.port")
|
||||
config.set_config("node", "nickname", f"introducer-{port}")
|
||||
config.set_config("node", "web.port", f"{port}")
|
||||
config.set_config("node", "log_gatherer.furl", flog_gatherer.furl)
|
||||
|
||||
# on windows, "tahoe start" means: run forever in the foreground,
|
||||
# but on linux it means daemonize. "tahoe run" is consistent
|
||||
# between platforms.
|
||||
protocol = _MagicTextProtocol('introducer running', "introducer")
|
||||
transport = _tahoe_runner_optional_coverage(
|
||||
protocol,
|
||||
reactor,
|
||||
request,
|
||||
(
|
||||
'run',
|
||||
intro_dir,
|
||||
),
|
||||
)
|
||||
|
||||
def clean():
|
||||
return _cleanup_tahoe_process(transport, protocol.exited)
|
||||
request.addfinalizer(clean)
|
||||
|
||||
yield protocol.magic_seen
|
||||
|
||||
furl_fname = join(intro_dir, 'private', 'introducer.furl')
|
||||
while not exists(furl_fname):
|
||||
print("Don't see {} yet".format(furl_fname))
|
||||
yield deferLater(reactor, .1, lambda: None)
|
||||
furl = _validate_furl(furl_fname)
|
||||
|
||||
returnValue(
|
||||
Introducer(
|
||||
process=TahoeProcess(transport, intro_dir),
|
||||
protocol=protocol,
|
||||
furl=furl,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class Grid(object):
|
||||
"""
|
||||
Represents an entire Tahoe Grid setup
|
||||
|
||||
A Grid includes an Introducer, Flog Gatherer and some number of
|
||||
Storage Servers. Optionally includes Clients.
|
||||
"""
|
||||
|
||||
_reactor = attr.ib()
|
||||
_request = attr.ib()
|
||||
_temp_dir = attr.ib()
|
||||
_port_allocator = attr.ib()
|
||||
introducer = attr.ib()
|
||||
flog_gatherer = attr.ib()
|
||||
storage_servers = attr.ib(factory=list)
|
||||
clients = attr.ib(factory=dict)
|
||||
|
||||
@storage_servers.validator
|
||||
def check(self, attribute, value):
|
||||
for server in value:
|
||||
if not isinstance(server, StorageServer):
|
||||
raise ValueError(
|
||||
"storage_servers must be StorageServer"
|
||||
)
|
||||
|
||||
@inlineCallbacks
|
||||
def add_storage_node(self):
|
||||
"""
|
||||
Creates a new storage node, returns a StorageServer instance
|
||||
(which will already be added to our .storage_servers list)
|
||||
"""
|
||||
port = yield self._port_allocator()
|
||||
print("make {}".format(port))
|
||||
name = 'node{}'.format(port)
|
||||
web_port = 'tcp:{}:interface=localhost'.format(port)
|
||||
server = yield create_storage_server(
|
||||
self._reactor,
|
||||
self._request,
|
||||
self._temp_dir,
|
||||
self.introducer,
|
||||
self.flog_gatherer,
|
||||
name,
|
||||
web_port,
|
||||
)
|
||||
self.storage_servers.append(server)
|
||||
returnValue(server)
|
||||
|
||||
@inlineCallbacks
|
||||
def add_client(self, name, needed=2, happy=3, total=4):
|
||||
"""
|
||||
Create a new client node
|
||||
"""
|
||||
port = yield self._port_allocator()
|
||||
web_port = 'tcp:{}:interface=localhost'.format(port)
|
||||
client = yield create_client(
|
||||
self._reactor,
|
||||
self._request,
|
||||
self._temp_dir,
|
||||
self.introducer,
|
||||
self.flog_gatherer,
|
||||
name,
|
||||
web_port,
|
||||
needed=needed,
|
||||
happy=happy,
|
||||
total=total,
|
||||
)
|
||||
self.clients[name] = client
|
||||
yield await_client_ready(client.process)
|
||||
returnValue(client)
|
||||
|
||||
|
||||
# A grid is now forever tied to its original 'request' which is where
|
||||
# it must hang finalizers off of. The "main" one is a session-level
|
||||
# fixture so it'll live the life of the tests but it could be
|
||||
# per-function Grid too.
|
||||
@inlineCallbacks
|
||||
def create_grid(reactor, request, temp_dir, flog_gatherer, port_allocator):
|
||||
"""
|
||||
Create a new grid. This will have one Introducer but zero
|
||||
storage-servers or clients; those must be added by a test or
|
||||
subsequent fixtures.
|
||||
"""
|
||||
intro_port = yield port_allocator()
|
||||
introducer = yield create_introducer(reactor, request, temp_dir, flog_gatherer, intro_port)
|
||||
grid = Grid(
|
||||
reactor,
|
||||
request,
|
||||
temp_dir,
|
||||
port_allocator,
|
||||
introducer,
|
||||
flog_gatherer,
|
||||
)
|
||||
returnValue(grid)
|
@ -1,794 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
|
||||
set -euxo pipefail
|
||||
|
||||
CODENAME=$(lsb_release --short --codename)
|
||||
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
SUDO="sudo"
|
||||
else
|
||||
SUDO=""
|
||||
fi
|
||||
|
||||
# Script to install Tor
|
||||
echo "deb http://deb.torproject.org/torproject.org ${CODENAME} main" | ${SUDO} tee -a /etc/apt/sources.list
|
||||
echo "deb-src http://deb.torproject.org/torproject.org ${CODENAME} main" | ${SUDO} tee -a /etc/apt/sources.list
|
||||
|
||||
# # Install Tor repo signing key
|
||||
${SUDO} apt-key add - <<EOF
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
+AIVYmX7rWpEH+bqlfaEZYJh5kLDebzCq8gtRteRzvRz0jCX/Zr7SZu1PskjRN+e
|
||||
MLdkNgsF9TXQy2vFFM5aRoEenHeD5gMla8pBFnbZP+AA4nPrbuKUyVcfdrLIIgQE
|
||||
KIvxjKetSj3MV1PTuH5YNBw2I3qxrgmrv+YlClmiy/VQXt3/////////////////
|
||||
////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////4kCHAQQAQIABgUCUfg8
|
||||
wQAKCRAiLOjENkQCiI1OEACItuCpRR9YS9HeORrELMBSd2IqJBeto6V0VNse//g/
|
||||
nCVKgOKJo2hpEp9BqPidjBvP20Ek/xIqHr/Pz7R6T1UVsjqtQAlLngxab81wJsRA
|
||||
QNuTpHQ0VoststglEsLtp/ziQYOvgt0yEcqKs7NmIlyA6/Uw4uzXF1D9hnfsQ1sh
|
||||
Iec3d8YpQGZf0jZFu94Hp9hpxtFkTI87yfUkqmFRRsNi9KGksl/hyN7pQMm1rmGh
|
||||
7cERHIHCiaUSu1THiAhEUc5hkMWlM2wbbFn9ZYVVGgoyDWyhDjn7qhKnERrF5dwC
|
||||
cP6mFGo9whO4U4lKUNJHA8OxtDb7mDhagY0wGVTqa+Ob2zqgqiqeLqTYdii7BnBq
|
||||
swcvkbm7BLGzpiLgyJsoxS6Rhzmb+eJiTS0Pkg22y3I/ehD2efoIO4qe/nuoBqho
|
||||
SRDkC1nl3o05NqwF+c4JB7rZo6mO6mSHut4l55avPAeurWXLdnWML9zPbdl9jJMd
|
||||
1EdVMUGfMCY5kmEkuPRw3yGYeTSM+fEB/AHj5bQZN9sjMUhatJZ3RihMoRNqJjMj
|
||||
WM0rdBHF3LGmoqq6YUPYjyfHwmNvTDpCkUM/Utz/zTmRUK6i982r3yV9vp6cdLpj
|
||||
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
|
||||
CMbEgq0I5FE5A+348Ovmobe0TUPn2WClwyRGPCe4X0WMEikEHs3Bb1CFzYfbbIe0
|
||||
N1M/DqjUvfKv0lc325P7i2DlbDuUoLmNMgHHx6+jFqsxlNCobkq+IrhKLxv27/K3
|
||||
13UOzECiPRIbMhHmLHQic9MeJp0bzJiTo1icQVRnim5ZovcpXW2piJQaWqx/TUXG
|
||||
aRdCjYrJJJZObIi6qnSB7SjdxwJUq6GuTEb/BJElQFnjsxySvTu24YkCGwQQAQIA
|
||||
BgUCUVSNVAAKCRB+fTNcWi1ewX4xD/d0R2OHFLo42KJPsIc9Wz3AMO7mfpbCmSXc
|
||||
xoM+Cyd9/GT2qgAt9hgItv3iqg9dj+AbjPNUKfpGG4Q4D/x/tb018C3F4U1PLC/P
|
||||
Q2lYX0csvuv3Gp5MuNpCuHS5bW4kLyOpRZh1JrqniL8K1Mp8cdBhMf6H+ZckQuXS
|
||||
hGHwOhGyBMu3X7biXikSvdgQmbDQMtaDbxuYZ+JGXF0uacPVnlAUwW1F55IIhmUH
|
||||
IV7t+poYo/8M0HJ/lB9y5auamrJT4acsPWS+fYHAjfGfpSE7T7QWuiIKJ2EmpVa5
|
||||
hpGhzII9ahF0wtHTKkF7d7RYV1p1UUA5nu8QFTope8fyERJDZg88ICt+TpXJ7+PJ
|
||||
9THcXgNI+papKy2wKHPfly6B+071BA4n0UX0tV7zqWk9axoN+nyUL97/k572kLTb
|
||||
xahrBEYXphdNeqqXHa/udWpTYaKwSGYmIohTSIqBZh7Xa/rhLsx2UfgR5B0WW34E
|
||||
8cTzuiZz////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////
|
||||
////////iQIcBBABAgAGBQJW5/QxAAoJEPvqMRCoU3iU3SkP+wRdT8z3EczONAcv
|
||||
Jsu7ZHgh1ggzsmozTciSuaAZRfvFmUyB9h63cKNTS86CIrqHmMZrtHRu9llkNNiE
|
||||
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
|
||||
keSPmmDrjl8cySCNsMo=
|
||||
=R0uO
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
EOF
|
||||
|
||||
${SUDO} apt-get --quiet update
|
||||
${SUDO} apt-get --quiet --yes install tor deb.torproject.org-keyring
|
@ -5,15 +5,6 @@
|
||||
# You can safely skip any of these tests, it'll just appear to "take
|
||||
# longer" to start the first test as the fixtures get built
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
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
|
||||
|
||||
|
||||
def test_create_flogger(flog_gatherer):
|
||||
print("Created flog_gatherer")
|
||||
|
@ -3,9 +3,11 @@ Integration tests for getting and putting files, including reading from stdin
|
||||
and stdout.
|
||||
"""
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import Popen, PIPE, check_output, check_call
|
||||
|
||||
import pytest
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.threads import blockingCallFromThread
|
||||
|
||||
from .util import run_in_thread, cli
|
||||
|
||||
@ -20,7 +22,7 @@ else:
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def get_put_alias(alice):
|
||||
cli(alice, "create-alias", "getput")
|
||||
cli(alice.process, "create-alias", "getput")
|
||||
|
||||
|
||||
def read_bytes(path):
|
||||
@ -36,17 +38,18 @@ def test_put_from_stdin(alice, get_put_alias, tmpdir):
|
||||
"""
|
||||
tempfile = str(tmpdir.join("file"))
|
||||
p = Popen(
|
||||
["tahoe", "--node-directory", alice.node_dir, "put", "-", "getput:fromstdin"],
|
||||
["tahoe", "--node-directory", alice.process.node_dir, "put", "-", "getput:fromstdin"],
|
||||
stdin=PIPE
|
||||
)
|
||||
p.stdin.write(DATA)
|
||||
p.stdin.close()
|
||||
assert p.wait() == 0
|
||||
|
||||
cli(alice, "get", "getput:fromstdin", tempfile)
|
||||
cli(alice.process, "get", "getput:fromstdin", tempfile)
|
||||
assert read_bytes(tempfile) == DATA
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_get_to_stdout(alice, get_put_alias, tmpdir):
|
||||
"""
|
||||
It's possible to upload a file, and then download it to stdout.
|
||||
@ -54,11 +57,75 @@ def test_get_to_stdout(alice, get_put_alias, tmpdir):
|
||||
tempfile = tmpdir.join("file")
|
||||
with tempfile.open("wb") as f:
|
||||
f.write(DATA)
|
||||
cli(alice, "put", str(tempfile), "getput:tostdout")
|
||||
cli(alice.process, "put", str(tempfile), "getput:tostdout")
|
||||
|
||||
p = Popen(
|
||||
["tahoe", "--node-directory", alice.node_dir, "get", "getput:tostdout", "-"],
|
||||
["tahoe", "--node-directory", alice.process.node_dir, "get", "getput:tostdout", "-"],
|
||||
stdout=PIPE
|
||||
)
|
||||
assert p.stdout.read() == DATA
|
||||
assert p.wait() == 0
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_large_file(alice, get_put_alias, tmp_path):
|
||||
"""
|
||||
It's possible to upload and download a larger file.
|
||||
|
||||
We avoid stdin/stdout since that's flaky on Windows.
|
||||
"""
|
||||
tempfile = tmp_path / "file"
|
||||
with tempfile.open("wb") as f:
|
||||
f.write(DATA * 1_000_000)
|
||||
cli(alice.process, "put", str(tempfile), "getput:largefile")
|
||||
|
||||
outfile = tmp_path / "out"
|
||||
check_call(
|
||||
["tahoe", "--node-directory", alice.process.node_dir, "get", "getput:largefile", str(outfile)],
|
||||
)
|
||||
assert outfile.read_bytes() == tempfile.read_bytes()
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_upload_download_immutable_different_default_max_segment_size(alice, get_put_alias, tmpdir, request):
|
||||
"""
|
||||
Tahoe-LAFS used to have a default max segment size of 128KB, and is now
|
||||
1MB. Test that an upload created when 128KB was the default can be
|
||||
downloaded with 1MB as the default (i.e. old uploader, new downloader), and
|
||||
vice versa, (new uploader, old downloader).
|
||||
"""
|
||||
tempfile = tmpdir.join("file")
|
||||
large_data = DATA * 100_000
|
||||
assert len(large_data) > 2 * 1024 * 1024
|
||||
with tempfile.open("wb") as f:
|
||||
f.write(large_data)
|
||||
|
||||
def set_segment_size(segment_size):
|
||||
return blockingCallFromThread(
|
||||
reactor,
|
||||
lambda: alice.reconfigure_zfec(
|
||||
reactor,
|
||||
(1, 1, 1),
|
||||
None,
|
||||
max_segment_size=segment_size
|
||||
)
|
||||
)
|
||||
|
||||
# 1. Upload file 1 with default segment size set to 1MB
|
||||
set_segment_size(1024 * 1024)
|
||||
cli(alice.process, "put", str(tempfile), "getput:seg1024kb")
|
||||
|
||||
# 2. Download file 1 with default segment size set to 128KB
|
||||
set_segment_size(128 * 1024)
|
||||
assert large_data == check_output(
|
||||
["tahoe", "--node-directory", alice.process.node_dir, "get", "getput:seg1024kb", "-"]
|
||||
)
|
||||
|
||||
# 3. Upload file 2 with default segment size set to 128KB
|
||||
cli(alice.process, "put", str(tempfile), "getput:seg128kb")
|
||||
|
||||
# 4. Download file 2 with default segment size set to 1MB
|
||||
set_segment_size(1024 * 1024)
|
||||
assert large_data == check_output(
|
||||
["tahoe", "--node-directory", alice.process.node_dir, "get", "getput:seg128kb", "-"]
|
||||
)
|
||||
|
351
integration/test_grid_manager.py
Normal file
351
integration/test_grid_manager.py
Normal file
@ -0,0 +1,351 @@
|
||||
import sys
|
||||
import json
|
||||
from os.path import join
|
||||
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
Encoding,
|
||||
PublicFormat,
|
||||
)
|
||||
|
||||
from twisted.internet.utils import (
|
||||
getProcessOutputAndValue,
|
||||
)
|
||||
from twisted.internet.defer import (
|
||||
inlineCallbacks,
|
||||
returnValue,
|
||||
)
|
||||
|
||||
from allmydata.crypto import ed25519
|
||||
from allmydata.util import base32
|
||||
from allmydata.util import configutil
|
||||
|
||||
from . import util
|
||||
from .grid import (
|
||||
create_grid,
|
||||
)
|
||||
|
||||
import pytest_twisted
|
||||
|
||||
|
||||
@inlineCallbacks
|
||||
def _run_gm(reactor, request, *args, **kwargs):
|
||||
"""
|
||||
Run the grid-manager process, passing all arguments as extra CLI
|
||||
args.
|
||||
|
||||
:returns: all process output
|
||||
"""
|
||||
if request.config.getoption('coverage'):
|
||||
base_args = ("-b", "-m", "coverage", "run", "-m", "allmydata.cli.grid_manager")
|
||||
else:
|
||||
base_args = ("-m", "allmydata.cli.grid_manager")
|
||||
|
||||
output, errput, exit_code = yield getProcessOutputAndValue(
|
||||
sys.executable,
|
||||
base_args + args,
|
||||
reactor=reactor,
|
||||
**kwargs
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise util.ProcessFailed(
|
||||
RuntimeError("Exit code {}".format(exit_code)),
|
||||
output + errput,
|
||||
)
|
||||
returnValue(output)
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_create_certificate(reactor, request):
|
||||
"""
|
||||
The Grid Manager produces a valid, correctly-signed certificate.
|
||||
"""
|
||||
gm_config = yield _run_gm(reactor, request, "--config", "-", "create")
|
||||
privkey_bytes = json.loads(gm_config)['private_key'].encode('ascii')
|
||||
privkey, pubkey = ed25519.signing_keypair_from_string(privkey_bytes)
|
||||
|
||||
# Note that zara + her key here are arbitrary and don't match any
|
||||
# "actual" clients in the test-grid; we're just checking that the
|
||||
# Grid Manager signs this properly.
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "add",
|
||||
"zara", "pub-v0-kzug3ut2m7ziihf3ndpqlquuxeie4foyl36wn54myqc4wmiwe4ga",
|
||||
stdinBytes=gm_config,
|
||||
)
|
||||
zara_cert_bytes = yield _run_gm(
|
||||
reactor, request, "--config", "-", "sign", "zara", "1",
|
||||
stdinBytes=gm_config,
|
||||
)
|
||||
zara_cert = json.loads(zara_cert_bytes)
|
||||
|
||||
# confirm that zara's certificate is made by the Grid Manager
|
||||
# (.verify returns None on success, raises exception on error)
|
||||
pubkey.verify(
|
||||
base32.a2b(zara_cert['signature'].encode('ascii')),
|
||||
zara_cert['certificate'].encode('ascii'),
|
||||
)
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_remove_client(reactor, request):
|
||||
"""
|
||||
A Grid Manager can add and successfully remove a client
|
||||
"""
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "create",
|
||||
)
|
||||
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "add",
|
||||
"zara", "pub-v0-kzug3ut2m7ziihf3ndpqlquuxeie4foyl36wn54myqc4wmiwe4ga",
|
||||
stdinBytes=gm_config,
|
||||
)
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "add",
|
||||
"yakov", "pub-v0-kvxhb3nexybmipkrar2ztfrwp4uxxsmrjzkpzafit3ket4u5yldq",
|
||||
stdinBytes=gm_config,
|
||||
)
|
||||
assert "zara" in json.loads(gm_config)['storage_servers']
|
||||
assert "yakov" in json.loads(gm_config)['storage_servers']
|
||||
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "remove",
|
||||
"zara",
|
||||
stdinBytes=gm_config,
|
||||
)
|
||||
assert "zara" not in json.loads(gm_config)['storage_servers']
|
||||
assert "yakov" in json.loads(gm_config)['storage_servers']
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_remove_last_client(reactor, request):
|
||||
"""
|
||||
A Grid Manager can remove all clients
|
||||
"""
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "create",
|
||||
)
|
||||
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "add",
|
||||
"zara", "pub-v0-kzug3ut2m7ziihf3ndpqlquuxeie4foyl36wn54myqc4wmiwe4ga",
|
||||
stdinBytes=gm_config,
|
||||
)
|
||||
assert "zara" in json.loads(gm_config)['storage_servers']
|
||||
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "remove",
|
||||
"zara",
|
||||
stdinBytes=gm_config,
|
||||
)
|
||||
# there are no storage servers left at all now
|
||||
assert "storage_servers" not in json.loads(gm_config)
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_add_remove_client_file(reactor, request, temp_dir):
|
||||
"""
|
||||
A Grid Manager can add and successfully remove a client (when
|
||||
keeping data on disk)
|
||||
"""
|
||||
gmconfig = join(temp_dir, "gmtest")
|
||||
gmconfig_file = join(temp_dir, "gmtest", "config.json")
|
||||
yield _run_gm(
|
||||
reactor, request, "--config", gmconfig, "create",
|
||||
)
|
||||
|
||||
yield _run_gm(
|
||||
reactor, request, "--config", gmconfig, "add",
|
||||
"zara", "pub-v0-kzug3ut2m7ziihf3ndpqlquuxeie4foyl36wn54myqc4wmiwe4ga",
|
||||
)
|
||||
yield _run_gm(
|
||||
reactor, request, "--config", gmconfig, "add",
|
||||
"yakov", "pub-v0-kvxhb3nexybmipkrar2ztfrwp4uxxsmrjzkpzafit3ket4u5yldq",
|
||||
)
|
||||
assert "zara" in json.load(open(gmconfig_file, "r"))['storage_servers']
|
||||
assert "yakov" in json.load(open(gmconfig_file, "r"))['storage_servers']
|
||||
|
||||
yield _run_gm(
|
||||
reactor, request, "--config", gmconfig, "remove",
|
||||
"zara",
|
||||
)
|
||||
assert "zara" not in json.load(open(gmconfig_file, "r"))['storage_servers']
|
||||
assert "yakov" in json.load(open(gmconfig_file, "r"))['storage_servers']
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def _test_reject_storage_server(reactor, request, temp_dir, flog_gatherer, port_allocator):
|
||||
"""
|
||||
A client with happines=2 fails to upload to a Grid when it is
|
||||
using Grid Manager and there is only 1 storage server with a valid
|
||||
certificate.
|
||||
"""
|
||||
grid = yield create_grid(reactor, request, temp_dir, flog_gatherer, port_allocator)
|
||||
storage0 = yield grid.add_storage_node()
|
||||
_ = yield grid.add_storage_node()
|
||||
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "create",
|
||||
)
|
||||
gm_privkey_bytes = json.loads(gm_config)['private_key'].encode('ascii')
|
||||
gm_privkey, gm_pubkey = ed25519.signing_keypair_from_string(gm_privkey_bytes)
|
||||
|
||||
# create certificate for the first storage-server
|
||||
pubkey_fname = join(storage0.process.node_dir, "node.pubkey")
|
||||
with open(pubkey_fname, 'r') as f:
|
||||
pubkey_str = f.read().strip()
|
||||
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "add",
|
||||
"storage0", pubkey_str,
|
||||
stdinBytes=gm_config,
|
||||
)
|
||||
assert json.loads(gm_config)['storage_servers'].keys() == {'storage0'}
|
||||
|
||||
print("inserting certificate")
|
||||
cert = yield _run_gm(
|
||||
reactor, request, "--config", "-", "sign", "storage0", "1",
|
||||
stdinBytes=gm_config,
|
||||
)
|
||||
print(cert)
|
||||
|
||||
yield util.run_tahoe(
|
||||
reactor, request, "--node-directory", storage0.process.node_dir,
|
||||
"admin", "add-grid-manager-cert",
|
||||
"--name", "default",
|
||||
"--filename", "-",
|
||||
stdin=cert,
|
||||
)
|
||||
|
||||
# re-start this storage server
|
||||
yield storage0.restart(reactor, request)
|
||||
|
||||
# now only one storage-server has the certificate .. configure
|
||||
# diana to have the grid-manager certificate
|
||||
|
||||
diana = yield grid.add_client("diana", needed=2, happy=2, total=2)
|
||||
|
||||
config = configutil.get_config(join(diana.process.node_dir, "tahoe.cfg"))
|
||||
config.add_section("grid_managers")
|
||||
config.set("grid_managers", "test", str(ed25519.string_from_verifying_key(gm_pubkey), "ascii"))
|
||||
with open(join(diana.process.node_dir, "tahoe.cfg"), "w") as f:
|
||||
config.write(f)
|
||||
|
||||
yield diana.restart(reactor, request, servers=2)
|
||||
|
||||
# try to put something into the grid, which should fail (because
|
||||
# diana has happy=2 but should only find storage0 to be acceptable
|
||||
# to upload to)
|
||||
|
||||
try:
|
||||
yield util.run_tahoe(
|
||||
reactor, request, "--node-directory", diana.process.node_dir,
|
||||
"put", "-",
|
||||
stdin=b"some content\n" * 200,
|
||||
)
|
||||
assert False, "Should get a failure"
|
||||
except util.ProcessFailed as e:
|
||||
if b'UploadUnhappinessError' in e.output:
|
||||
# We're done! We've succeeded.
|
||||
return
|
||||
|
||||
assert False, "Failed to see one of out of two servers"
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def _test_accept_storage_server(reactor, request, temp_dir, flog_gatherer, port_allocator):
|
||||
"""
|
||||
Successfully upload to a Grid Manager enabled Grid.
|
||||
"""
|
||||
grid = yield create_grid(reactor, request, temp_dir, flog_gatherer, port_allocator)
|
||||
happy0 = yield grid.add_storage_node()
|
||||
happy1 = yield grid.add_storage_node()
|
||||
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "create",
|
||||
)
|
||||
gm_privkey_bytes = json.loads(gm_config)['private_key'].encode('ascii')
|
||||
gm_privkey, gm_pubkey = ed25519.signing_keypair_from_string(gm_privkey_bytes)
|
||||
|
||||
# create certificates for all storage-servers
|
||||
servers = (
|
||||
("happy0", happy0),
|
||||
("happy1", happy1),
|
||||
)
|
||||
for st_name, st in servers:
|
||||
pubkey_fname = join(st.process.node_dir, "node.pubkey")
|
||||
with open(pubkey_fname, 'r') as f:
|
||||
pubkey_str = f.read().strip()
|
||||
|
||||
gm_config = yield _run_gm(
|
||||
reactor, request, "--config", "-", "add",
|
||||
st_name, pubkey_str,
|
||||
stdinBytes=gm_config,
|
||||
)
|
||||
assert json.loads(gm_config)['storage_servers'].keys() == {'happy0', 'happy1'}
|
||||
|
||||
# add the certificates from the grid-manager to the storage servers
|
||||
print("inserting storage-server certificates")
|
||||
for st_name, st in servers:
|
||||
cert = yield _run_gm(
|
||||
reactor, request, "--config", "-", "sign", st_name, "1",
|
||||
stdinBytes=gm_config,
|
||||
)
|
||||
|
||||
yield util.run_tahoe(
|
||||
reactor, request, "--node-directory", st.process.node_dir,
|
||||
"admin", "add-grid-manager-cert",
|
||||
"--name", "default",
|
||||
"--filename", "-",
|
||||
stdin=cert,
|
||||
)
|
||||
|
||||
# re-start the storage servers
|
||||
yield happy0.restart(reactor, request)
|
||||
yield happy1.restart(reactor, request)
|
||||
|
||||
# configure freya (a client) to have the grid-manager certificate
|
||||
freya = yield grid.add_client("freya", needed=2, happy=2, total=2)
|
||||
|
||||
config = configutil.get_config(join(freya.process.node_dir, "tahoe.cfg"))
|
||||
config.add_section("grid_managers")
|
||||
config.set("grid_managers", "test", str(ed25519.string_from_verifying_key(gm_pubkey), "ascii"))
|
||||
with open(join(freya.process.node_dir, "tahoe.cfg"), "w") as f:
|
||||
config.write(f)
|
||||
|
||||
yield freya.restart(reactor, request, servers=2)
|
||||
|
||||
# confirm that Freya will upload to the GridManager-enabled Grid
|
||||
yield util.run_tahoe(
|
||||
reactor, request, "--node-directory", freya.process.node_dir,
|
||||
"put", "-",
|
||||
stdin=b"some content\n" * 200,
|
||||
)
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_identity(reactor, request, temp_dir):
|
||||
"""
|
||||
Dump public key to CLI
|
||||
"""
|
||||
gm_config = join(temp_dir, "test_identity")
|
||||
yield _run_gm(
|
||||
reactor, request, "--config", gm_config, "create",
|
||||
)
|
||||
|
||||
# ask the CLI for the grid-manager pubkey
|
||||
pubkey = yield _run_gm(
|
||||
reactor, request, "--config", gm_config, "public-identity",
|
||||
)
|
||||
alleged_pubkey = ed25519.verifying_key_from_string(pubkey.strip())
|
||||
|
||||
# load the grid-manager pubkey "ourselves"
|
||||
with open(join(gm_config, "config.json"), "r") as f:
|
||||
real_config = json.load(f)
|
||||
real_privkey, real_pubkey = ed25519.signing_keypair_from_string(
|
||||
real_config["private_key"].encode("ascii"),
|
||||
)
|
||||
|
||||
# confirm the CLI told us the correct thing
|
||||
alleged_bytes = alleged_pubkey.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
real_bytes = real_pubkey.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
assert alleged_bytes == real_bytes, "Keys don't match"
|
@ -2,26 +2,11 @@
|
||||
Integration tests for I2P support.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
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
|
||||
|
||||
import sys
|
||||
from os.path import join, exists
|
||||
from os import mkdir
|
||||
from os import mkdir, environ
|
||||
from time import sleep
|
||||
|
||||
if PY2:
|
||||
def which(path):
|
||||
# This will result in skipping I2P tests on Python 2. Oh well.
|
||||
return None
|
||||
else:
|
||||
from shutil import which
|
||||
from shutil import which
|
||||
|
||||
from eliot import log_call
|
||||
|
||||
@ -38,6 +23,9 @@ from twisted.internet.error import ProcessExitedAlready
|
||||
from allmydata.test.common import (
|
||||
write_introducer,
|
||||
)
|
||||
from allmydata.node import read_config
|
||||
from allmydata.util.iputil import allocate_tcp_port
|
||||
|
||||
|
||||
if which("docker") is None:
|
||||
pytest.skip('Skipping I2P tests since Docker is unavailable', allow_module_level=True)
|
||||
@ -50,20 +38,24 @@ if sys.platform.startswith('win'):
|
||||
@pytest.fixture
|
||||
def i2p_network(reactor, temp_dir, request):
|
||||
"""Fixture to start up local i2pd."""
|
||||
proto = util._MagicTextProtocol("ephemeral keys")
|
||||
proto = util._MagicTextProtocol("ephemeral keys", "i2pd")
|
||||
reactor.spawnProcess(
|
||||
proto,
|
||||
which("docker"),
|
||||
(
|
||||
"docker", "run", "-p", "7656:7656", "purplei2p/i2pd",
|
||||
"docker", "run", "-p", "7656:7656", "purplei2p/i2pd:release-2.45.1",
|
||||
# Bad URL for reseeds, so it can't talk to other routers.
|
||||
"--reseed.urls", "http://localhost:1/",
|
||||
# Make sure we see the "ephemeral keys message"
|
||||
"--log=stdout",
|
||||
"--loglevel=info"
|
||||
),
|
||||
env=environ,
|
||||
)
|
||||
|
||||
def cleanup():
|
||||
try:
|
||||
proto.transport.signalProcess("KILL")
|
||||
proto.transport.signalProcess("INT")
|
||||
util.block_with_timeout(proto.exited, reactor)
|
||||
except ProcessExitedAlready:
|
||||
pass
|
||||
@ -79,13 +71,6 @@ def i2p_network(reactor, temp_dir, request):
|
||||
include_result=False,
|
||||
)
|
||||
def i2p_introducer(reactor, temp_dir, flog_gatherer, request):
|
||||
config = '''
|
||||
[node]
|
||||
nickname = introducer_i2p
|
||||
web.port = 4561
|
||||
log_gatherer.furl = {log_furl}
|
||||
'''.format(log_furl=flog_gatherer)
|
||||
|
||||
intro_dir = join(temp_dir, 'introducer_i2p')
|
||||
print("making introducer", intro_dir)
|
||||
|
||||
@ -105,12 +90,14 @@ log_gatherer.furl = {log_furl}
|
||||
pytest_twisted.blockon(done_proto.done)
|
||||
|
||||
# over-write the config file with our stuff
|
||||
with open(join(intro_dir, 'tahoe.cfg'), 'w') as f:
|
||||
f.write(config)
|
||||
config = read_config(intro_dir, "tub.port")
|
||||
config.set_config("node", "nickname", "introducer_i2p")
|
||||
config.set_config("node", "web.port", "4563")
|
||||
config.set_config("node", "log_gatherer.furl", flog_gatherer)
|
||||
|
||||
# "tahoe run" is consistent across Linux/macOS/Windows, unlike the old
|
||||
# "start" command.
|
||||
protocol = util._MagicTextProtocol('introducer running')
|
||||
protocol = util._MagicTextProtocol('introducer running', "introducer")
|
||||
transport = util._tahoe_runner_optional_coverage(
|
||||
protocol,
|
||||
reactor,
|
||||
@ -144,9 +131,12 @@ def i2p_introducer_furl(i2p_introducer, temp_dir):
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
@pytest.mark.skip("I2P tests are not functioning at all, for unknown reasons")
|
||||
def test_i2p_service_storage(reactor, request, temp_dir, flog_gatherer, i2p_network, i2p_introducer_furl):
|
||||
yield _create_anonymous_node(reactor, 'carol_i2p', 8008, request, temp_dir, flog_gatherer, i2p_network, i2p_introducer_furl)
|
||||
yield _create_anonymous_node(reactor, 'dave_i2p', 8009, request, temp_dir, flog_gatherer, i2p_network, i2p_introducer_furl)
|
||||
web_port0 = allocate_tcp_port()
|
||||
web_port1 = allocate_tcp_port()
|
||||
yield _create_anonymous_node(reactor, 'carol_i2p', web_port0, request, temp_dir, flog_gatherer, i2p_network, i2p_introducer_furl)
|
||||
yield _create_anonymous_node(reactor, 'dave_i2p', web_port1, request, temp_dir, flog_gatherer, i2p_network, i2p_introducer_furl)
|
||||
# ensure both nodes are connected to "a grid" by uploading
|
||||
# something via carol, and retrieve it using dave.
|
||||
gold_path = join(temp_dir, "gold")
|
||||
@ -167,7 +157,8 @@ def test_i2p_service_storage(reactor, request, temp_dir, flog_gatherer, i2p_netw
|
||||
sys.executable, '-b', '-m', 'allmydata.scripts.runner',
|
||||
'-d', join(temp_dir, 'carol_i2p'),
|
||||
'put', gold_path,
|
||||
)
|
||||
),
|
||||
env=environ,
|
||||
)
|
||||
yield proto.done
|
||||
cap = proto.output.getvalue().strip().split()[-1]
|
||||
@ -181,7 +172,8 @@ def test_i2p_service_storage(reactor, request, temp_dir, flog_gatherer, i2p_netw
|
||||
sys.executable, '-b', '-m', 'allmydata.scripts.runner',
|
||||
'-d', join(temp_dir, 'dave_i2p'),
|
||||
'get', cap,
|
||||
)
|
||||
),
|
||||
env=environ,
|
||||
)
|
||||
yield proto.done
|
||||
|
||||
@ -190,9 +182,8 @@ def test_i2p_service_storage(reactor, request, temp_dir, flog_gatherer, i2p_netw
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def _create_anonymous_node(reactor, name, control_port, request, temp_dir, flog_gatherer, i2p_network, introducer_furl):
|
||||
def _create_anonymous_node(reactor, name, web_port, request, temp_dir, flog_gatherer, i2p_network, introducer_furl):
|
||||
node_dir = FilePath(temp_dir).child(name)
|
||||
web_port = "tcp:{}:interface=localhost".format(control_port + 2000)
|
||||
|
||||
print("creating", node_dir.path)
|
||||
node_dir.makedirs()
|
||||
@ -208,7 +199,8 @@ def _create_anonymous_node(reactor, name, control_port, request, temp_dir, flog_
|
||||
'--hide-ip',
|
||||
'--listen', 'i2p',
|
||||
node_dir.path,
|
||||
)
|
||||
),
|
||||
env=environ,
|
||||
)
|
||||
yield proto.done
|
||||
|
||||
|
@ -1,19 +1,10 @@
|
||||
"""
|
||||
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
|
||||
|
||||
import sys
|
||||
from os.path import join
|
||||
|
||||
from twisted.internet.error import ProcessTerminated
|
||||
from os import environ
|
||||
|
||||
from . import util
|
||||
|
||||
@ -31,7 +22,7 @@ def test_upload_immutable(reactor, temp_dir, introducer_furl, flog_gatherer, sto
|
||||
happy=7,
|
||||
total=10,
|
||||
)
|
||||
util.await_client_ready(edna)
|
||||
yield util.await_client_ready(edna)
|
||||
|
||||
node_dir = join(temp_dir, 'edna')
|
||||
|
||||
@ -45,13 +36,14 @@ def test_upload_immutable(reactor, temp_dir, introducer_furl, flog_gatherer, sto
|
||||
sys.executable, '-b', '-m', 'allmydata.scripts.runner',
|
||||
'-d', node_dir,
|
||||
'put', __file__,
|
||||
]
|
||||
],
|
||||
env=environ,
|
||||
)
|
||||
try:
|
||||
yield proto.done
|
||||
assert False, "should raise exception"
|
||||
except Exception as e:
|
||||
assert isinstance(e, ProcessTerminated)
|
||||
except util.ProcessFailed as e:
|
||||
assert b"UploadUnhappinessError" in e.output
|
||||
|
||||
output = proto.output.getvalue()
|
||||
assert b"shares could be placed on only" in output
|
||||
|
@ -10,15 +10,7 @@ These tests use Paramiko, rather than Twisted's Conch, because:
|
||||
2. Its API is much simpler to use.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
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
|
||||
|
||||
import os.path
|
||||
from posixpath import join
|
||||
from stat import S_ISDIR
|
||||
|
||||
@ -33,7 +25,7 @@ import pytest
|
||||
from .util import generate_ssh_key, run_in_thread
|
||||
|
||||
|
||||
def connect_sftp(connect_args={"username": "alice", "password": "password"}):
|
||||
def connect_sftp(connect_args):
|
||||
"""Create an SFTP client."""
|
||||
client = SSHClient()
|
||||
client.set_missing_host_key_policy(AutoAddPolicy)
|
||||
@ -60,24 +52,24 @@ def connect_sftp(connect_args={"username": "alice", "password": "password"}):
|
||||
@run_in_thread
|
||||
def test_bad_account_password_ssh_key(alice, tmpdir):
|
||||
"""
|
||||
Can't login with unknown username, wrong password, or wrong SSH pub key.
|
||||
Can't login with unknown username, any password, or wrong SSH pub key.
|
||||
"""
|
||||
# Wrong password, wrong username:
|
||||
for u, p in [("alice", "wrong"), ("someuser", "password")]:
|
||||
# Any password, wrong username:
|
||||
for u, p in [("alice-key", "wrong"), ("someuser", "password")]:
|
||||
with pytest.raises(AuthenticationException):
|
||||
connect_sftp(connect_args={
|
||||
"username": u, "password": p,
|
||||
})
|
||||
|
||||
another_key = join(str(tmpdir), "ssh_key")
|
||||
another_key = os.path.join(str(tmpdir), "ssh_key")
|
||||
generate_ssh_key(another_key)
|
||||
good_key = RSAKey(filename=join(alice.node_dir, "private", "ssh_client_rsa_key"))
|
||||
good_key = RSAKey(filename=os.path.join(alice.process.node_dir, "private", "ssh_client_rsa_key"))
|
||||
bad_key = RSAKey(filename=another_key)
|
||||
|
||||
# Wrong key:
|
||||
with pytest.raises(AuthenticationException):
|
||||
connect_sftp(connect_args={
|
||||
"username": "alice2", "pkey": bad_key,
|
||||
"username": "alice-key", "pkey": bad_key,
|
||||
})
|
||||
|
||||
# Wrong username:
|
||||
@ -87,12 +79,22 @@ def test_bad_account_password_ssh_key(alice, tmpdir):
|
||||
})
|
||||
|
||||
|
||||
def sftp_client_key(client):
|
||||
"""
|
||||
:return RSAKey: the RSA client key associated with this grid.Client
|
||||
"""
|
||||
# XXX move to Client / grid.py?
|
||||
return RSAKey(
|
||||
filename=os.path.join(client.process.node_dir, "private", "ssh_client_rsa_key"),
|
||||
)
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_ssh_key_auth(alice):
|
||||
"""It's possible to login authenticating with SSH public key."""
|
||||
key = RSAKey(filename=join(alice.node_dir, "private", "ssh_client_rsa_key"))
|
||||
key = sftp_client_key(alice)
|
||||
sftp = connect_sftp(connect_args={
|
||||
"username": "alice2", "pkey": key
|
||||
"username": "alice-key", "pkey": key
|
||||
})
|
||||
assert sftp.listdir() == []
|
||||
|
||||
@ -100,7 +102,10 @@ def test_ssh_key_auth(alice):
|
||||
@run_in_thread
|
||||
def test_read_write_files(alice):
|
||||
"""It's possible to upload and download files."""
|
||||
sftp = connect_sftp()
|
||||
sftp = connect_sftp(connect_args={
|
||||
"username": "alice-key",
|
||||
"pkey": sftp_client_key(alice),
|
||||
})
|
||||
with sftp.file("myfile", "wb") as f:
|
||||
f.write(b"abc")
|
||||
f.write(b"def")
|
||||
@ -117,7 +122,10 @@ def test_directories(alice):
|
||||
It's possible to create, list directories, and create and remove files in
|
||||
them.
|
||||
"""
|
||||
sftp = connect_sftp()
|
||||
sftp = connect_sftp(connect_args={
|
||||
"username": "alice-key",
|
||||
"pkey": sftp_client_key(alice),
|
||||
})
|
||||
assert sftp.listdir() == []
|
||||
|
||||
sftp.mkdir("childdir")
|
||||
@ -148,7 +156,10 @@ def test_directories(alice):
|
||||
@run_in_thread
|
||||
def test_rename(alice):
|
||||
"""Directories and files can be renamed."""
|
||||
sftp = connect_sftp()
|
||||
sftp = connect_sftp(connect_args={
|
||||
"username": "alice-key",
|
||||
"pkey": sftp_client_key(alice),
|
||||
})
|
||||
sftp.mkdir("dir")
|
||||
|
||||
filepath = join("dir", "file")
|
||||
|
@ -1,16 +1,6 @@
|
||||
"""
|
||||
Ported to Python 3.
|
||||
"""
|
||||
from __future__ import (
|
||||
print_function,
|
||||
unicode_literals,
|
||||
absolute_import,
|
||||
division,
|
||||
)
|
||||
|
||||
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 six import ensure_text
|
||||
|
||||
|
@ -1,17 +1,10 @@
|
||||
"""
|
||||
Ported to Python 3.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
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
|
||||
|
||||
import sys
|
||||
from os.path import join
|
||||
from os import environ
|
||||
|
||||
import pytest
|
||||
import pytest_twisted
|
||||
@ -25,6 +18,8 @@ from twisted.python.filepath import (
|
||||
from allmydata.test.common import (
|
||||
write_introducer,
|
||||
)
|
||||
from allmydata.client import read_config
|
||||
from allmydata.util.deferredutil import async_to_deferred
|
||||
|
||||
# see "conftest.py" for the fixtures (e.g. "tor_network")
|
||||
|
||||
@ -35,12 +30,29 @@ from allmydata.test.common import (
|
||||
if sys.platform.startswith('win'):
|
||||
pytest.skip('Skipping Tor tests on Windows', allow_module_level=True)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info[:2] > (3, 11), reason='Chutney still does not support 3.12')
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_onion_service_storage(reactor, request, temp_dir, flog_gatherer, tor_network, tor_introducer_furl):
|
||||
yield _create_anonymous_node(reactor, 'carol', 8008, request, temp_dir, flog_gatherer, tor_network, tor_introducer_furl)
|
||||
yield _create_anonymous_node(reactor, 'dave', 8009, request, temp_dir, flog_gatherer, tor_network, tor_introducer_furl)
|
||||
# ensure both nodes are connected to "a grid" by uploading
|
||||
# something via carol, and retrieve it using dave.
|
||||
"""
|
||||
Two nodes and an introducer all configured to use Tahoe.
|
||||
|
||||
The two nodes can talk to the introducer and each other: we upload to one
|
||||
node, read from the other.
|
||||
"""
|
||||
carol = yield _create_anonymous_node(reactor, 'carol', 8100, request, temp_dir, flog_gatherer, tor_network, tor_introducer_furl, 2)
|
||||
dave = yield _create_anonymous_node(reactor, 'dave', 8101, request, temp_dir, flog_gatherer, tor_network, tor_introducer_furl, 2)
|
||||
yield util.await_client_ready(carol, minimum_number_of_servers=2, timeout=600)
|
||||
yield util.await_client_ready(dave, minimum_number_of_servers=2, timeout=600)
|
||||
yield upload_to_one_download_from_the_other(reactor, temp_dir, carol, dave)
|
||||
|
||||
|
||||
@async_to_deferred
|
||||
async def upload_to_one_download_from_the_other(reactor, temp_dir, upload_to: util.TahoeProcess, download_from: util.TahoeProcess):
|
||||
"""
|
||||
Ensure both nodes are connected to "a grid" by uploading something via one
|
||||
node, and retrieve it using the other.
|
||||
"""
|
||||
|
||||
gold_path = join(temp_dir, "gold")
|
||||
with open(gold_path, "w") as f:
|
||||
f.write(
|
||||
@ -57,13 +69,14 @@ def test_onion_service_storage(reactor, request, temp_dir, flog_gatherer, tor_ne
|
||||
sys.executable,
|
||||
(
|
||||
sys.executable, '-b', '-m', 'allmydata.scripts.runner',
|
||||
'-d', join(temp_dir, 'carol'),
|
||||
'-d', upload_to.node_dir,
|
||||
'put', gold_path,
|
||||
)
|
||||
),
|
||||
env=environ,
|
||||
)
|
||||
yield proto.done
|
||||
await proto.done
|
||||
cap = proto.output.getvalue().strip().split()[-1]
|
||||
print("TEH CAP!", cap)
|
||||
print("capability: {}".format(cap))
|
||||
|
||||
proto = util._CollectOutputProtocol(capture_stderr=False)
|
||||
reactor.spawnProcess(
|
||||
@ -71,74 +84,83 @@ def test_onion_service_storage(reactor, request, temp_dir, flog_gatherer, tor_ne
|
||||
sys.executable,
|
||||
(
|
||||
sys.executable, '-b', '-m', 'allmydata.scripts.runner',
|
||||
'-d', join(temp_dir, 'dave'),
|
||||
'-d', download_from.node_dir,
|
||||
'get', cap,
|
||||
)
|
||||
),
|
||||
env=environ,
|
||||
)
|
||||
yield proto.done
|
||||
|
||||
dave_got = proto.output.getvalue().strip()
|
||||
assert dave_got == open(gold_path, 'rb').read().strip()
|
||||
await proto.done
|
||||
download_got = proto.output.getvalue().strip()
|
||||
assert download_got == open(gold_path, 'rb').read().strip()
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def _create_anonymous_node(reactor, name, control_port, request, temp_dir, flog_gatherer, tor_network, introducer_furl):
|
||||
def _create_anonymous_node(reactor, name, web_port, request, temp_dir, flog_gatherer, tor_network, introducer_furl, shares_total: int) -> util.TahoeProcess:
|
||||
node_dir = FilePath(temp_dir).child(name)
|
||||
web_port = "tcp:{}:interface=localhost".format(control_port + 2000)
|
||||
|
||||
if True:
|
||||
print("creating", node_dir.path)
|
||||
node_dir.makedirs()
|
||||
proto = util._DumpOutputProtocol(None)
|
||||
reactor.spawnProcess(
|
||||
proto,
|
||||
sys.executable,
|
||||
(
|
||||
sys.executable, '-b', '-m', 'allmydata.scripts.runner',
|
||||
'create-node',
|
||||
'--nickname', name,
|
||||
'--introducer', introducer_furl,
|
||||
'--hide-ip',
|
||||
'--tor-control-port', 'tcp:localhost:{}'.format(control_port),
|
||||
'--listen', 'tor',
|
||||
node_dir.path,
|
||||
)
|
||||
if node_dir.exists():
|
||||
raise RuntimeError(
|
||||
"A node already exists in '{}'".format(node_dir)
|
||||
)
|
||||
yield proto.done
|
||||
print(f"creating {node_dir.path} with introducer {introducer_furl}")
|
||||
node_dir.makedirs()
|
||||
proto = util._DumpOutputProtocol(None)
|
||||
reactor.spawnProcess(
|
||||
proto,
|
||||
sys.executable,
|
||||
(
|
||||
sys.executable, '-b', '-m', 'allmydata.scripts.runner',
|
||||
'create-node',
|
||||
'--nickname', name,
|
||||
'--webport', str(web_port),
|
||||
'--introducer', introducer_furl,
|
||||
'--hide-ip',
|
||||
'--tor-control-port', tor_network.client_control_endpoint,
|
||||
'--listen', 'tor',
|
||||
'--shares-needed', '1',
|
||||
'--shares-happy', '1',
|
||||
'--shares-total', str(shares_total),
|
||||
node_dir.path,
|
||||
),
|
||||
env=environ,
|
||||
)
|
||||
yield proto.done
|
||||
|
||||
|
||||
# Which services should this client connect to?
|
||||
write_introducer(node_dir, "default", introducer_furl)
|
||||
with node_dir.child('tahoe.cfg').open('w') as f:
|
||||
node_config = '''
|
||||
[node]
|
||||
nickname = %(name)s
|
||||
web.port = %(web_port)s
|
||||
web.static = public_html
|
||||
log_gatherer.furl = %(log_furl)s
|
||||
util.basic_node_configuration(request, flog_gatherer.furl, node_dir.path)
|
||||
|
||||
[tor]
|
||||
control.port = tcp:localhost:%(control_port)d
|
||||
onion.external_port = 3457
|
||||
onion.local_port = %(local_port)d
|
||||
onion = true
|
||||
onion.private_key_file = private/tor_onion.privkey
|
||||
|
||||
[client]
|
||||
shares.needed = 1
|
||||
shares.happy = 1
|
||||
shares.total = 2
|
||||
|
||||
''' % {
|
||||
'name': name,
|
||||
'web_port': web_port,
|
||||
'log_furl': flog_gatherer,
|
||||
'control_port': control_port,
|
||||
'local_port': control_port + 1000,
|
||||
}
|
||||
node_config = node_config.encode("utf-8")
|
||||
f.write(node_config)
|
||||
config = read_config(node_dir.path, "tub.port")
|
||||
config.set_config("tor", "onion", "true")
|
||||
config.set_config("tor", "onion.external_port", "3457")
|
||||
config.set_config("tor", "control.port", tor_network.client_control_endpoint)
|
||||
config.set_config("tor", "onion.private_key_file", "private/tor_onion.privkey")
|
||||
|
||||
print("running")
|
||||
yield util._run_node(reactor, node_dir.path, request, None)
|
||||
result = yield util._run_node(reactor, node_dir.path, request, None)
|
||||
print("okay, launched")
|
||||
return result
|
||||
|
||||
@pytest.mark.skipif(sys.version_info[:2] > (3, 11), reason='Chutney still does not support 3.12')
|
||||
@pytest.mark.skipif(sys.platform.startswith('darwin'), reason='This test has issues on macOS')
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_anonymous_client(reactor, request, temp_dir, flog_gatherer, tor_network, introducer_furl):
|
||||
"""
|
||||
A normal node (normie) and a normal introducer are configured, and one node
|
||||
(anonymoose) which is configured to be anonymous by talking via Tor.
|
||||
|
||||
Anonymoose should be able to communicate with normie.
|
||||
|
||||
TODO how to ensure that anonymoose is actually using Tor?
|
||||
"""
|
||||
normie = yield util._create_node(
|
||||
reactor, request, temp_dir, introducer_furl, flog_gatherer, "normie",
|
||||
web_port="tcp:9989:interface=localhost",
|
||||
storage=True, needed=1, happy=1, total=1,
|
||||
)
|
||||
yield util.await_client_ready(normie)
|
||||
|
||||
anonymoose = yield _create_anonymous_node(reactor, 'anonymoose', 8102, request, temp_dir, flog_gatherer, tor_network, introducer_furl, 1)
|
||||
yield util.await_client_ready(anonymoose, minimum_number_of_servers=1, timeout=600)
|
||||
|
||||
yield upload_to_one_download_from_the_other(reactor, temp_dir, normie, anonymoose)
|
||||
|
120
integration/test_vectors.py
Normal file
120
integration/test_vectors.py
Normal file
@ -0,0 +1,120 @@
|
||||
"""
|
||||
Verify certain results against test vectors with well-known results.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from typing import AsyncGenerator, Iterator
|
||||
from itertools import starmap, product
|
||||
|
||||
from attrs import evolve
|
||||
|
||||
from pytest import mark
|
||||
from pytest_twisted import ensureDeferred
|
||||
|
||||
from . import vectors
|
||||
from .vectors import parameters
|
||||
from .util import upload
|
||||
from .grid import Client
|
||||
|
||||
@mark.parametrize('convergence', parameters.CONVERGENCE_SECRETS)
|
||||
def test_convergence(convergence):
|
||||
"""
|
||||
Convergence secrets are 16 bytes.
|
||||
"""
|
||||
assert isinstance(convergence, bytes), "Convergence secret must be bytes"
|
||||
assert len(convergence) == 16, "Convergence secret must by 16 bytes"
|
||||
|
||||
|
||||
@mark.slow
|
||||
@mark.parametrize('case,expected', vectors.capabilities.items())
|
||||
@ensureDeferred
|
||||
async def test_capability(reactor, request, alice, case, expected):
|
||||
"""
|
||||
The capability that results from uploading certain well-known data
|
||||
with certain well-known parameters results in exactly the previously
|
||||
computed value.
|
||||
"""
|
||||
# rewrite alice's config to match params and convergence
|
||||
await alice.reconfigure_zfec(
|
||||
reactor, (1, case.params.required, case.params.total), case.convergence, case.segment_size)
|
||||
|
||||
# upload data in the correct format
|
||||
actual = upload(alice, case.fmt, case.data)
|
||||
|
||||
# compare the resulting cap to the expected result
|
||||
assert actual == expected
|
||||
|
||||
|
||||
@ensureDeferred
|
||||
async def skiptest_generate(reactor, request, alice):
|
||||
"""
|
||||
This is a helper for generating the test vectors.
|
||||
|
||||
You can re-generate the test vectors by fixing the name of the test and
|
||||
running it. Normally this test doesn't run because it ran once and we
|
||||
captured its output. Other tests run against that output and we want them
|
||||
to run against the results produced originally, not a possibly
|
||||
ever-changing set of outputs.
|
||||
"""
|
||||
space = starmap(
|
||||
# segment_size could be a parameter someday but it's not easy to vary
|
||||
# using the Python implementation so it isn't one for now.
|
||||
partial(vectors.Case, segment_size=parameters.SEGMENT_SIZE),
|
||||
product(
|
||||
parameters.ZFEC_PARAMS,
|
||||
parameters.CONVERGENCE_SECRETS,
|
||||
parameters.OBJECT_DESCRIPTIONS,
|
||||
parameters.FORMATS,
|
||||
),
|
||||
)
|
||||
iterresults = generate(reactor, request, alice, space)
|
||||
|
||||
results = []
|
||||
async for result in iterresults:
|
||||
# Accumulate the new result
|
||||
results.append(result)
|
||||
# Then rewrite the whole output file with the new accumulator value.
|
||||
# This means that if we fail partway through, we will still have
|
||||
# recorded partial results -- instead of losing them all.
|
||||
vectors.save_capabilities(results)
|
||||
|
||||
async def generate(
|
||||
reactor,
|
||||
request,
|
||||
alice: Client,
|
||||
cases: Iterator[vectors.Case],
|
||||
) -> AsyncGenerator[[vectors.Case, str], None]:
|
||||
"""
|
||||
Generate all of the test vectors using the given node.
|
||||
|
||||
:param reactor: The reactor to use to restart the Tahoe-LAFS node when it
|
||||
needs to be reconfigured.
|
||||
|
||||
:param request: The pytest request object to use to arrange process
|
||||
cleanup.
|
||||
|
||||
:param format: The name of the encryption/data format to use.
|
||||
|
||||
:param alice: The Tahoe-LAFS node to use to generate the test vectors.
|
||||
|
||||
:param case: The inputs for which to generate a value.
|
||||
|
||||
:return: The capability for the case.
|
||||
"""
|
||||
# Share placement doesn't affect the resulting capability. For maximum
|
||||
# reliability of this generator, be happy if we can put shares anywhere
|
||||
happy = 1
|
||||
for case in cases:
|
||||
await alice.reconfigure_zfec(
|
||||
reactor,
|
||||
(happy, case.params.required, case.params.total),
|
||||
case.convergence,
|
||||
case.segment_size
|
||||
)
|
||||
|
||||
# Give the format a chance to make an RSA key if it needs it.
|
||||
case = evolve(case, fmt=case.fmt.customize())
|
||||
cap = upload(alice.process, case.fmt, case.data)
|
||||
yield case, cap
|
@ -7,48 +7,60 @@ Most of the tests have cursory asserts and encode 'what the WebAPI did
|
||||
at the time of testing' -- not necessarily a cohesive idea of what the
|
||||
WebAPI *should* do in every situation. It's not clear the latter
|
||||
exists anywhere, however.
|
||||
|
||||
Ported to Python 3.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
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 __future__ import annotations
|
||||
|
||||
import time
|
||||
from base64 import urlsafe_b64encode
|
||||
from urllib.parse import unquote as url_unquote, quote as url_quote
|
||||
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from twisted.internet.threads import deferToThread
|
||||
from twisted.python.filepath import FilePath
|
||||
|
||||
import allmydata.uri
|
||||
from allmydata.crypto.rsa import (
|
||||
create_signing_keypair,
|
||||
der_string_from_signing_key,
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
)
|
||||
from allmydata.mutable.common import derive_mutable_keys
|
||||
from allmydata.util import jsonbytes as json
|
||||
|
||||
from . import util
|
||||
from .util import run_in_thread
|
||||
|
||||
import requests
|
||||
import html5lib
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
import pytest_twisted
|
||||
|
||||
|
||||
DATA_PATH = FilePath(__file__).parent().sibling("src").child("allmydata").child("test").child("data")
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_index(alice):
|
||||
"""
|
||||
we can download the index file
|
||||
"""
|
||||
util.web_get(alice, u"")
|
||||
util.web_get(alice.process, u"")
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_index_json(alice):
|
||||
"""
|
||||
we can download the index file as json
|
||||
"""
|
||||
data = util.web_get(alice, u"", params={u"t": u"json"})
|
||||
data = util.web_get(alice.process, u"", params={u"t": u"json"})
|
||||
# it should be valid json
|
||||
json.loads(data)
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_upload_download(alice):
|
||||
"""
|
||||
upload a file, then download it via readcap
|
||||
@ -57,7 +69,7 @@ def test_upload_download(alice):
|
||||
FILE_CONTENTS = u"some contents"
|
||||
|
||||
readcap = util.web_post(
|
||||
alice, u"uri",
|
||||
alice.process, u"uri",
|
||||
data={
|
||||
u"t": u"upload",
|
||||
u"format": u"mdmf",
|
||||
@ -69,7 +81,7 @@ def test_upload_download(alice):
|
||||
readcap = readcap.strip()
|
||||
|
||||
data = util.web_get(
|
||||
alice, u"uri",
|
||||
alice.process, u"uri",
|
||||
params={
|
||||
u"uri": readcap,
|
||||
u"filename": u"boom",
|
||||
@ -78,6 +90,7 @@ def test_upload_download(alice):
|
||||
assert str(data, "utf-8") == FILE_CONTENTS
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_put(alice):
|
||||
"""
|
||||
use PUT to create a file
|
||||
@ -86,36 +99,38 @@ def test_put(alice):
|
||||
FILE_CONTENTS = b"added via PUT" * 20
|
||||
|
||||
resp = requests.put(
|
||||
util.node_url(alice.node_dir, u"uri"),
|
||||
util.node_url(alice.process.node_dir, u"uri"),
|
||||
data=FILE_CONTENTS,
|
||||
)
|
||||
cap = allmydata.uri.from_string(resp.text.strip().encode('ascii'))
|
||||
cfg = alice.get_config()
|
||||
cfg = alice.process.get_config()
|
||||
assert isinstance(cap, allmydata.uri.CHKFileURI)
|
||||
assert cap.size == len(FILE_CONTENTS)
|
||||
assert cap.total_shares == int(cfg.get_config("client", "shares.total"))
|
||||
assert cap.needed_shares == int(cfg.get_config("client", "shares.needed"))
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_helper_status(storage_nodes):
|
||||
"""
|
||||
successfully GET the /helper_status page
|
||||
"""
|
||||
|
||||
url = util.node_url(storage_nodes[0].node_dir, "helper_status")
|
||||
url = util.node_url(storage_nodes[0].process.node_dir, "helper_status")
|
||||
resp = requests.get(url)
|
||||
assert resp.status_code >= 200 and resp.status_code < 300
|
||||
dom = BeautifulSoup(resp.content, "html5lib")
|
||||
assert str(dom.h1.string) == u"Helper Status"
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_deep_stats(alice):
|
||||
"""
|
||||
create a directory, do deep-stats on it and prove the /operations/
|
||||
URIs work
|
||||
"""
|
||||
resp = requests.post(
|
||||
util.node_url(alice.node_dir, "uri"),
|
||||
util.node_url(alice.process.node_dir, "uri"),
|
||||
params={
|
||||
"format": "sdmf",
|
||||
"t": "mkdir",
|
||||
@ -129,7 +144,7 @@ def test_deep_stats(alice):
|
||||
uri = url_unquote(resp.url)
|
||||
assert 'URI:DIR2:' in uri
|
||||
dircap = uri[uri.find("URI:DIR2:"):].rstrip('/')
|
||||
dircap_uri = util.node_url(alice.node_dir, "uri/{}".format(url_quote(dircap)))
|
||||
dircap_uri = util.node_url(alice.process.node_dir, "uri/{}".format(url_quote(dircap)))
|
||||
|
||||
# POST a file into this directory
|
||||
FILE_CONTENTS = u"a file in a directory"
|
||||
@ -175,7 +190,7 @@ def test_deep_stats(alice):
|
||||
while tries > 0:
|
||||
tries -= 1
|
||||
resp = requests.get(
|
||||
util.node_url(alice.node_dir, u"operations/something_random"),
|
||||
util.node_url(alice.process.node_dir, u"operations/something_random"),
|
||||
)
|
||||
d = json.loads(resp.content)
|
||||
if d['size-literal-files'] == len(FILE_CONTENTS):
|
||||
@ -186,7 +201,7 @@ def test_deep_stats(alice):
|
||||
time.sleep(.5)
|
||||
|
||||
|
||||
@util.run_in_thread
|
||||
@run_in_thread
|
||||
def test_status(alice):
|
||||
"""
|
||||
confirm we get something sensible from /status and the various sub-types
|
||||
@ -200,21 +215,21 @@ def test_status(alice):
|
||||
FILE_CONTENTS = u"all the Important Data of alice\n" * 1200
|
||||
|
||||
resp = requests.put(
|
||||
util.node_url(alice.node_dir, u"uri"),
|
||||
util.node_url(alice.process.node_dir, u"uri"),
|
||||
data=FILE_CONTENTS,
|
||||
)
|
||||
cap = resp.text.strip()
|
||||
|
||||
print("Uploaded data, cap={}".format(cap))
|
||||
resp = requests.get(
|
||||
util.node_url(alice.node_dir, u"uri/{}".format(url_quote(cap))),
|
||||
util.node_url(alice.process.node_dir, u"uri/{}".format(url_quote(cap))),
|
||||
)
|
||||
|
||||
print("Downloaded {} bytes of data".format(len(resp.content)))
|
||||
assert str(resp.content, "ascii") == FILE_CONTENTS
|
||||
|
||||
resp = requests.get(
|
||||
util.node_url(alice.node_dir, "status"),
|
||||
util.node_url(alice.process.node_dir, "status"),
|
||||
)
|
||||
dom = html5lib.parse(resp.content)
|
||||
|
||||
@ -228,7 +243,7 @@ def test_status(alice):
|
||||
for href in hrefs:
|
||||
if href == u"/" or not href:
|
||||
continue
|
||||
resp = requests.get(util.node_url(alice.node_dir, href))
|
||||
resp = requests.get(util.node_url(alice.process.node_dir, href))
|
||||
if href.startswith(u"/status/up"):
|
||||
assert b"File Upload Status" in resp.content
|
||||
if b"Total Size: %d" % (len(FILE_CONTENTS),) in resp.content:
|
||||
@ -240,7 +255,7 @@ def test_status(alice):
|
||||
|
||||
# download the specialized event information
|
||||
resp = requests.get(
|
||||
util.node_url(alice.node_dir, u"{}/event_json".format(href)),
|
||||
util.node_url(alice.process.node_dir, u"{}/event_json".format(href)),
|
||||
)
|
||||
js = json.loads(resp.content)
|
||||
# there's usually just one "read" operation, but this can handle many ..
|
||||
@ -252,14 +267,25 @@ def test_status(alice):
|
||||
assert found_download, "Failed to find the file we downloaded in the status-page"
|
||||
|
||||
|
||||
def test_directory_deep_check(alice):
|
||||
@pytest_twisted.ensureDeferred
|
||||
async def test_directory_deep_check(reactor, request, alice):
|
||||
"""
|
||||
use deep-check and confirm the result pages work
|
||||
"""
|
||||
# Make sure the node is configured compatibly with expectations of this
|
||||
# test.
|
||||
happy = 3
|
||||
required = 2
|
||||
total = 4
|
||||
|
||||
await alice.reconfigure_zfec(reactor, (happy, required, total), convergence=None)
|
||||
await deferToThread(_test_directory_deep_check_blocking, alice)
|
||||
|
||||
|
||||
def _test_directory_deep_check_blocking(alice):
|
||||
# create a directory
|
||||
resp = requests.post(
|
||||
util.node_url(alice.node_dir, u"uri"),
|
||||
util.node_url(alice.process.node_dir, u"uri"),
|
||||
params={
|
||||
u"t": u"mkdir",
|
||||
u"redirect_to_result": u"true",
|
||||
@ -308,12 +334,12 @@ def test_directory_deep_check(alice):
|
||||
print("Uploaded data1, cap={}".format(cap1))
|
||||
|
||||
resp = requests.get(
|
||||
util.node_url(alice.node_dir, u"uri/{}".format(url_quote(cap0))),
|
||||
util.node_url(alice.process.node_dir, u"uri/{}".format(url_quote(cap0))),
|
||||
params={u"t": u"info"},
|
||||
)
|
||||
|
||||
def check_repair_data(checkdata):
|
||||
assert checkdata["healthy"] is True
|
||||
assert checkdata["healthy"]
|
||||
assert checkdata["count-happiness"] == 4
|
||||
assert checkdata["count-good-share-hosts"] == 4
|
||||
assert checkdata["count-shares-good"] == 4
|
||||
@ -417,6 +443,7 @@ def test_directory_deep_check(alice):
|
||||
assert dom is not None, "Operation never completed"
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_storage_info(storage_nodes):
|
||||
"""
|
||||
retrieve and confirm /storage URI for one storage node
|
||||
@ -424,10 +451,11 @@ def test_storage_info(storage_nodes):
|
||||
storage0 = storage_nodes[0]
|
||||
|
||||
requests.get(
|
||||
util.node_url(storage0.node_dir, u"storage"),
|
||||
util.node_url(storage0.process.node_dir, u"storage"),
|
||||
)
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_storage_info_json(storage_nodes):
|
||||
"""
|
||||
retrieve and confirm /storage?t=json URI for one storage node
|
||||
@ -435,24 +463,25 @@ def test_storage_info_json(storage_nodes):
|
||||
storage0 = storage_nodes[0]
|
||||
|
||||
resp = requests.get(
|
||||
util.node_url(storage0.node_dir, u"storage"),
|
||||
util.node_url(storage0.process.node_dir, u"storage"),
|
||||
params={u"t": u"json"},
|
||||
)
|
||||
data = json.loads(resp.content)
|
||||
assert data[u"stats"][u"storage_server.reserved_space"] == 1000000000
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_introducer_info(introducer):
|
||||
"""
|
||||
retrieve and confirm /introducer URI for the introducer
|
||||
"""
|
||||
resp = requests.get(
|
||||
util.node_url(introducer.node_dir, u""),
|
||||
util.node_url(introducer.process.node_dir, u""),
|
||||
)
|
||||
assert b"Introducer" in resp.content
|
||||
|
||||
resp = requests.get(
|
||||
util.node_url(introducer.node_dir, u""),
|
||||
util.node_url(introducer.process.node_dir, u""),
|
||||
params={u"t": u"json"},
|
||||
)
|
||||
data = json.loads(resp.content)
|
||||
@ -460,6 +489,7 @@ def test_introducer_info(introducer):
|
||||
assert "subscription_summary" in data
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_mkdir_with_children(alice):
|
||||
"""
|
||||
create a directory using ?t=mkdir-with-children
|
||||
@ -468,14 +498,14 @@ def test_mkdir_with_children(alice):
|
||||
# create a file to put in our directory
|
||||
FILE_CONTENTS = u"some file contents\n" * 500
|
||||
resp = requests.put(
|
||||
util.node_url(alice.node_dir, u"uri"),
|
||||
util.node_url(alice.process.node_dir, u"uri"),
|
||||
data=FILE_CONTENTS,
|
||||
)
|
||||
filecap = resp.content.strip()
|
||||
|
||||
# create a (sub) directory to put in our directory
|
||||
resp = requests.post(
|
||||
util.node_url(alice.node_dir, u"uri"),
|
||||
util.node_url(alice.process.node_dir, u"uri"),
|
||||
params={
|
||||
u"t": u"mkdir",
|
||||
}
|
||||
@ -518,10 +548,294 @@ def test_mkdir_with_children(alice):
|
||||
|
||||
# create a new directory with one file and one sub-dir (all-at-once)
|
||||
resp = util.web_post(
|
||||
alice, u"uri",
|
||||
alice.process, u"uri",
|
||||
params={u"t": "mkdir-with-children"},
|
||||
data=json.dumps(meta),
|
||||
)
|
||||
assert resp.startswith(b"URI:DIR2")
|
||||
cap = allmydata.uri.from_string(resp)
|
||||
assert isinstance(cap, allmydata.uri.DirectoryURI)
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_mkdir_with_random_private_key(alice):
|
||||
"""
|
||||
Create a new directory with ?t=mkdir&private-key=... using a
|
||||
randomly-generated RSA private key.
|
||||
|
||||
The writekey and fingerprint derived from the provided RSA key
|
||||
should match those of the newly-created directory capability.
|
||||
"""
|
||||
|
||||
privkey, pubkey = create_signing_keypair(2048)
|
||||
|
||||
writekey, _, fingerprint = derive_mutable_keys((pubkey, privkey))
|
||||
|
||||
# The "private-key" parameter takes a DER-encoded RSA private key
|
||||
# encoded in URL-safe base64; PEM blocks are not supported.
|
||||
privkey_der = der_string_from_signing_key(privkey)
|
||||
privkey_encoded = urlsafe_b64encode(privkey_der).decode("ascii")
|
||||
|
||||
resp = util.web_post(
|
||||
alice.process, u"uri",
|
||||
params={
|
||||
u"t": "mkdir",
|
||||
u"private-key": privkey_encoded,
|
||||
},
|
||||
)
|
||||
assert resp.startswith(b"URI:DIR2")
|
||||
|
||||
dircap = allmydata.uri.from_string(resp)
|
||||
assert isinstance(dircap, allmydata.uri.DirectoryURI)
|
||||
|
||||
# DirectoryURI objects lack 'writekey' and 'fingerprint' attributes
|
||||
# so extract them from the enclosed WriteableSSKFileURI object.
|
||||
filecap = dircap.get_filenode_cap()
|
||||
assert isinstance(filecap, allmydata.uri.WriteableSSKFileURI)
|
||||
|
||||
assert (writekey, fingerprint) == (filecap.writekey, filecap.fingerprint)
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_mkdir_with_known_private_key(alice):
|
||||
"""
|
||||
Create a new directory with ?t=mkdir&private-key=... using a
|
||||
known-in-advance RSA private key.
|
||||
|
||||
The writekey and fingerprint derived from the provided RSA key
|
||||
should match those of the newly-created directory capability.
|
||||
In addition, because the writekey and fingerprint are derived
|
||||
deterministically, given the same RSA private key, the resultant
|
||||
directory capability should always be the same.
|
||||
"""
|
||||
# Generated with `openssl genrsa -out openssl-rsa-2048-3.txt 2048`
|
||||
pempath = DATA_PATH.child("openssl-rsa-2048-3.txt")
|
||||
privkey = load_pem_private_key(pempath.getContent(), password=None)
|
||||
assert isinstance(privkey, PrivateKey)
|
||||
pubkey = privkey.public_key()
|
||||
assert isinstance(pubkey, PublicKey)
|
||||
|
||||
writekey, _, fingerprint = derive_mutable_keys((pubkey, privkey))
|
||||
|
||||
# The "private-key" parameter takes a DER-encoded RSA private key
|
||||
# encoded in URL-safe base64; PEM blocks are not supported.
|
||||
privkey_der = der_string_from_signing_key(privkey)
|
||||
privkey_encoded = urlsafe_b64encode(privkey_der).decode("ascii")
|
||||
|
||||
resp = util.web_post(
|
||||
alice.process, u"uri",
|
||||
params={
|
||||
u"t": "mkdir",
|
||||
u"private-key": privkey_encoded,
|
||||
},
|
||||
)
|
||||
assert resp.startswith(b"URI:DIR2")
|
||||
|
||||
dircap = allmydata.uri.from_string(resp)
|
||||
assert isinstance(dircap, allmydata.uri.DirectoryURI)
|
||||
|
||||
# DirectoryURI objects lack 'writekey' and 'fingerprint' attributes
|
||||
# so extract them from the enclosed WriteableSSKFileURI object.
|
||||
filecap = dircap.get_filenode_cap()
|
||||
assert isinstance(filecap, allmydata.uri.WriteableSSKFileURI)
|
||||
|
||||
assert (writekey, fingerprint) == (filecap.writekey, filecap.fingerprint)
|
||||
|
||||
assert resp == b"URI:DIR2:3oo7j7f7qqxnet2z2lf57ucup4:cpktmsxlqnd5yeekytxjxvff5e6d6fv7py6rftugcndvss7tzd2a"
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_mkdir_with_children_and_random_private_key(alice):
|
||||
"""
|
||||
Create a new directory with ?t=mkdir-with-children&private-key=...
|
||||
using a randomly-generated RSA private key.
|
||||
|
||||
The writekey and fingerprint derived from the provided RSA key
|
||||
should match those of the newly-created directory capability.
|
||||
"""
|
||||
|
||||
# create a file to put in our directory
|
||||
FILE_CONTENTS = u"some file contents\n" * 500
|
||||
resp = requests.put(
|
||||
util.node_url(alice.process.node_dir, u"uri"),
|
||||
data=FILE_CONTENTS,
|
||||
)
|
||||
filecap = resp.content.strip()
|
||||
|
||||
# create a (sub) directory to put in our directory
|
||||
resp = requests.post(
|
||||
util.node_url(alice.process.node_dir, u"uri"),
|
||||
params={
|
||||
u"t": u"mkdir",
|
||||
}
|
||||
)
|
||||
# (we need both the read-write and read-only URIs I guess)
|
||||
dircap = resp.content
|
||||
dircap_obj = allmydata.uri.from_string(dircap)
|
||||
dircap_ro = dircap_obj.get_readonly().to_string()
|
||||
|
||||
# create json information about our directory
|
||||
meta = {
|
||||
"a_file": [
|
||||
"filenode", {
|
||||
"ro_uri": filecap,
|
||||
"metadata": {
|
||||
"ctime": 1202777696.7564139,
|
||||
"mtime": 1202777696.7564139,
|
||||
"tahoe": {
|
||||
"linkcrtime": 1202777696.7564139,
|
||||
"linkmotime": 1202777696.7564139
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"some_subdir": [
|
||||
"dirnode", {
|
||||
"rw_uri": dircap,
|
||||
"ro_uri": dircap_ro,
|
||||
"metadata": {
|
||||
"ctime": 1202778102.7589991,
|
||||
"mtime": 1202778111.2160511,
|
||||
"tahoe": {
|
||||
"linkcrtime": 1202777696.7564139,
|
||||
"linkmotime": 1202777696.7564139
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
privkey, pubkey = create_signing_keypair(2048)
|
||||
|
||||
writekey, _, fingerprint = derive_mutable_keys((pubkey, privkey))
|
||||
|
||||
# The "private-key" parameter takes a DER-encoded RSA private key
|
||||
# encoded in URL-safe base64; PEM blocks are not supported.
|
||||
privkey_der = der_string_from_signing_key(privkey)
|
||||
privkey_encoded = urlsafe_b64encode(privkey_der).decode("ascii")
|
||||
|
||||
# create a new directory with one file and one sub-dir (all-at-once)
|
||||
# with the supplied RSA private key
|
||||
resp = util.web_post(
|
||||
alice.process, u"uri",
|
||||
params={
|
||||
u"t": "mkdir-with-children",
|
||||
u"private-key": privkey_encoded,
|
||||
},
|
||||
data=json.dumps(meta),
|
||||
)
|
||||
assert resp.startswith(b"URI:DIR2")
|
||||
|
||||
dircap = allmydata.uri.from_string(resp)
|
||||
assert isinstance(dircap, allmydata.uri.DirectoryURI)
|
||||
|
||||
# DirectoryURI objects lack 'writekey' and 'fingerprint' attributes
|
||||
# so extract them from the enclosed WriteableSSKFileURI object.
|
||||
filecap = dircap.get_filenode_cap()
|
||||
assert isinstance(filecap, allmydata.uri.WriteableSSKFileURI)
|
||||
|
||||
assert (writekey, fingerprint) == (filecap.writekey, filecap.fingerprint)
|
||||
|
||||
|
||||
@run_in_thread
|
||||
def test_mkdir_with_children_and_known_private_key(alice):
|
||||
"""
|
||||
Create a new directory with ?t=mkdir-with-children&private-key=...
|
||||
using a known-in-advance RSA private key.
|
||||
|
||||
|
||||
The writekey and fingerprint derived from the provided RSA key
|
||||
should match those of the newly-created directory capability.
|
||||
In addition, because the writekey and fingerprint are derived
|
||||
deterministically, given the same RSA private key, the resultant
|
||||
directory capability should always be the same.
|
||||
"""
|
||||
|
||||
# create a file to put in our directory
|
||||
FILE_CONTENTS = u"some file contents\n" * 500
|
||||
resp = requests.put(
|
||||
util.node_url(alice.process.node_dir, u"uri"),
|
||||
data=FILE_CONTENTS,
|
||||
)
|
||||
filecap = resp.content.strip()
|
||||
|
||||
# create a (sub) directory to put in our directory
|
||||
resp = requests.post(
|
||||
util.node_url(alice.process.node_dir, u"uri"),
|
||||
params={
|
||||
u"t": u"mkdir",
|
||||
}
|
||||
)
|
||||
# (we need both the read-write and read-only URIs I guess)
|
||||
dircap = resp.content
|
||||
dircap_obj = allmydata.uri.from_string(dircap)
|
||||
dircap_ro = dircap_obj.get_readonly().to_string()
|
||||
|
||||
# create json information about our directory
|
||||
meta = {
|
||||
"a_file": [
|
||||
"filenode", {
|
||||
"ro_uri": filecap,
|
||||
"metadata": {
|
||||
"ctime": 1202777696.7564139,
|
||||
"mtime": 1202777696.7564139,
|
||||
"tahoe": {
|
||||
"linkcrtime": 1202777696.7564139,
|
||||
"linkmotime": 1202777696.7564139
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"some_subdir": [
|
||||
"dirnode", {
|
||||
"rw_uri": dircap,
|
||||
"ro_uri": dircap_ro,
|
||||
"metadata": {
|
||||
"ctime": 1202778102.7589991,
|
||||
"mtime": 1202778111.2160511,
|
||||
"tahoe": {
|
||||
"linkcrtime": 1202777696.7564139,
|
||||
"linkmotime": 1202777696.7564139
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Generated with `openssl genrsa -out openssl-rsa-2048-4.txt 2048`
|
||||
pempath = DATA_PATH.child("openssl-rsa-2048-4.txt")
|
||||
privkey = load_pem_private_key(pempath.getContent(), password=None)
|
||||
assert isinstance(privkey, PrivateKey)
|
||||
pubkey = privkey.public_key()
|
||||
assert isinstance(pubkey, PublicKey)
|
||||
|
||||
writekey, _, fingerprint = derive_mutable_keys((pubkey, privkey))
|
||||
|
||||
# The "private-key" parameter takes a DER-encoded RSA private key
|
||||
# encoded in URL-safe base64; PEM blocks are not supported.
|
||||
privkey_der = der_string_from_signing_key(privkey)
|
||||
privkey_encoded = urlsafe_b64encode(privkey_der).decode("ascii")
|
||||
|
||||
# create a new directory with one file and one sub-dir (all-at-once)
|
||||
# with the supplied RSA private key
|
||||
resp = util.web_post(
|
||||
alice.process, u"uri",
|
||||
params={
|
||||
u"t": "mkdir-with-children",
|
||||
u"private-key": privkey_encoded,
|
||||
},
|
||||
data=json.dumps(meta),
|
||||
)
|
||||
assert resp.startswith(b"URI:DIR2")
|
||||
|
||||
dircap = allmydata.uri.from_string(resp)
|
||||
assert isinstance(dircap, allmydata.uri.DirectoryURI)
|
||||
|
||||
# DirectoryURI objects lack 'writekey' and 'fingerprint' attributes
|
||||
# so extract them from the enclosed WriteableSSKFileURI object.
|
||||
filecap = dircap.get_filenode_cap()
|
||||
assert isinstance(filecap, allmydata.uri.WriteableSSKFileURI)
|
||||
|
||||
assert (writekey, fingerprint) == (filecap.writekey, filecap.fingerprint)
|
||||
|
||||
assert resp == b"URI:DIR2:ppwzpwrd37xi7tpribxyaa25uy:imdws47wwpzfkc5vfllo4ugspb36iit4cqps6ttuhaouc66jb2da"
|
||||
|
@ -1,22 +1,19 @@
|
||||
"""
|
||||
Ported to Python 3.
|
||||
General functionality useful for the implementation of integration tests.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
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 __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Any
|
||||
from typing_extensions import Literal
|
||||
from tempfile import NamedTemporaryFile
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
from os import mkdir, environ
|
||||
from os.path import exists, join
|
||||
from os.path import exists, join, basename
|
||||
from io import StringIO, BytesIO
|
||||
from functools import partial
|
||||
from subprocess import check_output
|
||||
|
||||
from twisted.python.filepath import (
|
||||
@ -26,18 +23,30 @@ from twisted.internet.defer import Deferred, succeed
|
||||
from twisted.internet.protocol import ProcessProtocol
|
||||
from twisted.internet.error import ProcessExitedAlready, ProcessDone
|
||||
from twisted.internet.threads import deferToThread
|
||||
from twisted.internet.interfaces import IProcessTransport, IReactorProcess
|
||||
|
||||
from attrs import frozen, evolve
|
||||
import requests
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
Encoding,
|
||||
PrivateFormat,
|
||||
NoEncryption,
|
||||
)
|
||||
|
||||
from paramiko.rsakey import RSAKey
|
||||
from boltons.funcutils import wraps
|
||||
|
||||
from allmydata.util import base32
|
||||
from allmydata.util.configutil import (
|
||||
get_config,
|
||||
set_config,
|
||||
write_config,
|
||||
)
|
||||
from allmydata import client
|
||||
from allmydata.interfaces import DEFAULT_IMMUTABLE_MAX_SEGMENT_SIZE
|
||||
|
||||
import pytest_twisted
|
||||
|
||||
@ -61,16 +70,40 @@ class _ProcessExitedProtocol(ProcessProtocol):
|
||||
self.done.callback(None)
|
||||
|
||||
|
||||
class ProcessFailed(Exception):
|
||||
"""
|
||||
A subprocess has failed.
|
||||
|
||||
:ivar ProcessTerminated reason: the original reason from .processExited
|
||||
|
||||
:ivar StringIO output: all stdout and stderr collected to this point.
|
||||
"""
|
||||
|
||||
def __init__(self, reason, output):
|
||||
self.reason = reason
|
||||
self.output = output
|
||||
|
||||
def __str__(self):
|
||||
return "<ProcessFailed: {}>:\n{}".format(self.reason, self.output)
|
||||
|
||||
|
||||
class _CollectOutputProtocol(ProcessProtocol):
|
||||
"""
|
||||
Internal helper. Collects all output (stdout + stderr) into
|
||||
self.output, and callback's on done with all of it after the
|
||||
process exits (for any reason).
|
||||
"""
|
||||
def __init__(self, capture_stderr=True):
|
||||
|
||||
def __init__(self, capture_stderr=True, stdin=None):
|
||||
self.done = Deferred()
|
||||
self.output = BytesIO()
|
||||
self.capture_stderr = capture_stderr
|
||||
self._stdin = stdin
|
||||
|
||||
def connectionMade(self):
|
||||
if self._stdin is not None:
|
||||
self.transport.write(self._stdin)
|
||||
self.transport.closeStdin()
|
||||
|
||||
def processEnded(self, reason):
|
||||
if not self.done.called:
|
||||
@ -78,13 +111,12 @@ class _CollectOutputProtocol(ProcessProtocol):
|
||||
|
||||
def processExited(self, reason):
|
||||
if not isinstance(reason.value, ProcessDone):
|
||||
self.done.errback(reason)
|
||||
self.done.errback(ProcessFailed(reason, self.output.getvalue()))
|
||||
|
||||
def outReceived(self, data):
|
||||
self.output.write(data)
|
||||
|
||||
def errReceived(self, data):
|
||||
print("ERR: {!r}".format(data))
|
||||
if self.capture_stderr:
|
||||
self.output.write(data)
|
||||
|
||||
@ -120,8 +152,9 @@ class _MagicTextProtocol(ProcessProtocol):
|
||||
and then .callback()s on self.done and .errback's if the process exits
|
||||
"""
|
||||
|
||||
def __init__(self, magic_text):
|
||||
def __init__(self, magic_text: str, name: str) -> None:
|
||||
self.magic_seen = Deferred()
|
||||
self.name = f"{name}: "
|
||||
self.exited = Deferred()
|
||||
self._magic_text = magic_text
|
||||
self._output = StringIO()
|
||||
@ -131,7 +164,8 @@ class _MagicTextProtocol(ProcessProtocol):
|
||||
|
||||
def outReceived(self, data):
|
||||
data = str(data, sys.stdout.encoding)
|
||||
sys.stdout.write(data)
|
||||
for line in data.splitlines():
|
||||
sys.stdout.write(self.name + line + "\n")
|
||||
self._output.write(data)
|
||||
if not self.magic_seen.called and self._magic_text in self._output.getvalue():
|
||||
print("Saw '{}' in the logs".format(self._magic_text))
|
||||
@ -139,12 +173,39 @@ class _MagicTextProtocol(ProcessProtocol):
|
||||
|
||||
def errReceived(self, data):
|
||||
data = str(data, sys.stderr.encoding)
|
||||
sys.stdout.write(data)
|
||||
for line in data.splitlines():
|
||||
sys.stdout.write(self.name + line + "\n")
|
||||
|
||||
|
||||
def _cleanup_process_async(transport: IProcessTransport) -> None:
|
||||
"""
|
||||
If the given process transport seems to still be associated with a
|
||||
running process, send a SIGTERM to that process.
|
||||
|
||||
:param transport: The transport to use.
|
||||
|
||||
:raise: ``ValueError`` if ``allow_missing`` is ``False`` and the transport
|
||||
has no process.
|
||||
"""
|
||||
if transport.pid is None:
|
||||
# in cases of "restart", we will have registered a finalizer
|
||||
# that will kill the process -- but already explicitly killed
|
||||
# it (and then ran again) due to the "restart". So, if the
|
||||
# process is already killed, our job is done.
|
||||
print("Process already cleaned up and that's okay.")
|
||||
return
|
||||
print("signaling {} with TERM".format(transport.pid))
|
||||
try:
|
||||
transport.signalProcess('TERM')
|
||||
except ProcessExitedAlready:
|
||||
# The transport object thought it still had a process but the real OS
|
||||
# process has already exited. That's fine. We accomplished what we
|
||||
# wanted to.
|
||||
pass
|
||||
|
||||
def _cleanup_tahoe_process(tahoe_transport, exited):
|
||||
"""
|
||||
Terminate the given process with a kill signal (SIGKILL on POSIX,
|
||||
Terminate the given process with a kill signal (SIGTERM on POSIX,
|
||||
TerminateProcess on Windows).
|
||||
|
||||
:param tahoe_transport: The `IProcessTransport` representing the process.
|
||||
@ -153,14 +214,24 @@ def _cleanup_tahoe_process(tahoe_transport, exited):
|
||||
:return: After the process has exited.
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
try:
|
||||
print("signaling {} with TERM".format(tahoe_transport.pid))
|
||||
tahoe_transport.signalProcess('TERM')
|
||||
print("signaled, blocking on exit")
|
||||
block_with_timeout(exited, reactor)
|
||||
print("exited, goodbye")
|
||||
except ProcessExitedAlready:
|
||||
pass
|
||||
_cleanup_process_async(tahoe_transport)
|
||||
print(f"signaled, blocking on exit {exited}")
|
||||
block_with_timeout(exited, reactor)
|
||||
print("exited, goodbye")
|
||||
|
||||
|
||||
def run_tahoe(reactor, request, *args, **kwargs):
|
||||
"""
|
||||
Helper to run tahoe with optional coverage.
|
||||
|
||||
:returns: a Deferred that fires when the command is done (or a
|
||||
ProcessFailed exception if it exits non-zero)
|
||||
"""
|
||||
stdin = kwargs.get("stdin", None)
|
||||
protocol = _CollectOutputProtocol(stdin=stdin)
|
||||
process = _tahoe_runner_optional_coverage(protocol, reactor, request, args)
|
||||
process.exited = protocol.done
|
||||
return protocol.done
|
||||
|
||||
|
||||
def _tahoe_runner_optional_coverage(proto, reactor, request, other_args):
|
||||
@ -169,7 +240,7 @@ def _tahoe_runner_optional_coverage(proto, reactor, request, other_args):
|
||||
allmydata.scripts.runner` and `other_args`, optionally inserting a
|
||||
`--coverage` option if the `request` indicates we should.
|
||||
"""
|
||||
if request.config.getoption('coverage'):
|
||||
if request.config.getoption('coverage', False):
|
||||
args = [sys.executable, '-b', '-m', 'coverage', 'run', '-m', 'allmydata.scripts.runner', '--coverage']
|
||||
else:
|
||||
args = [sys.executable, '-b', '-m', 'allmydata.scripts.runner']
|
||||
@ -206,14 +277,43 @@ class TahoeProcess(object):
|
||||
)
|
||||
|
||||
def kill(self):
|
||||
"""Kill the process, block until it's done."""
|
||||
"""
|
||||
Kill the process, block until it's done.
|
||||
Does nothing if the process is already stopped (or never started).
|
||||
"""
|
||||
print(f"TahoeProcess.kill({self.transport.pid} / {self.node_dir})")
|
||||
_cleanup_tahoe_process(self.transport, self.transport.exited)
|
||||
|
||||
def kill_async(self):
|
||||
"""
|
||||
Kill the process, return a Deferred that fires when it's done.
|
||||
Does nothing if the process is already stopped (or never started).
|
||||
"""
|
||||
print(f"TahoeProcess.kill_async({self.transport.pid} / {self.node_dir})")
|
||||
_cleanup_process_async(self.transport)
|
||||
return self.transport.exited
|
||||
|
||||
def restart_async(self, reactor: IReactorProcess, request: Any) -> Deferred:
|
||||
"""
|
||||
Stop and then re-start the associated process.
|
||||
|
||||
:return: A Deferred that fires after the new process is ready to
|
||||
handle requests.
|
||||
"""
|
||||
d = self.kill_async()
|
||||
d.addCallback(lambda ignored: _run_node(reactor, self.node_dir, request, None))
|
||||
def got_new_process(proc):
|
||||
# Grab the new transport since the one we had before is no longer
|
||||
# valid after the stop/start cycle.
|
||||
self._process_transport = proc.transport
|
||||
d.addCallback(got_new_process)
|
||||
return d
|
||||
|
||||
def __str__(self):
|
||||
return "<TahoeProcess in '{}'>".format(self._node_dir)
|
||||
|
||||
|
||||
def _run_node(reactor, node_dir, request, magic_text, finalize=True):
|
||||
def _run_node(reactor, node_dir, request, magic_text):
|
||||
"""
|
||||
Run a tahoe process from its node_dir.
|
||||
|
||||
@ -221,7 +321,7 @@ def _run_node(reactor, node_dir, request, magic_text, finalize=True):
|
||||
"""
|
||||
if magic_text is None:
|
||||
magic_text = "client running"
|
||||
protocol = _MagicTextProtocol(magic_text)
|
||||
protocol = _MagicTextProtocol(magic_text, basename(node_dir))
|
||||
|
||||
# "tahoe run" is consistent across Linux/macOS/Windows, unlike the old
|
||||
# "start" command.
|
||||
@ -237,19 +337,46 @@ def _run_node(reactor, node_dir, request, magic_text, finalize=True):
|
||||
)
|
||||
transport.exited = protocol.exited
|
||||
|
||||
if finalize:
|
||||
request.addfinalizer(partial(_cleanup_tahoe_process, transport, protocol.exited))
|
||||
tahoe_process = TahoeProcess(
|
||||
transport,
|
||||
node_dir,
|
||||
)
|
||||
|
||||
# XXX abusing the Deferred; should use .when_magic_seen() pattern
|
||||
request.addfinalizer(tahoe_process.kill)
|
||||
|
||||
def got_proto(proto):
|
||||
transport._protocol = proto
|
||||
return TahoeProcess(
|
||||
transport,
|
||||
node_dir,
|
||||
)
|
||||
protocol.magic_seen.addCallback(got_proto)
|
||||
return protocol.magic_seen
|
||||
d = protocol.magic_seen
|
||||
d.addCallback(lambda ignored: tahoe_process)
|
||||
return d
|
||||
|
||||
|
||||
def basic_node_configuration(request, flog_gatherer, node_dir: str):
|
||||
"""
|
||||
Setup common configuration options for a node, given a ``pytest`` request
|
||||
fixture.
|
||||
"""
|
||||
config_path = join(node_dir, 'tahoe.cfg')
|
||||
config = get_config(config_path)
|
||||
set_config(
|
||||
config,
|
||||
u'node',
|
||||
u'log_gatherer.furl',
|
||||
flog_gatherer,
|
||||
)
|
||||
force_foolscap = request.config.getoption("force_foolscap")
|
||||
assert force_foolscap in (True, False)
|
||||
set_config(
|
||||
config,
|
||||
'storage',
|
||||
'force_foolscap',
|
||||
str(force_foolscap),
|
||||
)
|
||||
set_config(
|
||||
config,
|
||||
'client',
|
||||
'force_foolscap',
|
||||
str(force_foolscap),
|
||||
)
|
||||
write_config(FilePath(config_path), config)
|
||||
|
||||
|
||||
def _create_node(reactor, request, temp_dir, introducer_furl, flog_gatherer, name, web_port,
|
||||
@ -257,8 +384,7 @@ def _create_node(reactor, request, temp_dir, introducer_furl, flog_gatherer, nam
|
||||
magic_text=None,
|
||||
needed=2,
|
||||
happy=3,
|
||||
total=4,
|
||||
finalize=True):
|
||||
total=4):
|
||||
"""
|
||||
Helper to create a single node, run it and return the instance
|
||||
spawnProcess returned (ITransport)
|
||||
@ -269,7 +395,7 @@ def _create_node(reactor, request, temp_dir, introducer_furl, flog_gatherer, nam
|
||||
if exists(node_dir):
|
||||
created_d = succeed(None)
|
||||
else:
|
||||
print("creating", node_dir)
|
||||
print("creating: {}".format(node_dir))
|
||||
mkdir(node_dir)
|
||||
done_proto = _ProcessExitedProtocol()
|
||||
args = [
|
||||
@ -292,21 +418,13 @@ def _create_node(reactor, request, temp_dir, introducer_furl, flog_gatherer, nam
|
||||
created_d = done_proto.done
|
||||
|
||||
def created(_):
|
||||
config_path = join(node_dir, 'tahoe.cfg')
|
||||
config = get_config(config_path)
|
||||
set_config(
|
||||
config,
|
||||
u'node',
|
||||
u'log_gatherer.furl',
|
||||
flog_gatherer,
|
||||
)
|
||||
write_config(FilePath(config_path), config)
|
||||
basic_node_configuration(request, flog_gatherer.furl, node_dir)
|
||||
created_d.addCallback(created)
|
||||
|
||||
d = Deferred()
|
||||
d.callback(None)
|
||||
d.addCallback(lambda _: created_d)
|
||||
d.addCallback(lambda _: _run_node(reactor, node_dir, request, magic_text, finalize=finalize))
|
||||
d.addCallback(lambda _: _run_node(reactor, node_dir, request, magic_text))
|
||||
return d
|
||||
|
||||
|
||||
@ -357,6 +475,31 @@ class FileShouldVanishException(Exception):
|
||||
)
|
||||
|
||||
|
||||
def run_in_thread(f):
|
||||
"""Decorator for integration tests that runs code in a thread.
|
||||
|
||||
Because we're using pytest_twisted, tests that rely on the reactor are
|
||||
expected to return a Deferred and use async APIs so the reactor can run.
|
||||
|
||||
In the case of the integration test suite, it launches nodes in the
|
||||
background using Twisted APIs. The nodes stdout and stderr is read via
|
||||
Twisted code. If the reactor doesn't run, reads don't happen, and
|
||||
eventually the buffers fill up, and the nodes block when they try to flush
|
||||
logs.
|
||||
|
||||
We can switch to Twisted APIs (treq instead of requests etc.), but
|
||||
sometimes it's easier or expedient to just have a blocking test. So this
|
||||
decorator allows you to run the test in a thread, and the reactor can keep
|
||||
running in the main thread.
|
||||
|
||||
See https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3597 for tracking bug.
|
||||
"""
|
||||
@wraps(f)
|
||||
def test(*args, **kwargs):
|
||||
return deferToThread(lambda: f(*args, **kwargs))
|
||||
return test
|
||||
|
||||
|
||||
def await_file_contents(path, contents, timeout=15, error_if=None):
|
||||
"""
|
||||
wait up to `timeout` seconds for the file at `path` (any path-like
|
||||
@ -482,14 +625,16 @@ def web_post(tahoe, uri_fragment, **kwargs):
|
||||
return resp.content
|
||||
|
||||
|
||||
def await_client_ready(tahoe, timeout=10, liveness=60*2):
|
||||
@run_in_thread
|
||||
def await_client_ready(tahoe, timeout=10, liveness=60*2, minimum_number_of_servers=1):
|
||||
"""
|
||||
Uses the status API to wait for a client-type node (in `tahoe`, a
|
||||
`TahoeProcess` instance usually from a fixture e.g. `alice`) to be
|
||||
'ready'. A client is deemed ready if:
|
||||
|
||||
- it answers `http://<node_url>/statistics/?t=json/`
|
||||
- there is at least one storage-server connected
|
||||
- there is at least one storage-server connected (configurable via
|
||||
``minimum_number_of_servers``)
|
||||
- every storage-server has a "last_received_data" and it is
|
||||
within the last `liveness` seconds
|
||||
|
||||
@ -505,25 +650,35 @@ def await_client_ready(tahoe, timeout=10, liveness=60*2):
|
||||
print("waiting because '{}'".format(e))
|
||||
time.sleep(1)
|
||||
continue
|
||||
servers = js['servers']
|
||||
|
||||
if len(js['servers']) == 0:
|
||||
print("waiting because no servers at all")
|
||||
if len(servers) < minimum_number_of_servers:
|
||||
print(f"waiting because {servers} is fewer than required ({minimum_number_of_servers})")
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
now = time.time()
|
||||
server_times = [
|
||||
server['last_received_data']
|
||||
for server in js['servers']
|
||||
for server
|
||||
in servers
|
||||
if server['last_received_data'] is not None
|
||||
]
|
||||
# if any times are null/None that server has never been
|
||||
# contacted (so it's down still, probably)
|
||||
if any(t is None for t in server_times):
|
||||
print("waiting because at least one server not contacted")
|
||||
time.sleep(1)
|
||||
continue
|
||||
print(
|
||||
f"Now: {time.ctime(now)}\n"
|
||||
f"Liveness required: {liveness}\n"
|
||||
f"Server last-received-data: {[time.ctime(s) for s in server_times]}\n"
|
||||
f"Server ages: {[now - s for s in server_times]}\n"
|
||||
)
|
||||
|
||||
# check that all times are 'recent enough'
|
||||
if any([time.time() - t > liveness for t in server_times]):
|
||||
print("waiting because at least one server too old")
|
||||
# check that all times are 'recent enough' (it's OK if _some_ servers
|
||||
# are down, we just want to make sure a sufficient number are up)
|
||||
alive = [t for t in server_times if now - t <= liveness]
|
||||
if len(alive) < minimum_number_of_servers:
|
||||
print(
|
||||
f"waiting because we found {len(alive)} servers "
|
||||
f"and want {minimum_number_of_servers}"
|
||||
)
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
@ -548,26 +703,171 @@ def generate_ssh_key(path):
|
||||
f.write(s.encode("ascii"))
|
||||
|
||||
|
||||
def run_in_thread(f):
|
||||
"""Decorator for integration tests that runs code in a thread.
|
||||
|
||||
Because we're using pytest_twisted, tests that rely on the reactor are
|
||||
expected to return a Deferred and use async APIs so the reactor can run.
|
||||
|
||||
In the case of the integration test suite, it launches nodes in the
|
||||
background using Twisted APIs. The nodes stdout and stderr is read via
|
||||
Twisted code. If the reactor doesn't run, reads don't happen, and
|
||||
eventually the buffers fill up, and the nodes block when they try to flush
|
||||
logs.
|
||||
|
||||
We can switch to Twisted APIs (treq instead of requests etc.), but
|
||||
sometimes it's easier or expedient to just have a blocking test. So this
|
||||
decorator allows you to run the test in a thread, and the reactor can keep
|
||||
running in the main thread.
|
||||
|
||||
See https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3597 for tracking bug.
|
||||
@frozen
|
||||
class CHK:
|
||||
"""
|
||||
@wraps(f)
|
||||
def test(*args, **kwargs):
|
||||
return deferToThread(lambda: f(*args, **kwargs))
|
||||
return test
|
||||
Represent the CHK encoding sufficiently to run a ``tahoe put`` command
|
||||
using it.
|
||||
"""
|
||||
kind = "chk"
|
||||
max_shares = 256
|
||||
|
||||
def customize(self) -> CHK:
|
||||
# Nothing to do.
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def load(cls, params: None) -> CHK:
|
||||
assert params is None
|
||||
return cls()
|
||||
|
||||
def to_json(self) -> None:
|
||||
return None
|
||||
|
||||
@contextmanager
|
||||
def to_argv(self) -> None:
|
||||
yield []
|
||||
|
||||
@frozen
|
||||
class SSK:
|
||||
"""
|
||||
Represent the SSK encodings (SDMF and MDMF) sufficiently to run a
|
||||
``tahoe put`` command using one of them.
|
||||
"""
|
||||
kind = "ssk"
|
||||
|
||||
# SDMF and MDMF encode share counts (N and k) into the share itself as an
|
||||
# unsigned byte. They could have encoded (share count - 1) to fit the
|
||||
# full range supported by ZFEC into the unsigned byte - but they don't.
|
||||
# So 256 is inaccessible to those formats and we set the upper bound at
|
||||
# 255.
|
||||
max_shares = 255
|
||||
|
||||
name: Literal["sdmf", "mdmf"]
|
||||
key: None | bytes
|
||||
|
||||
@classmethod
|
||||
def load(cls, params: dict) -> SSK:
|
||||
assert params.keys() == {"format", "mutable", "key"}
|
||||
return cls(params["format"], params["key"].encode("ascii"))
|
||||
def customize(self) -> SSK:
|
||||
"""
|
||||
Return an SSK with a newly generated random RSA key.
|
||||
"""
|
||||
return evolve(self, key=generate_rsa_key())
|
||||
|
||||
def to_json(self) -> dict[str, str]:
|
||||
return {
|
||||
"format": self.name,
|
||||
"mutable": None,
|
||||
"key": self.key.decode("ascii"),
|
||||
}
|
||||
|
||||
@contextmanager
|
||||
def to_argv(self) -> None:
|
||||
with NamedTemporaryFile() as f:
|
||||
f.write(self.key)
|
||||
f.flush()
|
||||
yield [f"--format={self.name}", "--mutable", f"--private-key-path={f.name}"]
|
||||
|
||||
|
||||
def upload(alice: TahoeProcess, fmt: CHK | SSK, data: bytes) -> str:
|
||||
"""
|
||||
Upload the given data to the given node.
|
||||
|
||||
:param alice: The node to upload to.
|
||||
|
||||
:param fmt: The name of the format for the upload. CHK, SDMF, or MDMF.
|
||||
|
||||
:param data: The data to upload.
|
||||
|
||||
:return: The capability for the uploaded data.
|
||||
"""
|
||||
|
||||
with NamedTemporaryFile() as f:
|
||||
f.write(data)
|
||||
f.flush()
|
||||
with fmt.to_argv() as fmt_argv:
|
||||
argv = [alice.process, "put"] + fmt_argv + [f.name]
|
||||
return cli(*argv).decode("utf-8").strip()
|
||||
|
||||
|
||||
async def reconfigure(reactor, request, node: TahoeProcess,
|
||||
params: tuple[int, int, int],
|
||||
convergence: None | bytes,
|
||||
max_segment_size: None | int = None) -> None:
|
||||
"""
|
||||
Reconfigure a Tahoe-LAFS node with different ZFEC parameters and
|
||||
convergence secret.
|
||||
|
||||
TODO This appears to have issues on Windows.
|
||||
|
||||
If the current configuration is different from the specified
|
||||
configuration, the node will be restarted so it takes effect.
|
||||
|
||||
:param reactor: A reactor to use to restart the process.
|
||||
:param request: The pytest request object to use to arrange process
|
||||
cleanup.
|
||||
:param node: The Tahoe-LAFS node to reconfigure.
|
||||
:param params: The ``happy``, ``needed``, and ``total`` ZFEC encoding
|
||||
parameters.
|
||||
:param convergence: If given, the convergence secret. If not given, the
|
||||
existing convergence secret will be left alone.
|
||||
|
||||
:return: ``None`` after the node configuration has been rewritten, the
|
||||
node has been restarted, and the node is ready to provide service.
|
||||
"""
|
||||
happy, needed, total = params
|
||||
config = node.get_config()
|
||||
|
||||
changed = False
|
||||
cur_happy = int(config.get_config("client", "shares.happy"))
|
||||
cur_needed = int(config.get_config("client", "shares.needed"))
|
||||
cur_total = int(config.get_config("client", "shares.total"))
|
||||
|
||||
if (happy, needed, total) != (cur_happy, cur_needed, cur_total):
|
||||
changed = True
|
||||
config.set_config("client", "shares.happy", str(happy))
|
||||
config.set_config("client", "shares.needed", str(needed))
|
||||
config.set_config("client", "shares.total", str(total))
|
||||
|
||||
if convergence is not None:
|
||||
cur_convergence = config.get_private_config("convergence").encode("ascii")
|
||||
if base32.a2b(cur_convergence) != convergence:
|
||||
changed = True
|
||||
config.write_private_config("convergence", base32.b2a(convergence))
|
||||
|
||||
if max_segment_size is not None:
|
||||
cur_segment_size = int(config.get_config("client", "shares._max_immutable_segment_size_for_testing", DEFAULT_IMMUTABLE_MAX_SEGMENT_SIZE))
|
||||
if cur_segment_size != max_segment_size:
|
||||
changed = True
|
||||
config.set_config(
|
||||
"client",
|
||||
"shares._max_immutable_segment_size_for_testing",
|
||||
str(max_segment_size)
|
||||
)
|
||||
|
||||
if changed:
|
||||
# restart the node
|
||||
print(f"Restarting {node.node_dir} for ZFEC reconfiguration")
|
||||
await node.restart_async(reactor, request)
|
||||
print("Restarted. Waiting for ready state.")
|
||||
await await_client_ready(node)
|
||||
print("Ready.")
|
||||
else:
|
||||
print("Config unchanged, not restarting.")
|
||||
|
||||
|
||||
def generate_rsa_key() -> bytes:
|
||||
"""
|
||||
Generate a 2048 bit RSA key suitable for use with SSKs.
|
||||
"""
|
||||
return rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=2048,
|
||||
backend=default_backend()
|
||||
).private_bytes(
|
||||
encoding=Encoding.PEM,
|
||||
format=PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=NoEncryption(),
|
||||
)
|
||||
|
30
integration/vectors/__init__.py
Normal file
30
integration/vectors/__init__.py
Normal file
@ -0,0 +1,30 @@
|
||||
__all__ = [
|
||||
"DATA_PATH",
|
||||
"CURRENT_VERSION",
|
||||
"MAX_SHARES",
|
||||
|
||||
"Case",
|
||||
"Sample",
|
||||
"SeedParam",
|
||||
"encode_bytes",
|
||||
"save_capabilities",
|
||||
|
||||
"capabilities",
|
||||
]
|
||||
|
||||
from .vectors import (
|
||||
DATA_PATH,
|
||||
CURRENT_VERSION,
|
||||
|
||||
Case,
|
||||
Sample,
|
||||
SeedParam,
|
||||
encode_bytes,
|
||||
save_capabilities,
|
||||
|
||||
capabilities,
|
||||
)
|
||||
|
||||
from .parameters import (
|
||||
MAX_SHARES,
|
||||
)
|
58
integration/vectors/model.py
Normal file
58
integration/vectors/model.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""
|
||||
Simple data type definitions useful in the definition/verification of test
|
||||
vectors.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from attrs import frozen
|
||||
|
||||
# CHK have a max of 256 shares. SDMF / MDMF have a max of 255 shares!
|
||||
# Represent max symbolically and resolve it when we know what format we're
|
||||
# dealing with.
|
||||
MAX_SHARES = "max"
|
||||
|
||||
@frozen
|
||||
class Sample:
|
||||
"""
|
||||
Some instructions for building a long byte string.
|
||||
|
||||
:ivar seed: Some bytes to repeat some times to produce the string.
|
||||
:ivar length: The length of the desired byte string.
|
||||
"""
|
||||
seed: bytes
|
||||
length: int
|
||||
|
||||
@frozen
|
||||
class Param:
|
||||
"""
|
||||
Some ZFEC parameters.
|
||||
"""
|
||||
required: int
|
||||
total: int
|
||||
|
||||
@frozen
|
||||
class SeedParam:
|
||||
"""
|
||||
Some ZFEC parameters, almost.
|
||||
|
||||
:ivar required: The number of required shares.
|
||||
|
||||
:ivar total: Either the number of total shares or the constant
|
||||
``MAX_SHARES`` to indicate that the total number of shares should be
|
||||
the maximum number supported by the object format.
|
||||
"""
|
||||
required: int
|
||||
total: int | str
|
||||
|
||||
def realize(self, max_total: int) -> Param:
|
||||
"""
|
||||
Create a ``Param`` from this object's values, possibly
|
||||
substituting the given real value for total if necessary.
|
||||
|
||||
:param max_total: The value to use to replace ``MAX_SHARES`` if
|
||||
necessary.
|
||||
"""
|
||||
if self.total == MAX_SHARES:
|
||||
return Param(self.required, max_total)
|
||||
return Param(self.required, self.total)
|
93
integration/vectors/parameters.py
Normal file
93
integration/vectors/parameters.py
Normal file
@ -0,0 +1,93 @@
|
||||
"""
|
||||
Define input parameters for test vector generation.
|
||||
|
||||
:ivar CONVERGENCE_SECRETS: Convergence secrets.
|
||||
|
||||
:ivar SEGMENT_SIZE: The single segment size that the Python implementation
|
||||
currently supports without a lot of refactoring.
|
||||
|
||||
:ivar OBJECT_DESCRIPTIONS: Small objects with instructions which can be
|
||||
expanded into a possibly large byte string. These are intended to be used
|
||||
as plaintext inputs.
|
||||
|
||||
:ivar ZFEC_PARAMS: Input parameters to ZFEC.
|
||||
|
||||
:ivar FORMATS: Encoding/encryption formats.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from hashlib import sha256
|
||||
|
||||
from .model import MAX_SHARES
|
||||
from .vectors import Sample, SeedParam
|
||||
from ..util import CHK, SSK
|
||||
|
||||
def digest(bs: bytes) -> bytes:
|
||||
"""
|
||||
Digest bytes to bytes.
|
||||
"""
|
||||
return sha256(bs).digest()
|
||||
|
||||
|
||||
def hexdigest(bs: bytes) -> str:
|
||||
"""
|
||||
Digest bytes to text.
|
||||
"""
|
||||
return sha256(bs).hexdigest()
|
||||
|
||||
# Just a couple convergence secrets. The only thing we do with this value is
|
||||
# feed it into a tagged hash. It certainly makes a difference to the output
|
||||
# but the hash should destroy any structure in the input so it doesn't seem
|
||||
# like there's a reason to test a lot of different values.
|
||||
CONVERGENCE_SECRETS: list[bytes] = [
|
||||
b"aaaaaaaaaaaaaaaa",
|
||||
digest(b"Hello world")[:16],
|
||||
]
|
||||
|
||||
SEGMENT_SIZE: int = 128 * 1024
|
||||
|
||||
# Exercise at least a handful of different sizes, trying to cover:
|
||||
#
|
||||
# 1. Some cases smaller than one "segment" (128k).
|
||||
# This covers shrinking of some parameters to match data size.
|
||||
# This includes one case of the smallest possible CHK.
|
||||
#
|
||||
# 2. Some cases right on the edges of integer segment multiples.
|
||||
# Because boundaries are tricky.
|
||||
#
|
||||
# 4. Some cases that involve quite a few segments.
|
||||
# This exercises merkle tree construction more thoroughly.
|
||||
#
|
||||
# See ``stretch`` for construction of the actual test data.
|
||||
OBJECT_DESCRIPTIONS: list[Sample] = [
|
||||
# The smallest possible. 55 bytes and smaller are LIT.
|
||||
Sample(b"a", 56),
|
||||
Sample(b"a", 1024),
|
||||
Sample(b"c", 4096),
|
||||
Sample(digest(b"foo"), SEGMENT_SIZE - 1),
|
||||
Sample(digest(b"bar"), SEGMENT_SIZE + 1),
|
||||
Sample(digest(b"baz"), SEGMENT_SIZE * 16 - 1),
|
||||
Sample(digest(b"quux"), SEGMENT_SIZE * 16 + 1),
|
||||
Sample(digest(b"bazquux"), SEGMENT_SIZE * 32),
|
||||
Sample(digest(b"foobar"), SEGMENT_SIZE * 64 - 1),
|
||||
Sample(digest(b"barbaz"), SEGMENT_SIZE * 64 + 1),
|
||||
]
|
||||
|
||||
ZFEC_PARAMS: list[SeedParam] = [
|
||||
SeedParam(1, 1),
|
||||
SeedParam(1, 3),
|
||||
SeedParam(2, 3),
|
||||
SeedParam(3, 10),
|
||||
SeedParam(71, 255),
|
||||
SeedParam(101, MAX_SHARES),
|
||||
]
|
||||
|
||||
FORMATS: list[CHK | SSK] = [
|
||||
CHK(),
|
||||
|
||||
# These start out unaware of a key but various keys will be supplied
|
||||
# during generation.
|
||||
SSK(name="sdmf", key=None),
|
||||
SSK(name="mdmf", key=None),
|
||||
]
|
18002
integration/vectors/test_vectors.yaml
Executable file
18002
integration/vectors/test_vectors.yaml
Executable file
File diff suppressed because it is too large
Load Diff
155
integration/vectors/vectors.py
Normal file
155
integration/vectors/vectors.py
Normal file
@ -0,0 +1,155 @@
|
||||
"""
|
||||
A module that loads pre-generated test vectors.
|
||||
|
||||
:ivar DATA_PATH: The path of the file containing test vectors.
|
||||
|
||||
:ivar capabilities: The capability test vectors.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TextIO
|
||||
from attrs import frozen
|
||||
from yaml import safe_load, safe_dump
|
||||
from base64 import b64encode, b64decode
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
|
||||
from .model import Param, Sample, SeedParam
|
||||
from ..util import CHK, SSK
|
||||
|
||||
DATA_PATH: FilePath = FilePath(__file__).sibling("test_vectors.yaml")
|
||||
|
||||
# The version of the persisted test vector data this code can interpret.
|
||||
CURRENT_VERSION: str = "2023-01-16.2"
|
||||
|
||||
@frozen
|
||||
class Case:
|
||||
"""
|
||||
Represent one case for which we want/have a test vector.
|
||||
"""
|
||||
seed_params: Param
|
||||
convergence: bytes
|
||||
seed_data: Sample
|
||||
fmt: CHK | SSK
|
||||
segment_size: int
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return stretch(self.seed_data.seed, self.seed_data.length)
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
return self.seed_params.realize(self.fmt.max_shares)
|
||||
|
||||
|
||||
def encode_bytes(b: bytes) -> str:
|
||||
"""
|
||||
Base64 encode some bytes to text so they are representable in JSON.
|
||||
"""
|
||||
return b64encode(b).decode("ascii")
|
||||
|
||||
|
||||
def decode_bytes(b: str) -> bytes:
|
||||
"""
|
||||
Base64 decode some text to bytes.
|
||||
"""
|
||||
return b64decode(b.encode("ascii"))
|
||||
|
||||
|
||||
def stretch(seed: bytes, size: int) -> bytes:
|
||||
"""
|
||||
Given a simple description of a byte string, return the byte string
|
||||
itself.
|
||||
"""
|
||||
assert isinstance(seed, bytes)
|
||||
assert isinstance(size, int)
|
||||
assert size > 0
|
||||
assert len(seed) > 0
|
||||
|
||||
multiples = size // len(seed) + 1
|
||||
return (seed * multiples)[:size]
|
||||
|
||||
|
||||
def save_capabilities(results: list[tuple[Case, str]], path: FilePath = DATA_PATH) -> None:
|
||||
"""
|
||||
Save some test vector cases and their expected values.
|
||||
|
||||
This is logically the inverse of ``load_capabilities``.
|
||||
"""
|
||||
path.setContent(safe_dump({
|
||||
"version": CURRENT_VERSION,
|
||||
"vector": [
|
||||
{
|
||||
"convergence": encode_bytes(case.convergence),
|
||||
"format": {
|
||||
"kind": case.fmt.kind,
|
||||
"params": case.fmt.to_json(),
|
||||
},
|
||||
"sample": {
|
||||
"seed": encode_bytes(case.seed_data.seed),
|
||||
"length": case.seed_data.length,
|
||||
},
|
||||
"zfec": {
|
||||
"segmentSize": case.segment_size,
|
||||
"required": case.params.required,
|
||||
"total": case.params.total,
|
||||
},
|
||||
"expected": cap,
|
||||
}
|
||||
for (case, cap)
|
||||
in results
|
||||
],
|
||||
}).encode("ascii"))
|
||||
|
||||
|
||||
def load_format(serialized: dict) -> CHK | SSK:
|
||||
"""
|
||||
Load an encrypted object format from a simple description of it.
|
||||
|
||||
:param serialized: A ``dict`` describing either CHK or SSK, possibly with
|
||||
some parameters.
|
||||
"""
|
||||
if serialized["kind"] == "chk":
|
||||
return CHK.load(serialized["params"])
|
||||
elif serialized["kind"] == "ssk":
|
||||
return SSK.load(serialized["params"])
|
||||
else:
|
||||
raise ValueError(f"Unrecognized format: {serialized}")
|
||||
|
||||
|
||||
def load_capabilities(f: TextIO) -> dict[Case, str]:
|
||||
"""
|
||||
Load some test vector cases and their expected results from the given
|
||||
file.
|
||||
|
||||
This is logically the inverse of ``save_capabilities``.
|
||||
"""
|
||||
data = safe_load(f)
|
||||
if data is None:
|
||||
return {}
|
||||
if data["version"] != CURRENT_VERSION:
|
||||
print(
|
||||
f"Current version is {CURRENT_VERSION}; "
|
||||
f"cannot load version {data['version']} data."
|
||||
)
|
||||
return {}
|
||||
|
||||
return {
|
||||
Case(
|
||||
seed_params=SeedParam(case["zfec"]["required"], case["zfec"]["total"]),
|
||||
segment_size=case["zfec"]["segmentSize"],
|
||||
convergence=decode_bytes(case["convergence"]),
|
||||
seed_data=Sample(decode_bytes(case["sample"]["seed"]), case["sample"]["length"]),
|
||||
fmt=load_format(case["format"]),
|
||||
): case["expected"]
|
||||
for case
|
||||
in data["vector"]
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
with DATA_PATH.open() as f:
|
||||
capabilities: dict[Case, str] = load_capabilities(f)
|
||||
except FileNotFoundError:
|
||||
capabilities = {}
|
@ -1,6 +1,5 @@
|
||||
# -*- python -*-
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
"""Monitor a Tahoe grid, by playing sounds in response to remote events.
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
# This helper script is used with the 'test-desert-island' Makefile target.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
# This script generates a table of dependencies in HTML format on stdout.
|
||||
# It expects to be run in the tahoe-lafs-dep-eggs directory.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import re, os, sys
|
||||
import pkg_resources
|
||||
|
@ -1,4 +1,3 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import sys, os, io, re
|
||||
from twisted.internet import reactor, protocol, task, defer
|
||||
@ -26,10 +25,10 @@ python run-deprecations.py [--warnings=STDERRFILE] [--package=PYTHONPACKAGE ] CO
|
||||
class RunPP(protocol.ProcessProtocol):
|
||||
def outReceived(self, data):
|
||||
self.stdout.write(data)
|
||||
sys.stdout.write(data)
|
||||
sys.stdout.write(str(data, sys.stdout.encoding))
|
||||
def errReceived(self, data):
|
||||
self.stderr.write(data)
|
||||
sys.stderr.write(data)
|
||||
sys.stderr.write(str(data, sys.stdout.encoding))
|
||||
def processEnded(self, reason):
|
||||
signal = reason.value.signal
|
||||
rc = reason.value.exitCode
|
||||
@ -100,17 +99,19 @@ def run_command(main):
|
||||
|
||||
pp.stdout.seek(0)
|
||||
for line in pp.stdout.readlines():
|
||||
line = str(line, sys.stdout.encoding)
|
||||
if match(line):
|
||||
add(line) # includes newline
|
||||
|
||||
pp.stderr.seek(0)
|
||||
for line in pp.stderr.readlines():
|
||||
line = str(line, sys.stdout.encoding)
|
||||
if match(line):
|
||||
add(line)
|
||||
|
||||
if warnings:
|
||||
if config["warnings"]:
|
||||
with open(config["warnings"], "wb") as f:
|
||||
with open(config["warnings"], "w") as f:
|
||||
print("".join(warnings), file=f)
|
||||
print("ERROR: %d deprecation warnings found" % len(warnings))
|
||||
sys.exit(1)
|
||||
|
@ -1,8 +1,7 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import locale, os, platform, subprocess, sys, traceback
|
||||
from importlib.metadata import version, PackageNotFoundError
|
||||
|
||||
|
||||
def foldlines(s, numlines=None):
|
||||
@ -72,17 +71,10 @@ def print_as_ver():
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
def print_setuptools_ver():
|
||||
try:
|
||||
import pkg_resources
|
||||
out = str(pkg_resources.require("setuptools"))
|
||||
print("setuptools:", foldlines(out))
|
||||
except (ImportError, EnvironmentError):
|
||||
sys.stderr.write("\nGot exception using 'pkg_resources' to get the version of setuptools. Exception follows\n")
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
except pkg_resources.DistributionNotFound:
|
||||
print("setuptools:", version("setuptools"))
|
||||
except PackageNotFoundError:
|
||||
print('setuptools: DistributionNotFound')
|
||||
|
||||
|
||||
@ -91,14 +83,8 @@ def print_py_pkg_ver(pkgname, modulename=None):
|
||||
modulename = pkgname
|
||||
print()
|
||||
try:
|
||||
import pkg_resources
|
||||
out = str(pkg_resources.require(pkgname))
|
||||
print(pkgname + ': ' + foldlines(out))
|
||||
except (ImportError, EnvironmentError):
|
||||
sys.stderr.write("\nGot exception using 'pkg_resources' to get the version of %s. Exception follows.\n" % (pkgname,))
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
except pkg_resources.DistributionNotFound:
|
||||
print(pkgname + ': ' + version(pkgname))
|
||||
except PackageNotFoundError:
|
||||
print(pkgname + ': DistributionNotFound')
|
||||
try:
|
||||
__import__(modulename)
|
||||
|
@ -29,7 +29,6 @@
|
||||
# characteristic: 14.1.0 (/Applications/tahoe.app/support/lib/python2.7/site-packages)
|
||||
# pyasn1-modules: 0.0.5 (/Applications/tahoe.app/support/lib/python2.7/site-packages/pyasn1_modules-0.0.5-py2.7.egg)
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os, re, shutil, subprocess, sys, tempfile
|
||||
|
||||
|
95
misc/build_helpers/update-version.py
Normal file
95
misc/build_helpers/update-version.py
Normal file
@ -0,0 +1,95 @@
|
||||
#
|
||||
# this updates the (tagged) version of the software
|
||||
#
|
||||
# Any "options" are hard-coded in here (e.g. the GnuPG key to use)
|
||||
#
|
||||
|
||||
author = "meejah <meejah@meejah.ca>"
|
||||
|
||||
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from packaging.version import Version
|
||||
|
||||
from dulwich.repo import Repo
|
||||
from dulwich.porcelain import (
|
||||
tag_list,
|
||||
tag_create,
|
||||
status,
|
||||
)
|
||||
|
||||
from twisted.internet.task import (
|
||||
react,
|
||||
)
|
||||
from twisted.internet.defer import (
|
||||
ensureDeferred,
|
||||
)
|
||||
|
||||
|
||||
def existing_tags(git):
|
||||
versions = sorted(
|
||||
Version(v.decode("utf8").lstrip("tahoe-lafs-"))
|
||||
for v in tag_list(git)
|
||||
if v.startswith(b"tahoe-lafs-")
|
||||
)
|
||||
return versions
|
||||
|
||||
|
||||
def create_new_version(git):
|
||||
versions = existing_tags(git)
|
||||
biggest = versions[-1]
|
||||
|
||||
return Version(
|
||||
"{}.{}.{}".format(
|
||||
biggest.major,
|
||||
biggest.minor + 1,
|
||||
0,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def main(reactor):
|
||||
git = Repo(".")
|
||||
|
||||
st = status(git)
|
||||
if any(st.staged.values()) or st.unstaged:
|
||||
print("unclean checkout; aborting")
|
||||
raise SystemExit(1)
|
||||
|
||||
v = create_new_version(git)
|
||||
if "--no-tag" in sys.argv:
|
||||
print(v)
|
||||
return
|
||||
|
||||
print("Existing tags: {}".format("\n".join(str(x) for x in existing_tags(git))))
|
||||
print("New tag will be {}".format(v))
|
||||
|
||||
# the "tag time" is seconds from the epoch .. we quantize these to
|
||||
# the start of the day in question, in UTC.
|
||||
now = datetime.now()
|
||||
s = now.utctimetuple()
|
||||
ts = int(
|
||||
time.mktime(
|
||||
time.struct_time((s.tm_year, s.tm_mon, s.tm_mday, 0, 0, 0, 0, s.tm_yday, 0))
|
||||
)
|
||||
)
|
||||
tag_create(
|
||||
repo=git,
|
||||
tag="tahoe-lafs-{}".format(str(v)).encode("utf8"),
|
||||
author=author.encode("utf8"),
|
||||
message="Release {}".format(v).encode("utf8"),
|
||||
annotated=True,
|
||||
objectish=b"HEAD",
|
||||
sign=author.encode("utf8"),
|
||||
tag_time=ts,
|
||||
tag_timezone=0,
|
||||
)
|
||||
|
||||
print("Tag created locally, it is not pushed")
|
||||
print("To push it run something like:")
|
||||
print(" git push origin {}".format(v))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
react(lambda r: ensureDeferred(main(r)))
|
@ -1,4 +1,3 @@
|
||||
from __future__ import print_function
|
||||
|
||||
"""
|
||||
Test an existing Tahoe grid, both to see if the grid is still running and to
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import print_function
|
||||
|
||||
"""
|
||||
this is a load-generating client program. It does all of its work through a
|
||||
given tahoe node (specified by URL), and performs random reads and writes
|
||||
@ -33,20 +31,11 @@ a mean of 10kB and a max of 100MB, so filesize=min(int(1.0/random(.0002)),1e8)
|
||||
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os, sys, httplib, binascii
|
||||
import urllib, json, random, time, urlparse
|
||||
|
||||
try:
|
||||
from typing import Dict
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Python 2 compatibility
|
||||
from future.utils import PY2
|
||||
if PY2:
|
||||
from future.builtins import str # noqa: F401
|
||||
|
||||
if sys.argv[1] == "--stats":
|
||||
statsfiles = sys.argv[2:]
|
||||
# gather stats every 10 seconds, do a moving-window average of the last
|
||||
@ -54,9 +43,9 @@ if sys.argv[1] == "--stats":
|
||||
DELAY = 10
|
||||
MAXSAMPLES = 6
|
||||
totals = []
|
||||
last_stats = {} # type: Dict[str, float]
|
||||
last_stats : dict[str, float] = {}
|
||||
while True:
|
||||
stats = {} # type: Dict[str, float]
|
||||
stats : dict[str, float] = {}
|
||||
for sf in statsfiles:
|
||||
for line in open(sf, "r").readlines():
|
||||
name, str_value = line.split(":")
|
||||
|
@ -1,522 +0,0 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import os, shutil, sys, urllib, time, stat, urlparse
|
||||
|
||||
# Python 2 compatibility
|
||||
from future.utils import PY2
|
||||
if PY2:
|
||||
from future.builtins import str # noqa: F401
|
||||
from six.moves import cStringIO as StringIO
|
||||
|
||||
from twisted.python.filepath import (
|
||||
FilePath,
|
||||
)
|
||||
from twisted.internet import defer, reactor, protocol, error
|
||||
from twisted.application import service, internet
|
||||
from twisted.web import client as tw_client
|
||||
from twisted.python import log, procutils
|
||||
from foolscap.api import Tub, fireEventually, flushEventualQueue
|
||||
|
||||
from allmydata import client, introducer
|
||||
from allmydata.immutable import upload
|
||||
from allmydata.scripts import create_node
|
||||
from allmydata.util import fileutil, pollmixin
|
||||
from allmydata.util.fileutil import abspath_expanduser_unicode
|
||||
from allmydata.util.encodingutil import get_filesystem_encoding
|
||||
|
||||
from allmydata.scripts.common import (
|
||||
write_introducer,
|
||||
)
|
||||
|
||||
class StallableHTTPGetterDiscarder(tw_client.HTTPPageGetter, object):
|
||||
full_speed_ahead = False
|
||||
_bytes_so_far = 0
|
||||
stalled = None
|
||||
def handleResponsePart(self, data):
|
||||
self._bytes_so_far += len(data)
|
||||
if not self.factory.do_stall:
|
||||
return
|
||||
if self.full_speed_ahead:
|
||||
return
|
||||
if self._bytes_so_far > 1e6+100:
|
||||
if not self.stalled:
|
||||
print("STALLING")
|
||||
self.transport.pauseProducing()
|
||||
self.stalled = reactor.callLater(10.0, self._resume_speed)
|
||||
def _resume_speed(self):
|
||||
print("RESUME SPEED")
|
||||
self.stalled = None
|
||||
self.full_speed_ahead = True
|
||||
self.transport.resumeProducing()
|
||||
def handleResponseEnd(self):
|
||||
if self.stalled:
|
||||
print("CANCEL")
|
||||
self.stalled.cancel()
|
||||
self.stalled = None
|
||||
return tw_client.HTTPPageGetter.handleResponseEnd(self)
|
||||
|
||||
class StallableDiscardingHTTPClientFactory(tw_client.HTTPClientFactory, object):
|
||||
protocol = StallableHTTPGetterDiscarder
|
||||
|
||||
def discardPage(url, stall=False, *args, **kwargs):
|
||||
"""Start fetching the URL, but stall our pipe after the first 1MB.
|
||||
Wait 10 seconds, then resume downloading (and discarding) everything.
|
||||
"""
|
||||
# adapted from twisted.web.client.getPage . We can't just wrap or
|
||||
# subclass because it provides no way to override the HTTPClientFactory
|
||||
# that it creates.
|
||||
scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
|
||||
assert scheme == 'http'
|
||||
host, port = netloc, 80
|
||||
if ":" in host:
|
||||
host, port = host.split(":")
|
||||
port = int(port)
|
||||
factory = StallableDiscardingHTTPClientFactory(url, *args, **kwargs)
|
||||
factory.do_stall = stall
|
||||
reactor.connectTCP(host, port, factory)
|
||||
return factory.deferred
|
||||
|
||||
class ChildDidNotStartError(Exception):
|
||||
pass
|
||||
|
||||
class SystemFramework(pollmixin.PollMixin):
|
||||
numnodes = 7
|
||||
|
||||
def __init__(self, basedir, mode):
|
||||
self.basedir = basedir = abspath_expanduser_unicode(str(basedir))
|
||||
if not (basedir + os.path.sep).startswith(abspath_expanduser_unicode(u".") + os.path.sep):
|
||||
raise AssertionError("safety issue: basedir must be a subdir")
|
||||
self.testdir = testdir = os.path.join(basedir, "test")
|
||||
if os.path.exists(testdir):
|
||||
shutil.rmtree(testdir)
|
||||
fileutil.make_dirs(testdir)
|
||||
self.sparent = service.MultiService()
|
||||
self.sparent.startService()
|
||||
self.proc = None
|
||||
self.tub = Tub()
|
||||
self.tub.setOption("expose-remote-exception-types", False)
|
||||
self.tub.setServiceParent(self.sparent)
|
||||
self.mode = mode
|
||||
self.failed = False
|
||||
self.keepalive_file = None
|
||||
|
||||
def run(self):
|
||||
framelog = os.path.join(self.basedir, "driver.log")
|
||||
log.startLogging(open(framelog, "a"), setStdout=False)
|
||||
log.msg("CHECK_MEMORY(mode=%s) STARTING" % self.mode)
|
||||
#logfile = open(os.path.join(self.testdir, "log"), "w")
|
||||
#flo = log.FileLogObserver(logfile)
|
||||
#log.startLoggingWithObserver(flo.emit, setStdout=False)
|
||||
d = fireEventually()
|
||||
d.addCallback(lambda res: self.setUp())
|
||||
d.addCallback(lambda res: self.record_initial_memusage())
|
||||
d.addCallback(lambda res: self.make_nodes())
|
||||
d.addCallback(lambda res: self.wait_for_client_connected())
|
||||
d.addCallback(lambda res: self.do_test())
|
||||
d.addBoth(self.tearDown)
|
||||
def _err(err):
|
||||
self.failed = err
|
||||
log.err(err)
|
||||
print(err)
|
||||
d.addErrback(_err)
|
||||
def _done(res):
|
||||
reactor.stop()
|
||||
return res
|
||||
d.addBoth(_done)
|
||||
reactor.run()
|
||||
if self.failed:
|
||||
# raiseException doesn't work for CopiedFailures
|
||||
self.failed.raiseException()
|
||||
|
||||
def setUp(self):
|
||||
#print("STARTING")
|
||||
self.stats = {}
|
||||
self.statsfile = open(os.path.join(self.basedir, "stats.out"), "a")
|
||||
self.make_introducer()
|
||||
d = self.start_client()
|
||||
def _record_control_furl(control_furl):
|
||||
self.control_furl = control_furl
|
||||
#print("OBTAINING '%s'" % (control_furl,))
|
||||
return self.tub.getReference(self.control_furl)
|
||||
d.addCallback(_record_control_furl)
|
||||
def _record_control(control_rref):
|
||||
self.control_rref = control_rref
|
||||
d.addCallback(_record_control)
|
||||
def _ready(res):
|
||||
#print("CLIENT READY")
|
||||
pass
|
||||
d.addCallback(_ready)
|
||||
return d
|
||||
|
||||
def record_initial_memusage(self):
|
||||
print()
|
||||
print("Client started (no connections yet)")
|
||||
d = self._print_usage()
|
||||
d.addCallback(self.stash_stats, "init")
|
||||
return d
|
||||
|
||||
def wait_for_client_connected(self):
|
||||
print()
|
||||
print("Client connecting to other nodes..")
|
||||
return self.control_rref.callRemote("wait_for_client_connections",
|
||||
self.numnodes+1)
|
||||
|
||||
def tearDown(self, passthrough):
|
||||
# the client node will shut down in a few seconds
|
||||
#os.remove(os.path.join(self.clientdir, client.Client.EXIT_TRIGGER_FILE))
|
||||
log.msg("shutting down SystemTest services")
|
||||
if self.keepalive_file and os.path.exists(self.keepalive_file):
|
||||
age = time.time() - os.stat(self.keepalive_file)[stat.ST_MTIME]
|
||||
log.msg("keepalive file at shutdown was %ds old" % age)
|
||||
d = defer.succeed(None)
|
||||
if self.proc:
|
||||
d.addCallback(lambda res: self.kill_client())
|
||||
d.addCallback(lambda res: self.sparent.stopService())
|
||||
d.addCallback(lambda res: flushEventualQueue())
|
||||
def _close_statsfile(res):
|
||||
self.statsfile.close()
|
||||
d.addCallback(_close_statsfile)
|
||||
d.addCallback(lambda res: passthrough)
|
||||
return d
|
||||
|
||||
def make_introducer(self):
|
||||
iv_basedir = os.path.join(self.testdir, "introducer")
|
||||
os.mkdir(iv_basedir)
|
||||
self.introducer = introducer.IntroducerNode(basedir=iv_basedir)
|
||||
self.introducer.setServiceParent(self)
|
||||
self.introducer_furl = self.introducer.introducer_url
|
||||
|
||||
def make_nodes(self):
|
||||
root = FilePath(self.testdir)
|
||||
self.nodes = []
|
||||
for i in range(self.numnodes):
|
||||
nodedir = root.child("node%d" % (i,))
|
||||
private = nodedir.child("private")
|
||||
private.makedirs()
|
||||
write_introducer(nodedir, "default", self.introducer_url)
|
||||
config = (
|
||||
"[client]\n"
|
||||
"shares.happy = 1\n"
|
||||
"[storage]\n"
|
||||
)
|
||||
# the only tests for which we want the internal nodes to actually
|
||||
# retain shares are the ones where somebody's going to download
|
||||
# them.
|
||||
if self.mode in ("download", "download-GET", "download-GET-slow"):
|
||||
# retain shares
|
||||
pass
|
||||
else:
|
||||
# for these tests, we tell the storage servers to pretend to
|
||||
# accept shares, but really just throw them out, since we're
|
||||
# only testing upload and not download.
|
||||
config += "debug_discard = true\n"
|
||||
if self.mode in ("receive",):
|
||||
# for this mode, the client-under-test gets all the shares,
|
||||
# so our internal nodes can refuse requests
|
||||
config += "readonly = true\n"
|
||||
nodedir.child("tahoe.cfg").setContent(config)
|
||||
c = client.Client(basedir=nodedir.path)
|
||||
c.setServiceParent(self)
|
||||
self.nodes.append(c)
|
||||
# the peers will start running, eventually they will connect to each
|
||||
# other and the introducer
|
||||
|
||||
def touch_keepalive(self):
|
||||
if os.path.exists(self.keepalive_file):
|
||||
age = time.time() - os.stat(self.keepalive_file)[stat.ST_MTIME]
|
||||
log.msg("touching keepalive file, was %ds old" % age)
|
||||
f = open(self.keepalive_file, "w")
|
||||
f.write("""\
|
||||
If the node notices this file at startup, it will poll every 5 seconds and
|
||||
terminate if the file is more than 10 seconds old, or if it has been deleted.
|
||||
If the test harness has an internal failure and neglects to kill off the node
|
||||
itself, this helps to avoid leaving processes lying around. The contents of
|
||||
this file are ignored.
|
||||
""")
|
||||
f.close()
|
||||
|
||||
def start_client(self):
|
||||
# this returns a Deferred that fires with the client's control.furl
|
||||
log.msg("MAKING CLIENT")
|
||||
# self.testdir is an absolute Unicode path
|
||||
clientdir = self.clientdir = os.path.join(self.testdir, u"client")
|
||||
clientdir_str = clientdir.encode(get_filesystem_encoding())
|
||||
quiet = StringIO()
|
||||
create_node.create_node({'basedir': clientdir}, out=quiet)
|
||||
log.msg("DONE MAKING CLIENT")
|
||||
write_introducer(clientdir, "default", self.introducer_furl)
|
||||
# now replace tahoe.cfg
|
||||
# set webport=0 and then ask the node what port it picked.
|
||||
f = open(os.path.join(clientdir, "tahoe.cfg"), "w")
|
||||
f.write("[node]\n"
|
||||
"web.port = tcp:0:interface=127.0.0.1\n"
|
||||
"[client]\n"
|
||||
"shares.happy = 1\n"
|
||||
"[storage]\n"
|
||||
)
|
||||
|
||||
if self.mode in ("upload-self", "receive"):
|
||||
# accept and store shares, to trigger the memory consumption bugs
|
||||
pass
|
||||
else:
|
||||
# don't accept any shares
|
||||
f.write("readonly = true\n")
|
||||
## also, if we do receive any shares, throw them away
|
||||
#f.write("debug_discard = true")
|
||||
if self.mode == "upload-self":
|
||||
pass
|
||||
f.close()
|
||||
self.keepalive_file = os.path.join(clientdir,
|
||||
client.Client.EXIT_TRIGGER_FILE)
|
||||
# now start updating the mtime.
|
||||
self.touch_keepalive()
|
||||
ts = internet.TimerService(1.0, self.touch_keepalive)
|
||||
ts.setServiceParent(self.sparent)
|
||||
|
||||
pp = ClientWatcher()
|
||||
self.proc_done = pp.d = defer.Deferred()
|
||||
logfile = os.path.join(self.basedir, "client.log")
|
||||
tahoes = procutils.which("tahoe")
|
||||
if not tahoes:
|
||||
raise RuntimeError("unable to find a 'tahoe' executable")
|
||||
cmd = [tahoes[0], "run", ".", "-l", logfile]
|
||||
env = os.environ.copy()
|
||||
self.proc = reactor.spawnProcess(pp, cmd[0], cmd, env, path=clientdir_str)
|
||||
log.msg("CLIENT STARTED")
|
||||
|
||||
# now we wait for the client to get started. we're looking for the
|
||||
# control.furl file to appear.
|
||||
furl_file = os.path.join(clientdir, "private", "control.furl")
|
||||
url_file = os.path.join(clientdir, "node.url")
|
||||
def _check():
|
||||
if pp.ended and pp.ended.value.status != 0:
|
||||
# the twistd process ends normally (with rc=0) if the child
|
||||
# is successfully launched. It ends abnormally (with rc!=0)
|
||||
# if the child cannot be launched.
|
||||
raise ChildDidNotStartError("process ended while waiting for startup")
|
||||
return os.path.exists(furl_file)
|
||||
d = self.poll(_check, 0.1)
|
||||
# once it exists, wait a moment before we read from it, just in case
|
||||
# it hasn't finished writing the whole thing. Ideally control.furl
|
||||
# would be created in some atomic fashion, or made non-readable until
|
||||
# it's ready, but I can't think of an easy way to do that, and I
|
||||
# think the chances that we'll observe a half-write are pretty low.
|
||||
def _stall(res):
|
||||
d2 = defer.Deferred()
|
||||
reactor.callLater(0.1, d2.callback, None)
|
||||
return d2
|
||||
d.addCallback(_stall)
|
||||
def _read(res):
|
||||
# read the node's URL
|
||||
self.webish_url = open(url_file, "r").read().strip()
|
||||
if self.webish_url[-1] == "/":
|
||||
# trim trailing slash, since the rest of the code wants it gone
|
||||
self.webish_url = self.webish_url[:-1]
|
||||
f = open(furl_file, "r")
|
||||
furl = f.read()
|
||||
return furl.strip()
|
||||
d.addCallback(_read)
|
||||
return d
|
||||
|
||||
|
||||
def kill_client(self):
|
||||
# returns a Deferred that fires when the process exits. This may only
|
||||
# be called once.
|
||||
try:
|
||||
self.proc.signalProcess("INT")
|
||||
except error.ProcessExitedAlready:
|
||||
pass
|
||||
return self.proc_done
|
||||
|
||||
|
||||
def create_data(self, name, size):
|
||||
filename = os.path.join(self.testdir, name + ".data")
|
||||
f = open(filename, "wb")
|
||||
block = "a" * 8192
|
||||
while size > 0:
|
||||
l = min(size, 8192)
|
||||
f.write(block[:l])
|
||||
size -= l
|
||||
return filename
|
||||
|
||||
def stash_stats(self, stats, name):
|
||||
self.statsfile.write("%s %s: %d\n" % (self.mode, name, stats['VmPeak']))
|
||||
self.statsfile.flush()
|
||||
self.stats[name] = stats['VmPeak']
|
||||
|
||||
def POST(self, urlpath, **fields):
|
||||
url = self.webish_url + urlpath
|
||||
sepbase = "boogabooga"
|
||||
sep = "--" + sepbase
|
||||
form = []
|
||||
form.append(sep)
|
||||
form.append('Content-Disposition: form-data; name="_charset"')
|
||||
form.append('')
|
||||
form.append('UTF-8')
|
||||
form.append(sep)
|
||||
for name, value in fields.iteritems():
|
||||
if isinstance(value, tuple):
|
||||
filename, value = value
|
||||
form.append('Content-Disposition: form-data; name="%s"; '
|
||||
'filename="%s"' % (name, filename))
|
||||
else:
|
||||
form.append('Content-Disposition: form-data; name="%s"' % name)
|
||||
form.append('')
|
||||
form.append(value)
|
||||
form.append(sep)
|
||||
form[-1] += "--"
|
||||
body = "\r\n".join(form) + "\r\n"
|
||||
headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
|
||||
}
|
||||
return tw_client.getPage(url, method="POST", postdata=body,
|
||||
headers=headers, followRedirect=False)
|
||||
|
||||
def GET_discard(self, urlpath, stall):
|
||||
url = self.webish_url + urlpath + "?filename=dummy-get.out"
|
||||
return discardPage(url, stall)
|
||||
|
||||
def _print_usage(self, res=None):
|
||||
d = self.control_rref.callRemote("get_memory_usage")
|
||||
def _print(stats):
|
||||
print("VmSize: %9d VmPeak: %9d" % (stats["VmSize"],
|
||||
stats["VmPeak"]))
|
||||
return stats
|
||||
d.addCallback(_print)
|
||||
return d
|
||||
|
||||
def _do_upload(self, res, size, files, uris):
|
||||
name = '%d' % size
|
||||
print()
|
||||
print("uploading %s" % name)
|
||||
if self.mode in ("upload", "upload-self"):
|
||||
d = self.control_rref.callRemote("upload_random_data_from_file",
|
||||
size,
|
||||
convergence="check-memory")
|
||||
elif self.mode == "upload-POST":
|
||||
data = "a" * size
|
||||
url = "/uri"
|
||||
d = self.POST(url, t="upload", file=("%d.data" % size, data))
|
||||
elif self.mode in ("receive",
|
||||
"download", "download-GET", "download-GET-slow"):
|
||||
# mode=receive: upload the data from a local peer, so that the
|
||||
# client-under-test receives and stores the shares
|
||||
#
|
||||
# mode=download*: upload the data from a local peer, then have
|
||||
# the client-under-test download it.
|
||||
#
|
||||
# we need to wait until the uploading node has connected to all
|
||||
# peers, since the wait_for_client_connections() above doesn't
|
||||
# pay attention to our self.nodes[] and their connections.
|
||||
files[name] = self.create_data(name, size)
|
||||
u = self.nodes[0].getServiceNamed("uploader")
|
||||
d = self.nodes[0].debug_wait_for_client_connections(self.numnodes+1)
|
||||
d.addCallback(lambda res:
|
||||
u.upload(upload.FileName(files[name],
|
||||
convergence="check-memory")))
|
||||
d.addCallback(lambda results: results.get_uri())
|
||||
else:
|
||||
raise ValueError("unknown mode=%s" % self.mode)
|
||||
def _complete(uri):
|
||||
uris[name] = uri
|
||||
print("uploaded %s" % name)
|
||||
d.addCallback(_complete)
|
||||
return d
|
||||
|
||||
def _do_download(self, res, size, uris):
|
||||
if self.mode not in ("download", "download-GET", "download-GET-slow"):
|
||||
return
|
||||
name = '%d' % size
|
||||
print("downloading %s" % name)
|
||||
uri = uris[name]
|
||||
|
||||
if self.mode == "download":
|
||||
d = self.control_rref.callRemote("download_to_tempfile_and_delete",
|
||||
uri)
|
||||
elif self.mode == "download-GET":
|
||||
url = "/uri/%s" % uri
|
||||
d = self.GET_discard(urllib.quote(url), stall=False)
|
||||
elif self.mode == "download-GET-slow":
|
||||
url = "/uri/%s" % uri
|
||||
d = self.GET_discard(urllib.quote(url), stall=True)
|
||||
|
||||
def _complete(res):
|
||||
print("downloaded %s" % name)
|
||||
return res
|
||||
d.addCallback(_complete)
|
||||
return d
|
||||
|
||||
def do_test(self):
|
||||
#print("CLIENT STARTED")
|
||||
#print("FURL", self.control_furl)
|
||||
#print("RREF", self.control_rref)
|
||||
#print()
|
||||
kB = 1000; MB = 1000*1000
|
||||
files = {}
|
||||
uris = {}
|
||||
|
||||
d = self._print_usage()
|
||||
d.addCallback(self.stash_stats, "0B")
|
||||
|
||||
for i in range(10):
|
||||
d.addCallback(self._do_upload, 10*kB+i, files, uris)
|
||||
d.addCallback(self._do_download, 10*kB+i, uris)
|
||||
d.addCallback(self._print_usage)
|
||||
d.addCallback(self.stash_stats, "10kB")
|
||||
|
||||
for i in range(3):
|
||||
d.addCallback(self._do_upload, 10*MB+i, files, uris)
|
||||
d.addCallback(self._do_download, 10*MB+i, uris)
|
||||
d.addCallback(self._print_usage)
|
||||
d.addCallback(self.stash_stats, "10MB")
|
||||
|
||||
for i in range(1):
|
||||
d.addCallback(self._do_upload, 50*MB+i, files, uris)
|
||||
d.addCallback(self._do_download, 50*MB+i, uris)
|
||||
d.addCallback(self._print_usage)
|
||||
d.addCallback(self.stash_stats, "50MB")
|
||||
|
||||
#for i in range(1):
|
||||
# d.addCallback(self._do_upload, 100*MB+i, files, uris)
|
||||
# d.addCallback(self._do_download, 100*MB+i, uris)
|
||||
# d.addCallback(self._print_usage)
|
||||
#d.addCallback(self.stash_stats, "100MB")
|
||||
|
||||
#d.addCallback(self.stall)
|
||||
def _done(res):
|
||||
print("FINISHING")
|
||||
d.addCallback(_done)
|
||||
return d
|
||||
|
||||
def stall(self, res):
|
||||
d = defer.Deferred()
|
||||
reactor.callLater(5, d.callback, None)
|
||||
return d
|
||||
|
||||
|
||||
class ClientWatcher(protocol.ProcessProtocol, object):
|
||||
ended = False
|
||||
def outReceived(self, data):
|
||||
print("OUT:", data)
|
||||
def errReceived(self, data):
|
||||
print("ERR:", data)
|
||||
def processEnded(self, reason):
|
||||
self.ended = reason
|
||||
self.d.callback(None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
mode = "upload"
|
||||
if len(sys.argv) > 1:
|
||||
mode = sys.argv[1]
|
||||
if sys.maxsize == 2147483647:
|
||||
bits = "32"
|
||||
elif sys.maxsize == 9223372036854775807:
|
||||
bits = "64"
|
||||
else:
|
||||
bits = "?"
|
||||
print("%s-bit system (sys.maxsize=%d)" % (bits, sys.maxsize))
|
||||
# put the logfile and stats.out in _test_memory/ . These stick around.
|
||||
# put the nodes and other files in _test_memory/test/ . These are
|
||||
# removed each time we run.
|
||||
sf = SystemFramework("_test_memory", mode)
|
||||
sf.run()
|
@ -1,234 +0,0 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import os, sys
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.python import log
|
||||
from twisted.application import service
|
||||
from foolscap.api import Tub, fireEventually
|
||||
|
||||
MB = 1000000
|
||||
|
||||
class SpeedTest(object):
|
||||
DO_IMMUTABLE = True
|
||||
DO_MUTABLE_CREATE = True
|
||||
DO_MUTABLE = True
|
||||
|
||||
def __init__(self, test_client_dir):
|
||||
#self.real_stderr = sys.stderr
|
||||
log.startLogging(open("st.log", "a"), setStdout=False)
|
||||
f = open(os.path.join(test_client_dir, "private", "control.furl"), "r")
|
||||
self.control_furl = f.read().strip()
|
||||
f.close()
|
||||
self.base_service = service.MultiService()
|
||||
self.failed = None
|
||||
self.upload_times = {}
|
||||
self.download_times = {}
|
||||
|
||||
def run(self):
|
||||
print("STARTING")
|
||||
d = fireEventually()
|
||||
d.addCallback(lambda res: self.setUp())
|
||||
d.addCallback(lambda res: self.do_test())
|
||||
d.addBoth(self.tearDown)
|
||||
def _err(err):
|
||||
self.failed = err
|
||||
log.err(err)
|
||||
print(err)
|
||||
d.addErrback(_err)
|
||||
def _done(res):
|
||||
reactor.stop()
|
||||
return res
|
||||
d.addBoth(_done)
|
||||
reactor.run()
|
||||
if self.failed:
|
||||
print("EXCEPTION")
|
||||
print(self.failed)
|
||||
sys.exit(1)
|
||||
|
||||
def setUp(self):
|
||||
self.base_service.startService()
|
||||
self.tub = Tub()
|
||||
self.tub.setOption("expose-remote-exception-types", False)
|
||||
self.tub.setServiceParent(self.base_service)
|
||||
d = self.tub.getReference(self.control_furl)
|
||||
def _gotref(rref):
|
||||
self.client_rref = rref
|
||||
print("Got Client Control reference")
|
||||
return self.stall(5)
|
||||
d.addCallback(_gotref)
|
||||
return d
|
||||
|
||||
def stall(self, delay, result=None):
|
||||
d = defer.Deferred()
|
||||
reactor.callLater(delay, d.callback, result)
|
||||
return d
|
||||
|
||||
def record_times(self, times, key):
|
||||
print("TIME (%s): %s up, %s down" % (key, times[0], times[1]))
|
||||
self.upload_times[key], self.download_times[key] = times
|
||||
|
||||
def one_test(self, res, name, count, size, mutable):
|
||||
# values for 'mutable':
|
||||
# False (upload a different CHK file for each 'count')
|
||||
# "create" (upload different contents into a new SSK file)
|
||||
# "upload" (upload different contents into the same SSK file. The
|
||||
# time consumed does not include the creation of the file)
|
||||
d = self.client_rref.callRemote("speed_test", count, size, mutable)
|
||||
d.addCallback(self.record_times, name)
|
||||
return d
|
||||
|
||||
def measure_rtt(self, res):
|
||||
# use RIClient.get_nodeid() to measure the foolscap-level RTT
|
||||
d = self.client_rref.callRemote("measure_peer_response_time")
|
||||
def _got(res):
|
||||
assert len(res) # need at least one peer
|
||||
times = res.values()
|
||||
self.total_rtt = sum(times)
|
||||
self.average_rtt = sum(times) / len(times)
|
||||
self.max_rtt = max(times)
|
||||
print("num-peers: %d" % len(times))
|
||||
print("total-RTT: %f" % self.total_rtt)
|
||||
print("average-RTT: %f" % self.average_rtt)
|
||||
print("max-RTT: %f" % self.max_rtt)
|
||||
d.addCallback(_got)
|
||||
return d
|
||||
|
||||
def do_test(self):
|
||||
print("doing test")
|
||||
d = defer.succeed(None)
|
||||
d.addCallback(self.one_test, "startup", 1, 1000, False) #ignore this one
|
||||
d.addCallback(self.measure_rtt)
|
||||
|
||||
if self.DO_IMMUTABLE:
|
||||
# immutable files
|
||||
d.addCallback(self.one_test, "1x 200B", 1, 200, False)
|
||||
d.addCallback(self.one_test, "10x 200B", 10, 200, False)
|
||||
def _maybe_do_100x_200B(res):
|
||||
if self.upload_times["10x 200B"] < 5:
|
||||
print("10x 200B test went too fast, doing 100x 200B test")
|
||||
return self.one_test(None, "100x 200B", 100, 200, False)
|
||||
return
|
||||
d.addCallback(_maybe_do_100x_200B)
|
||||
d.addCallback(self.one_test, "1MB", 1, 1*MB, False)
|
||||
d.addCallback(self.one_test, "10MB", 1, 10*MB, False)
|
||||
def _maybe_do_100MB(res):
|
||||
if self.upload_times["10MB"] > 30:
|
||||
print("10MB test took too long, skipping 100MB test")
|
||||
return
|
||||
return self.one_test(None, "100MB", 1, 100*MB, False)
|
||||
d.addCallback(_maybe_do_100MB)
|
||||
|
||||
if self.DO_MUTABLE_CREATE:
|
||||
# mutable file creation
|
||||
d.addCallback(self.one_test, "10x 200B SSK creation", 10, 200,
|
||||
"create")
|
||||
|
||||
if self.DO_MUTABLE:
|
||||
# mutable file upload/download
|
||||
d.addCallback(self.one_test, "10x 200B SSK", 10, 200, "upload")
|
||||
def _maybe_do_100x_200B_SSK(res):
|
||||
if self.upload_times["10x 200B SSK"] < 5:
|
||||
print("10x 200B SSK test went too fast, doing 100x 200B SSK")
|
||||
return self.one_test(None, "100x 200B SSK", 100, 200,
|
||||
"upload")
|
||||
return
|
||||
d.addCallback(_maybe_do_100x_200B_SSK)
|
||||
d.addCallback(self.one_test, "1MB SSK", 1, 1*MB, "upload")
|
||||
|
||||
d.addCallback(self.calculate_speeds)
|
||||
return d
|
||||
|
||||
def calculate_speeds(self, res):
|
||||
# time = A*size+B
|
||||
# we assume that A*200bytes is negligible
|
||||
|
||||
if self.DO_IMMUTABLE:
|
||||
# upload
|
||||
if "100x 200B" in self.upload_times:
|
||||
B = self.upload_times["100x 200B"] / 100
|
||||
else:
|
||||
B = self.upload_times["10x 200B"] / 10
|
||||
print("upload per-file time: %.3fs" % B)
|
||||
print("upload per-file times-avg-RTT: %f" % (B / self.average_rtt))
|
||||
print("upload per-file times-total-RTT: %f" % (B / self.total_rtt))
|
||||
A1 = 1*MB / (self.upload_times["1MB"] - B) # in bytes per second
|
||||
print("upload speed (1MB):", self.number(A1, "Bps"))
|
||||
A2 = 10*MB / (self.upload_times["10MB"] - B)
|
||||
print("upload speed (10MB):", self.number(A2, "Bps"))
|
||||
if "100MB" in self.upload_times:
|
||||
A3 = 100*MB / (self.upload_times["100MB"] - B)
|
||||
print("upload speed (100MB):", self.number(A3, "Bps"))
|
||||
|
||||
# download
|
||||
if "100x 200B" in self.download_times:
|
||||
B = self.download_times["100x 200B"] / 100
|
||||
else:
|
||||
B = self.download_times["10x 200B"] / 10
|
||||
print("download per-file time: %.3fs" % B)
|
||||
print("download per-file times-avg-RTT: %f" % (B / self.average_rtt))
|
||||
print("download per-file times-total-RTT: %f" % (B / self.total_rtt))
|
||||
A1 = 1*MB / (self.download_times["1MB"] - B) # in bytes per second
|
||||
print("download speed (1MB):", self.number(A1, "Bps"))
|
||||
A2 = 10*MB / (self.download_times["10MB"] - B)
|
||||
print("download speed (10MB):", self.number(A2, "Bps"))
|
||||
if "100MB" in self.download_times:
|
||||
A3 = 100*MB / (self.download_times["100MB"] - B)
|
||||
print("download speed (100MB):", self.number(A3, "Bps"))
|
||||
|
||||
if self.DO_MUTABLE_CREATE:
|
||||
# SSK creation
|
||||
B = self.upload_times["10x 200B SSK creation"] / 10
|
||||
print("create per-file time SSK: %.3fs" % B)
|
||||
|
||||
if self.DO_MUTABLE:
|
||||
# upload SSK
|
||||
if "100x 200B SSK" in self.upload_times:
|
||||
B = self.upload_times["100x 200B SSK"] / 100
|
||||
else:
|
||||
B = self.upload_times["10x 200B SSK"] / 10
|
||||
print("upload per-file time SSK: %.3fs" % B)
|
||||
A1 = 1*MB / (self.upload_times["1MB SSK"] - B) # in bytes per second
|
||||
print("upload speed SSK (1MB):", self.number(A1, "Bps"))
|
||||
|
||||
# download SSK
|
||||
if "100x 200B SSK" in self.download_times:
|
||||
B = self.download_times["100x 200B SSK"] / 100
|
||||
else:
|
||||
B = self.download_times["10x 200B SSK"] / 10
|
||||
print("download per-file time SSK: %.3fs" % B)
|
||||
A1 = 1*MB / (self.download_times["1MB SSK"] - B) # in bytes per
|
||||
# second
|
||||
print("download speed SSK (1MB):", self.number(A1, "Bps"))
|
||||
|
||||
def number(self, value, suffix=""):
|
||||
scaling = 1
|
||||
if value < 1:
|
||||
fmt = "%1.2g%s"
|
||||
elif value < 100:
|
||||
fmt = "%.1f%s"
|
||||
elif value < 1000:
|
||||
fmt = "%d%s"
|
||||
elif value < 1e6:
|
||||
fmt = "%.2fk%s"; scaling = 1e3
|
||||
elif value < 1e9:
|
||||
fmt = "%.2fM%s"; scaling = 1e6
|
||||
elif value < 1e12:
|
||||
fmt = "%.2fG%s"; scaling = 1e9
|
||||
elif value < 1e15:
|
||||
fmt = "%.2fT%s"; scaling = 1e12
|
||||
elif value < 1e18:
|
||||
fmt = "%.2fP%s"; scaling = 1e15
|
||||
else:
|
||||
fmt = "huge! %g%s"
|
||||
return fmt % (value / scaling, suffix)
|
||||
|
||||
def tearDown(self, res):
|
||||
d = self.base_service.stopService()
|
||||
d.addCallback(lambda ignored: res)
|
||||
return d
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_client_dir = sys.argv[1]
|
||||
st = SpeedTest(test_client_dir)
|
||||
st.run()
|
@ -8,7 +8,6 @@ Runs on Python 3.
|
||||
Usage: ./check-debugging.py src
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys, re, os
|
||||
|
||||
|
@ -4,7 +4,6 @@
|
||||
#
|
||||
# bin/tahoe @misc/coding_tools/check-interfaces.py
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os, sys, re, platform
|
||||
|
||||
|
@ -8,7 +8,6 @@ This runs on Python 3.
|
||||
|
||||
# ./check-umids.py src
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys, re, os
|
||||
|
||||
|
@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os, sys
|
||||
|
||||
from twisted.python import usage
|
||||
|
||||
class Options(usage.Options):
|
||||
optFlags = [
|
||||
("recursive", "r", "Search for .py files recursively"),
|
||||
]
|
||||
def parseArgs(self, *starting_points):
|
||||
self.starting_points = starting_points
|
||||
|
||||
found = [False]
|
||||
|
||||
def check(fn):
|
||||
f = open(fn, "r")
|
||||
for i,line in enumerate(f.readlines()):
|
||||
if line == "\n":
|
||||
continue
|
||||
if line[-1] == "\n":
|
||||
line = line[:-1]
|
||||
if line.rstrip() != line:
|
||||
# the %s:%d:%d: lets emacs' compile-mode jump to those locations
|
||||
print("%s:%d:%d: trailing whitespace" % (fn, i+1, len(line)+1))
|
||||
found[0] = True
|
||||
f.close()
|
||||
|
||||
o = Options()
|
||||
o.parseOptions()
|
||||
if o['recursive']:
|
||||
for starting_point in o.starting_points:
|
||||
for root, dirs, files in os.walk(starting_point):
|
||||
for fn in [f for f in files if f.endswith(".py")]:
|
||||
fn = os.path.join(root, fn)
|
||||
check(fn)
|
||||
else:
|
||||
for fn in o.starting_points:
|
||||
check(fn)
|
||||
if found[0]:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
@ -21,11 +21,10 @@
|
||||
# Install 'click' first. I run this with py2, but py3 might work too, if the
|
||||
# wheels can be built with py3.
|
||||
|
||||
from __future__ import unicode_literals, print_function
|
||||
import os, sys, subprocess, json, tempfile, zipfile, re, itertools
|
||||
import email.parser
|
||||
from pprint import pprint
|
||||
from six.moves import StringIO
|
||||
from io import StringIO
|
||||
import click
|
||||
|
||||
all_packages = {} # name -> version
|
||||
|
@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
"""
|
||||
Given a list of nodeids and a 'convergence' file, create a bunch of files
|
||||
|
@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
"""Create a short probably-unique string for use as a umid= argument in a
|
||||
Foolscap log() call, to make it easier to locate the source code that
|
||||
|
@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from foolscap import Tub, eventual
|
||||
from twisted.internet import reactor
|
||||
|
@ -1,6 +1,5 @@
|
||||
# -*- python -*-
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from twisted.internet import reactor
|
||||
import sys
|
||||
|
@ -1,6 +1,5 @@
|
||||
# -*- python -*-
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
"""
|
||||
# run this tool on a linux box in its own directory, with a file named
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
# feed this the results of 'tahoe catalog-shares' for all servers
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
|
||||
|
@ -1,20 +0,0 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from foolscap import Tub
|
||||
from foolscap.eventual import eventually
|
||||
import sys
|
||||
from twisted.internet import reactor
|
||||
|
||||
def go():
|
||||
t = Tub()
|
||||
d = t.getReference(sys.argv[1])
|
||||
d.addCallback(lambda rref: rref.callRemote("get_memory_usage"))
|
||||
def _got(res):
|
||||
print(res)
|
||||
reactor.stop()
|
||||
d.addCallback(_got)
|
||||
|
||||
eventually(go)
|
||||
reactor.run()
|
@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os, sys, re
|
||||
import urllib
|
||||
|
@ -5,7 +5,6 @@
|
||||
# is left on all disks across the grid. The plugin should be configured with
|
||||
# env_url= pointing at the diskwatcher.tac webport.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os, sys, urllib, json
|
||||
|
||||
|
@ -6,7 +6,6 @@
|
||||
# used. The plugin should be configured with env_url= pointing at the
|
||||
# diskwatcher.tac webport.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os, sys, urllib, json
|
||||
|
||||
|
@ -5,7 +5,6 @@
|
||||
# is being used per unit time. The plugin should be configured with env_url=
|
||||
# pointing at the diskwatcher.tac webport.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os, sys, urllib, json
|
||||
|
||||
|
@ -5,7 +5,6 @@
|
||||
# used on all disks across the grid. The plugin should be configured with
|
||||
# env_url= pointing at the diskwatcher.tac webport.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os, sys, urllib, json
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user