diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 76dce30e2e5137a8a9199f2d739f96db92988406..d6b06fae42f6e738725238fac59617aeb161dfd4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -39,7 +39,7 @@ morph-build-localdev: - | # The local grid configuration is *almost* complete enough to build. It # just needs this tweak. - sed -i 's/undefined/\"unundefined\"/' morph/grid/${GRID}/public-keys/users.nix + echo '{}' > morph/grid/${GRID}/public-keys/users.nix morph-build-testing: <<: *MORPH_BUILD @@ -74,22 +74,47 @@ system-tests: .update-grid: &UPDATE_GRID stage: "deploy" script: | - env --ignore-environment - NIX_PATH=$NIX_PATH GITLAB_USER_LOGIN=$GITLAB_USER_LOGIN CI_JOB_NAME=$CI_JOB_NAME CI_PIPELINE_SOURCE=$CI_PIPELINE_SOURCE CI_COMMIT_BRANCH=$CI_COMMIT_BRANCH ./ci-tools/update-grid-servers "${PRIVATESTORAGEIO_SSH_DEPLOY_KEY_PATH}" "${CI_ENVIRONMENT_NAME}" - -# Update the staging deployment - only on a merge to the staging branch. + env --ignore-environment - \ + NIX_PATH="$NIX_PATH" \ + GITLAB_USER_LOGIN="$GITLAB_USER_LOGIN" \ + CI_JOB_NAME="$CI_JOB_NAME" \ + CI_PIPELINE_SOURCE="$CI_PIPELINE_SOURCE" \ + CI_COMMIT_BRANCH="$CI_COMMIT_BRANCH" \ + ./ci-tools/update-grid-servers "${PRIVATESTORAGEIO_SSH_DEPLOY_KEY_PATH}" "${CI_ENVIRONMENT_NAME}" + +# Update the staging deployment - only on a commit to the develop branch. update-staging: <<: *UPDATE_GRID - only: - - "staging" + # https://docs.gitlab.com/ee/ci/yaml/#rules + rules: + # https://docs.gitlab.com/ee/ci/yaml/index.html#rulesif + # https://docs.gitlab.com/ee/ci/jobs/job_control.html#cicd-variable-expressions + # https://docs.gitlab.com/ee/ci/variables/predefined_variables.html + - if: '$CI_COMMIT_BRANCH == "develop"' environment: + # You can find some status information about environments in GitLab at + # https://whetstone.privatestorage.io/privatestorage/PrivateStorageio/-/environments. name: "staging" - url: "https://privatestorage-staging.com/" - -# Update the production deployment - only on a merge to the production branch. + # The URL controls where the "View Deployment" button for this environment + # will take you. The main website isn't controlled by this codebase so we + # don't point there. The monitoring system *is* controlled by this + # codebase and it also tells us lots of stuff about other things + # controlled by this codebase so that seems like a good place to land. + # Not that I make it a habit to visit the deployment using the GitLab + # button... Still, discoverability or something. + url: "https://monitoring.privatestorage-staging.com/" + +# Update the production deployment - only on a commit to the production branch. deploy-to-production: <<: *UPDATE_GRID - only: - - "production" + # https://docs.gitlab.com/ee/ci/yaml/#rules + rules: + # https://docs.gitlab.com/ee/ci/yaml/index.html#rulesif + # https://docs.gitlab.com/ee/ci/jobs/job_control.html#cicd-variable-expressions + # https://docs.gitlab.com/ee/ci/variables/predefined_variables.html + - if: '$CI_COMMIT_BRANCH == "production"' + environment: + # See notes in `update-staging`. name: "production" - url: "https://privatestorage.io/" + url: "https://monitoring.private.storage/" diff --git a/DEPLOYMENT-NOTES.rst b/DEPLOYMENT-NOTES.rst new file mode 100644 index 0000000000000000000000000000000000000000..5de83386dbb939cc3cfe2a8b68c198f218934933 --- /dev/null +++ b/DEPLOYMENT-NOTES.rst @@ -0,0 +1,14 @@ +Deployment notes +================ + +- 2021-09-07 `Manage access to payment metrics <https://whetstone.privatestorage.io/privatestorage/PrivateStorageio/-/merge_requests/146>`_ requires moving and chown'ing the PaymentServer database on the ``payments`` host:: + + mkdir /var/lib/zkapissuer + + mv /var/db/vouchers.sqlite3 /var/lib/zkapissuer/vouchers.sqlite3 + + chown -R zkapissuer:zkapissuer /var/lib/zkapissuer + + chmod 750 /var/lib/zkapissuer + chmod 640 /var/lib/zkapissuer/vouchers.sqlite3 + diff --git a/README.rst b/README.rst index d3d9f088db4f8b976f3f55852715762280bb93c0..3a2b2d8ecfcdba6ffaa4f3f2e275cb85feb709de 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ Project Hosting Moved ===================== -This project can now be found at https://whetstone.privatestorage.io/opensource/PrivateStorageio +This project can now be found at https://whetstone.privatestorage.io/privatestorage/PrivateStorageio PrivateStorageio ================ @@ -13,8 +13,8 @@ Documentation There is documentation for: -* Operators/Admins: ``docs/ops/README.rst`` -* Developers: ``docs/dev/README.rst`` +* Operators/Admins: `<docs/source/ops/README.rst>`_ +* Developers: `<docs/source/dev/README.rst>`_ The documentation can be built using this command:: diff --git a/docs/source/conf.py b/docs/source/conf.py index 36b62a34aae4ecea544e5f0527c85809106057a8..66aa921e2ba799e1b1b4d8e7a778ab07ee07a73b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -86,7 +86,6 @@ html_theme_options = { 'logo': 'logo-ps.svg', 'description': " ", # ugly hack to get some white space below the logo 'fixed_sidebar': True, - 'extra_nav_links': {"Fork me on GitHub": "https://github.com/PrivateStorageio/PrivateStorageio"}, } # Add any paths that contain custom static files (such as style sheets) here, diff --git a/docs/source/dev/README.rst b/docs/source/dev/README.rst index 904e8b3be07bdcc1473a3c1fe22afe8ffb0e15a2..14d2de31f932a0aa50545643e30c679c36696e19 100644 --- a/docs/source/dev/README.rst +++ b/docs/source/dev/README.rst @@ -28,7 +28,7 @@ The system tests boot QEMU VMs which prevents them from running on CI at this ti The build requires > 10 GB of disk space, and the VMs might be timing out on slow or busy machines. If you run into timeouts, -try `raising the number of retries <https://github.com/PrivateStorageio/PrivateStorageio/blob/e8233d2/nixos/modules/tests/run-introducer.py#L55-L62>`_. +try `raising the number of retries <https://whetstone.privatestorage.io/privatestorage/PrivateStorageio/-/blob/e8233d2/nixos/modules/tests/run-introducer.py#L55-62>`_. It is also possible go through the testing script interactively - useful for debugging:: @@ -36,9 +36,27 @@ It is also possible go through the testing script interactively - useful for deb This will give you a result symlink in the current directory. Inside that is bin/nixos-test-driver which gives you a kind of REPL for interacting with the VMs. -The kind of `Perl in this testScript <https://github.com/PrivateStorageio/PrivateStorageio/blob/78881a3/nixos/modules/tests/private-storage.nix#L180>`_ is what you can enter into this REPL. +The kind of `Python in this testScript <https://whetstone.privatestorage.io/privatestorage/PrivateStorageio/-/blob/78881a3/nixos/modules/tests/private-storage.nix#L180>`_ is what you can enter into this REPL. Consult the `official documentation on NixOS Tests <https://nixos.org/manual/nixos/stable/index.html#sec-nixos-tests>`_ for more information. +Updatings Pins +-------------- + +Nixpkgs +``````` + +To update the version of NixOS we deploy with, run: + +.. code: shell + + nix-shell --run 'update-nixpkgs' + +That will update ``nixpkgs-2015.json`` to the latest release on the nixos-21.05 channel. + +To update the channel, the script will need to be updated, +along with the filenames that have the channel in them. + + Architecture overview --------------------- @@ -48,8 +66,5 @@ Architecture overview .. include:: ../../../morph/grid/local/README.rst - - - .. _Nix: https://nixos.org/nix diff --git a/morph/grid/local/.gitignore b/morph/grid/local/.gitignore index 8000dd9db47c0b9dd34046ec17880dcbb27e5eb9..00e940f3fb4c5e579dbdf2964110b9a187beb98a 100644 --- a/morph/grid/local/.gitignore +++ b/morph/grid/local/.gitignore @@ -1 +1,2 @@ -.vagrant +/.vagrant +/public-keys/users.nix diff --git a/morph/grid/local/README.rst b/morph/grid/local/README.rst index d30d8766a4ef5a8db228ef38374330734e69cba7..48f395cb82fc272481a61f0d1ab425ffbd20cd02 100644 --- a/morph/grid/local/README.rst +++ b/morph/grid/local/README.rst @@ -35,7 +35,7 @@ Use the local development environment Latest Morph honors the ``SSH_CONFIG_FILE`` environment variable (`since 3f90aa88 (March 2020, v 1.5.0) <https://github.com/DBCDK/morph/commit/3f90aa885fac1c29fce9242452fa7c0c505744ef#diff-d155ad793bd62e6ea4c44ba985049ecb13a4f4f32f799791b2bce695a16c0101>`_), so in the future this should get a bit more convenient. -6. Add your SSH key to ``users.nix`` so you'll be able to log in after deploying the new configuration:: +6. Create a ``public-keys/users.nix`` file with your SSH key (see ``public-keys/users.nix.example`` for the format) so you'll be able to log in after deploying the new configuration:: $EDITOR public-keys/users.nix diff --git a/morph/grid/local/grid.nix b/morph/grid/local/grid.nix index 51f41832ded8fe18290c47b5b3ad85fb58c2a511..46cb9c8ec1dc5278823c9e3ffc405289e7510469 100644 --- a/morph/grid/local/grid.nix +++ b/morph/grid/local/grid.nix @@ -2,20 +2,65 @@ let pkgs = import <nixpkgs> { }; gridlib = import ../../lib; - rawConfig = pkgs.lib.trivial.importJSON ./config.json; - config = rawConfig // { - sshUsers = import ./public-keys/users.nix; + grid-config = pkgs.lib.trivial.importJSON ./config.json; + ssh-users = let + ssh-users-file = ./public-keys/users.nix; + in + if builtins.pathExists ssh-users-file then + import ssh-users-file + else + # Use builtins.toString so that nix does not add the file + # to the nix store before including it in the string. + throw '' + ssh-keys for local grid are not configured. + Refusing to build a possibly inaccessible configuration. + Please create ${builtins.toString ssh-users-file} before building. + See ${builtins.toString ./README.rst} for more information. + ''; + + # Module with per-grid configuration + grid-module = {config, ...}: { + imports = [ + gridlib.base + # Allow us to remotely trigger updates to this system. + ../../../nixos/modules/deployment.nix + # Give it a good SSH configuration. + ../../../nixos/modules/ssh.nix + ]; + services.private-storage.sshUsers = ssh-users; + + # Include the ssh-users config in a form that can be read by nix, + # so the self-update deployment system can access it. + # nixos/modules/update-deployment imports the nix file into + # the checkout of this repository it creates. + environment.etc."nixos/ssh-users.json" = { + # Output the loaded value, rather than just copying the file, in case the + # file has external references. + mode = "0666"; + text = builtins.toJSON ssh-users; + }; + environment.etc."nixos/ssh-users.nix" = { + # This is the file that is imported by update-deployment. + # We don't directly read the JSON so that the script doesn't + # depend on the format we use. + mode = "0666"; + text = '' + # Include the ssh-users config + builtins.fromJSON (builtins.readFile ./ssh-users.json) + ''; + }; + + networking.domain = grid-config.domain; # Convert relative paths to absolute so library code can resolve names # correctly. - publicKeyPath = toString ./. + "/${rawConfig.publicKeyPath}"; - privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}"; - }; - - # Configure deployment management authorization for all systems in the grid. - deployment = { + grid = { + publicKeyPath = toString ./. + "/${grid-config.publicKeyPath}"; + privateKeyPath = toString ./. + "/${grid-config.privateKeyPath}"; + }; + # Configure deployment management authorization for all systems in the grid. services.private-storage.deployment = { - authorizedKey = builtins.readFile "${config.publicKeyPath}/deploy_key.pub"; + authorizedKey = builtins.readFile "${config.grid.publicKeyPath}/deploy_key.pub"; gridName = "local"; }; }; @@ -24,10 +69,10 @@ let imports = [ gridlib.issuer (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.21"; })) - (gridlib.customize-issuer (config // { + (gridlib.customize-issuer (grid-config // { monitoringvpnIPv4 = "172.23.23.11"; })) - deployment + grid-module ]; }; @@ -35,11 +80,11 @@ let imports = [ gridlib.storage (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.22"; })) - (gridlib.customize-storage (config // { + (gridlib.customize-storage (grid-config // { monitoringvpnIPv4 = "172.23.23.12"; stateVersion = "19.09"; })) - deployment + grid-module ]; }; @@ -47,11 +92,11 @@ let imports = [ gridlib.storage (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.23"; })) - (gridlib.customize-storage (config // { + (gridlib.customize-storage (grid-config // { monitoringvpnIPv4 = "172.23.23.13"; stateVersion = "19.09"; })) - deployment + grid-module ]; }; @@ -61,12 +106,12 @@ let (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.24"; })) (gridlib.customize-monitoring { inherit hostsMap vpnClientIPs nodeExporterTargets paymentExporterTargets; - inherit (config) domain publicKeyPath privateKeyPath sshUsers letsEncryptAdminEmail; - googleOAuthClientID = config.monitoringGoogleOAuthClientID; + inherit (grid-config) letsEncryptAdminEmail; + googleOAuthClientID = grid-config.monitoringGoogleOAuthClientID; monitoringvpnIPv4 = "172.23.23.1"; stateVersion = "19.09"; }) - deployment + grid-module ]; }; diff --git a/morph/grid/local/public-keys/users.nix b/morph/grid/local/public-keys/users.nix.example similarity index 62% rename from morph/grid/local/public-keys/users.nix rename to morph/grid/local/public-keys/users.nix.example index 412077c0d5d6d98024036e369dfa552604f2dc57..10a60be1f7b8760e81f7fdb6ecd1d177913e05af 100644 --- a/morph/grid/local/public-keys/users.nix +++ b/morph/grid/local/public-keys/users.nix.example @@ -1,4 +1,6 @@ # Add your public key. Example: # let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHx7wJQNqKn8jOC4AxySRL2UxidNp7uIK9ad3pMb1ifF flo@fs-la"; +# You can use the following to get key from the local machine. +# let key = builtins.readFile ~/.ssh/id_ed25519.pub; let key = undefined; in { "root" = key; "vagrant" = key; } diff --git a/morph/grid/production/grid.nix b/morph/grid/production/grid.nix index 06eefdd28da57ad65ea99543ba8421bc934ef752..6009be84fb2a7ed7ca63e2e73b4f08f1f45ecb0d 100644 --- a/morph/grid/production/grid.nix +++ b/morph/grid/production/grid.nix @@ -3,20 +3,28 @@ let pkgs = import <nixpkgs> { }; gridlib = import ../../lib; - rawConfig = pkgs.lib.trivial.importJSON ./config.json; - config = rawConfig // { - sshUsers = import ./public-keys/users.nix; + grid-config = pkgs.lib.trivial.importJSON ./config.json; + # Module with per-grid configuration + grid-module = {config, ...}: { + imports = [ + gridlib.base + # Allow us to remotely trigger updates to this system. + ../../../nixos/modules/deployment.nix + # Give it a good SSH configuration. + ../../../nixos/modules/ssh.nix + ]; + services.private-storage.sshUsers = import ./public-keys/users.nix; + networking.domain = grid-config.domain; # Convert relative paths to absolute so library code can resolve names # correctly. - publicKeyPath = toString ./. + "/${rawConfig.publicKeyPath}"; - privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}"; - }; - - # Configure deployment management authorization for all systems in the grid. - deployment = { + grid = { + publicKeyPath = toString ./. + "/${grid-config.publicKeyPath}"; + privateKeyPath = toString ./. + "/${grid-config.privateKeyPath}"; + }; + # Configure deployment management authorization for all systems in the grid. services.private-storage.deployment = { - authorizedKey = builtins.readFile "${config.publicKeyPath}/deploy_key.pub"; + authorizedKey = builtins.readFile "${config.grid.publicKeyPath}/deploy_key.pub"; gridName = "production"; }; }; @@ -25,10 +33,10 @@ let imports = [ gridlib.issuer gridlib.hardware-aws - (gridlib.customize-issuer (config // { + (gridlib.customize-issuer (grid-config // { monitoringvpnIPv4 = "172.23.23.11"; })) - deployment + grid-module ]; }; @@ -38,12 +46,12 @@ let gridlib.hardware-aws (gridlib.customize-monitoring { inherit hostsMap vpnClientIPs nodeExporterTargets paymentExporterTargets; - inherit (config) domain publicKeyPath privateKeyPath sshUsers letsEncryptAdminEmail; - googleOAuthClientID = config.monitoringGoogleOAuthClientID; + inherit (grid-config) letsEncryptAdminEmail; + googleOAuthClientID = grid-config.monitoringGoogleOAuthClientID; monitoringvpnIPv4 = "172.23.23.1"; stateVersion = "19.09"; }) - deployment + grid-module ]; }; @@ -65,13 +73,13 @@ let gridlib.storage # Then customize the storage system a little bit based on this node's particulars. - (gridlib.customize-storage (config // nodecfg // { + (gridlib.customize-storage (grid-config // nodecfg // { monitoringvpnIPv4 = vpnIP; inherit stateVersion; })) # Also configure deployment management authorization - deployment + grid-module ]; # And supply configuration for those hardware / network / bootloader diff --git a/morph/grid/testing/grid.nix b/morph/grid/testing/grid.nix index 7b06c99e1f7a1b65b535f924a0a24aebe6753586..18983f0b32d28f13981b56475d7691a8cb434808 100644 --- a/morph/grid/testing/grid.nix +++ b/morph/grid/testing/grid.nix @@ -3,20 +3,28 @@ let pkgs = import <nixpkgs> { }; gridlib = import ../../lib; - rawConfig = pkgs.lib.trivial.importJSON ./config.json; - config = rawConfig // { - sshUsers = import ./public-keys/users.nix; + grid-config = pkgs.lib.trivial.importJSON ./config.json; + # Module with per-grid configuration + grid-module = {config, ...}: { + imports = [ + gridlib.base + # Allow us to remotely trigger updates to this system. + ../../../nixos/modules/deployment.nix + # Give it a good SSH configuration. + ../../../nixos/modules/ssh.nix + ]; + services.private-storage.sshUsers = import ./public-keys/users.nix; + networking.domain = grid-config.domain; # Convert relative paths to absolute so library code can resolve names # correctly. - publicKeyPath = toString ./. + "/${rawConfig.publicKeyPath}"; - privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}"; - }; - - # Configure deployment management authorization for all systems in the grid. - deployment = { + grid = { + publicKeyPath = toString ./. + "/${grid-config.publicKeyPath}"; + privateKeyPath = toString ./. + "/${grid-config.privateKeyPath}"; + }; + # Configure deployment management authorization for all systems in the grid. services.private-storage.deployment = { - authorizedKey = builtins.readFile "${config.publicKeyPath}/deploy_key.pub"; + authorizedKey = builtins.readFile "${config.grid.publicKeyPath}/deploy_key.pub"; gridName = "testing"; }; }; @@ -25,10 +33,10 @@ let imports = [ gridlib.issuer gridlib.hardware-aws - (gridlib.customize-issuer (config // { + (gridlib.customize-issuer (grid-config // { monitoringvpnIPv4 = "172.23.23.11"; })) - deployment + grid-module ]; }; @@ -37,11 +45,11 @@ let gridlib.storage gridlib.hardware-aws ./testing001-hardware.nix - (gridlib.customize-storage (config // { + (gridlib.customize-storage (grid-config // { monitoringvpnIPv4 = "172.23.23.12"; stateVersion = "19.03"; })) - deployment + grid-module ]; }; @@ -51,12 +59,12 @@ let gridlib.hardware-aws (gridlib.customize-monitoring { inherit hostsMap vpnClientIPs nodeExporterTargets paymentExporterTargets; - inherit (config) domain publicKeyPath privateKeyPath sshUsers letsEncryptAdminEmail; - googleOAuthClientID = config.monitoringGoogleOAuthClientID; + inherit (grid-config) letsEncryptAdminEmail; + googleOAuthClientID = grid-config.monitoringGoogleOAuthClientID; monitoringvpnIPv4 = "172.23.23.1"; stateVersion = "19.09"; }) - deployment + grid-module ]; }; diff --git a/morph/lib/base.nix b/morph/lib/base.nix new file mode 100644 index 0000000000000000000000000000000000000000..271766d9cff5253f6d9a72e475dec3398b2cd6b3 --- /dev/null +++ b/morph/lib/base.nix @@ -0,0 +1,36 @@ +# This module contains settings and configuration that apply to all nodes in a grid. +{ lib, config, ...}: +{ + options.grid = { + publicKeyPath = lib.mkOption { + type = lib.types.path; + description = '' + A path on the deployment system of a directory containing all of the + public keys for the system. For example, this holds Wireguard public keys + for the VPN configuration and SSH public keys to configure SSH + authentication. + ''; + }; + privateKeyPath = lib.mkOption { + type = lib.types.path; + description = '' + A path on the deployment system of a directory containing all of the + corresponding private keys for the system. + ''; + }; + }; + + imports = [ + ../../nixos/modules/packages.nix + ]; + + config = { + # The morph default deployment target the name of the node in the network + # attrset. We don't always want to give the node its proper public address + # there (because it depends on which domain is associated with the grid + # being configured and using variable names complicates a lot of things). + # Instead, just tell morph how to reach the node here - by using its fully + # qualified domain name. + deployment.targetHost = "${config.networking.hostName}.${config.networking.domain}"; + }; +} diff --git a/morph/lib/bootstrap-configuration.nix b/morph/lib/bootstrap-configuration.nix index e26e345780d0d4da2d65e7b79af9c5e445a35e98..531f867572f3bd46963fc850384f6280f11531a1 100644 --- a/morph/lib/bootstrap-configuration.nix +++ b/morph/lib/bootstrap-configuration.nix @@ -67,7 +67,7 @@ let # Stop! I hope you're done when you get here. If you have to modify # anything below this point the expression should probably be refactored and # another variable added controlling whatever new thing you need to control. - # Open an issue: https://github.com/PrivateStorageio/PrivateStorageio/issues/new + # Open an issue: https://whetstone.privatestorage.io/privatestorage/PrivateStorageio/-/issues/new?issue in # Define a function that ignores all its arguments. We don't need any of them # for now. diff --git a/morph/lib/customize-issuer.nix b/morph/lib/customize-issuer.nix index 1c0d668fbd4ae59bab115c2116b7fa377395dcfc..0686556cdf6abe79f0ac9e16586c9c219f3cddb1 100644 --- a/morph/lib/customize-issuer.nix +++ b/morph/lib/customize-issuer.nix @@ -1,37 +1,13 @@ # Define a function which returns a value which fills in all the holes left by # ``issuer.nix``. { - # A path on the deployment system of a directory containing all of the - # public keys for the system. For example, this holds Wireguard public keys - # for the VPN configuration and SSH public keys to configure SSH - # authentication. - publicKeyPath - - # A path on the deployment system of a directory containing all of the - # corresponding private keys for the system. -, privateKeyPath - # A string giving the IP address and port number (":"-separated) of the VPN # server. -, monitoringvpnEndpoint + monitoringvpnEndpoint # A string giving the VPN IPv4 address for this system. , monitoringvpnIPv4 - # A string giving the domain name associated with this grid. This is meant - # to be combined with the hostname for this system to produce a - # fully-qualified domain name. For example, an issuer might have "payments" - # as its hostname and belong to a grid with the domain - # "example-grid.invalid". This ``domain`` parameter should have the value - # ``"example-grid.invalid"`` for the system figure out that - # ``payments.example-grid.invalid`` is the name of this system. -, domain - - # A set mapping usernames as strings to SSH public keys as strings. For - # each element of the site, the indicated user is configured on the system - # with the indicated SSH key as an authorized key. -, sshUsers - # A string giving an email address to use for Let's Encrypt registration and # certificate issuance. , letsEncryptAdminEmail @@ -45,25 +21,11 @@ , allowedChargeOrigins , ... }: -{ config, ... }: { - # The morph default deployment target the name of the node in the network - # attrset. We don't always want to give the node its proper public address - # there (because it depends on which domain is associated with the grid - # being configured and using variable names complicates a lot of things). - # Instead, just tell morph how to reach the node here - by using its fully - # qualified domain name. - deployment.targetHost = "${config.networking.hostName}.${config.networking.domain}"; - +{ config, ... }: +let + inherit (config.grid) publicKeyPath privateKeyPath; +in { deployment.secrets = { - # A path on the deployment system to a file containing the Ristretto - # signing key. This is used as the source of the Ristretto signing key - # morph secret. - "ristretto-signing-key".source = "${privateKeyPath}/ristretto.signing-key"; - - # A path on the deployment system to a file containing the Stripe secret - # key. This is used as the source of the Stripe secret key morph secret. - "stripe-secret-key".source = "${privateKeyPath}/stripe.secret"; - # ``.../monitoringvpn`` is a path on the deployment system of a directory # containing a number of VPN-related secrets. This is expected to contain # a number of files named like ``<VPN IPv4 address>.key`` containing the @@ -75,9 +37,6 @@ "monitoringvpn-preshared-key".source = "${privateKeyPath}/monitoringvpn/preshared.key"; }; - networking.domain = domain; - - services.private-storage.sshUsers = sshUsers; services.private-storage.monitoring.vpn.client = { enable = true; ip = monitoringvpnIPv4; diff --git a/morph/lib/customize-monitoring.nix b/morph/lib/customize-monitoring.nix index 391aa5602575100c8650d8e4fb6892e38fc95ebf..19a800f1fa806c09f132f2bb2769869a30c65ec2 100644 --- a/morph/lib/customize-monitoring.nix +++ b/morph/lib/customize-monitoring.nix @@ -9,11 +9,7 @@ hostsMap # See ``customize-issuer.nix``. -, publicKeyPath -, privateKeyPath , monitoringvpnIPv4 -, domain -, sshUsers , letsEncryptAdminEmail # A list of VPN IP addresses as strings indicating which clients will be @@ -40,10 +36,10 @@ , stateVersion , ... }: -{ config, ... }: { - # See customize-issuer.nix for an explanatoin of targetHost value. - deployment.targetHost = "${config.networking.hostName}.${config.networking.domain}"; - +{ config, ... }: +let + inherit (config.grid) publicKeyPath privateKeyPath; +in { deployment.secrets = let # When Grafana SSO is disabled there is not necessarily any client secret # available. Avoid telling morph that there is one in this case (so it @@ -82,11 +78,8 @@ in grafanaSSO // monitoringvpn; - networking.domain = domain; networking.hosts = hostsMap; - services.private-storage.sshUsers = sshUsers; - services.private-storage.monitoring.vpn.server = { enable = true; ip = monitoringvpnIPv4; diff --git a/morph/lib/customize-storage.nix b/morph/lib/customize-storage.nix index 68655874efd9ba39b52dacfdddaedb54863ed769..6a288213c3f117309b697e44304be9a7d5620bcb 100644 --- a/morph/lib/customize-storage.nix +++ b/morph/lib/customize-storage.nix @@ -2,12 +2,8 @@ # ``storage.nix``. { # See ``customize-issuer.nix`` - privateKeyPath -, publicKeyPath -, monitoringvpnEndpoint + monitoringvpnEndpoint , monitoringvpnIPv4 -, sshUsers -, domain # An integer giving the value of a single pass in byte×months. , passValue @@ -20,20 +16,17 @@ , stateVersion , ... }: -{ config, ... }: { - # See customize-issuer.nix for an explanatoin of targetHost value. - deployment.targetHost = "${config.networking.hostName}.${config.networking.domain}"; - +{ config, ... }: +let + inherit (config.grid) publicKeyPath privateKeyPath; +in { deployment.secrets = { - "ristretto-signing-key".source = "${privateKeyPath}/ristretto.signing-key"; "monitoringvpn-secret-key".source = "${privateKeyPath}/monitoringvpn/${monitoringvpnIPv4}.key"; "monitoringvpn-preshared-key".source = "${privateKeyPath}/monitoringvpn/preshared.key"; }; - networking.domain = domain; - services.private-storage = { - inherit sshUsers passValue publicStoragePort; + inherit passValue publicStoragePort; }; services.private-storage.monitoring.vpn.client = { diff --git a/morph/lib/default.nix b/morph/lib/default.nix index bdd92f4bfe52eba2e19df3ac73a087a4af4a53dc..bf25e5a58d04d148296bffef48acc4e4e125684b 100644 --- a/morph/lib/default.nix +++ b/morph/lib/default.nix @@ -2,6 +2,8 @@ # coherent public interface. Application code should prefer these names over # directly importing the source files in this directory. { + base = import ./base.nix; + hardware-aws = import ./issuer-aws.nix; hardware-virtual = import ./hardware-virtual.nix; @@ -13,4 +15,6 @@ monitoring = import ./monitoring.nix; customize-monitoring = import ./customize-monitoring.nix; + + modules = builtins.toString ../../nixos/modules; } diff --git a/morph/lib/issuer.nix b/morph/lib/issuer.nix index 51046b436e297cdc5034134e3503556e8030588c..d60af799888c97ec8f97a061d40b54d3f2db82a7 100644 --- a/morph/lib/issuer.nix +++ b/morph/lib/issuer.nix @@ -1,23 +1,28 @@ -# This is all of the static NixOS system configuration necessary to specify an -# "issuer"-type system. The configuration has various holes in it which must -# be filled somehow. These holes correspond to configuration which is not -# statically known. This value is suitable for use as a module to be imported -# into a more complete system configuration. It is expected that the holes -# will be filled by a sibling module created by ``customize-issuer.nix``. -rec { +# This, along with `customize-issuer.nix, contains all of the NixOS system +# configuration necessary to specify an "issuer"-type system. Originally, this +# file has all the static configuration, and `customize-issuer.nix` was a function +# that filled in the holes. We are in the process of merging the modules, using settings +# instead of function arguments. +# See https://whetstone.privatestorage.io/privatestorage/PrivateStorageio/-/issues/80 +{ config, ...}: +let + inherit (config.grid) publicKeyPath privateKeyPath; +in { deployment = { secrets = { "ristretto-signing-key" = { destination = "/run/keys/ristretto.signing-key"; - owner.user = "root"; - owner.group = "root"; + source = "${privateKeyPath}/ristretto.signing-key"; + owner.user = "zkapissuer"; + owner.group = "zkapissuer"; permissions = "0400"; action = ["sudo" "systemctl" "restart" "zkapissuer.service"]; }; "stripe-secret-key" = { destination = "/run/keys/stripe.secret-key"; - owner.user = "root"; - owner.group = "root"; + source = "${privateKeyPath}/stripe.secret"; + owner.user = "zkapissuer"; + owner.group = "zkapissuer"; permissions = "0400"; action = ["sudo" "systemctl" "restart" "zkapissuer.service"]; }; @@ -40,9 +45,6 @@ rec { }; imports = [ - # Allow us to remotely trigger updates to this system. - ../../nixos/modules/deployment.nix - ../../nixos/modules/issuer.nix ../../nixos/modules/monitoring/vpn/client.nix ../../nixos/modules/monitoring/exporters/node.nix @@ -51,9 +53,9 @@ rec { services.private-storage-issuer = { enable = true; tls = true; - ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination; - stripeSecretKeyPath = deployment.secrets.stripe-secret-key.destination; + ristrettoSigningKeyPath = config.deployment.secrets.ristretto-signing-key.destination; + stripeSecretKeyPath = config.deployment.secrets.stripe-secret-key.destination; database = "SQLite3"; - databasePath = "/var/db/vouchers.sqlite3"; + databasePath = "/var/lib/zkapissuer/vouchers.sqlite3"; }; } diff --git a/morph/lib/monitoring.nix b/morph/lib/monitoring.nix index f8810be2f7e878eeb979e82d2746895d6157212e..bf92d1041f2bf9b9fb1ff4580a25ff7b596a9bbb 100644 --- a/morph/lib/monitoring.nix +++ b/morph/lib/monitoring.nix @@ -1,6 +1,6 @@ # Similar to ``issuer.nix`` but for a "monitoring"-type system. Holes are # filled by ``customize-monitoring.nix``. -rec { +{ deployment = { secrets = { "monitoringvpn-private-key" = { @@ -21,11 +21,6 @@ rec { }; imports = [ - # Give it a good SSH configuration. - ../../nixos/modules/ssh.nix - # Allow us to remotely trigger updates to this system. - ../../nixos/modules/deployment.nix - ../../nixos/modules/monitoring/vpn/server.nix ../../nixos/modules/monitoring/server/grafana.nix ../../nixos/modules/monitoring/server/prometheus.nix diff --git a/morph/lib/storage.nix b/morph/lib/storage.nix index ebad3d17e17e0098f6e098d61d7c614fde91b31e..86e142286351237099337d38d03a9b54255b8246 100644 --- a/morph/lib/storage.nix +++ b/morph/lib/storage.nix @@ -1,10 +1,14 @@ # Similar to ``issuer.nix`` but for a "storage"-type system. Holes are filled # by ``customize-storage.nix``. -rec { +{ config, ...} : +let + inherit (config.grid) publicKeyPath privateKeyPath; +in { deployment = { secrets = { "ristretto-signing-key" = { destination = "/run/keys/ristretto.signing-key"; + source = "${privateKeyPath}/ristretto.signing-key"; owner.user = "root"; owner.group = "root"; permissions = "0400"; @@ -32,8 +36,6 @@ rec { # Any extra NixOS modules to load on this server. imports = [ - # Allow us to remotely trigger updates to this system. - ../../nixos/modules/deployment.nix # Bring in our module for configuring the Tahoe-LAFS service and other # Private Storage-specific things. ../../nixos/modules/private-storage.nix @@ -48,6 +50,6 @@ rec { # Yep. Turn it on. enable = true; # Give it the Ristretto signing key to support authorization. - ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination; + ristrettoSigningKeyPath = config.deployment.secrets.ristretto-signing-key.destination; }; } diff --git a/nixos/modules/issuer.nix b/nixos/modules/issuer.nix index ce1f928b2738066811425a3c7e3e3c85c03ac272..85c39c7271e9273b5e299980ebf7a46849bc9457 100644 --- a/nixos/modules/issuer.nix +++ b/nixos/modules/issuer.nix @@ -1,19 +1,12 @@ # A NixOS module which can run a Ristretto-based issuer for PrivateStorage # ZKAPs. -{ lib, pkgs, config, ... }: let +{ lib, pkgs, ourpkgs, config, ... }: let cfg = config.services.private-storage-issuer; - # Our own nixpkgs fork: - ourpkgs = import ../../nixpkgs-ps.nix {}; in { - imports = [ - # Give it a good SSH configuration. - ../../nixos/modules/ssh.nix - ]; - options = { services.private-storage-issuer.enable = lib.mkEnableOption "PrivateStorage ZKAP Issuer Service"; services.private-storage-issuer.package = lib.mkOption { - default = ourpkgs.zkapissuer.components.exes."PaymentServer-exe"; + default = ourpkgs.zkapissuer; type = lib.types.package; example = lib.literalExample "pkgs.zkapissuer.components.exes.\"PaymentServer-exe\""; description = '' @@ -115,11 +108,13 @@ in { config = let - certroot = "/var/lib/letsencrypt/live"; # We'll refer to this collection of domains by the first domain in the # list. domain = builtins.head cfg.domains; - certServiceName = "cert-${domain}"; + certServiceName = "acme-${domain}"; + # Payment server internal http port (arbitrary, non-priviledged): + internalHttpPort = "1061"; + in lib.mkIf cfg.enable { # Add a systemd service to run PaymentServer. systemd.services.zkapissuer = { @@ -127,25 +122,32 @@ in { description = "ZKAP Issuer"; wantedBy = [ "multi-user.target" ]; - # Make sure we have a certificate the first time, if we are running over - # TLS and require a certificate. - requires = lib.optional cfg.tls "${certServiceName}.service"; - - after = [ - # Make sure there is a network so we can bind to all of the - # interfaces. - "network.target" - ] ++ - # Make sure we run after the certificate is issued, if we are running - # over TLS and require a certificate. - lib.optional cfg.tls "${certServiceName}.service"; - # It really shouldn't ever exit on its own! If it does, it's a bug # we'll have to fix. Restart it and hope it doesn't happen too much # before we can fix whatever the issue is. serviceConfig.Restart = "always"; serviceConfig.Type = "simple"; + # Run w/o privileges + serviceConfig = { + DynamicUser = false; + User = "zkapissuer"; + Group = "zkapissuer"; + }; + + # Make systemd create a User/Group owned directory for PaymentServer + # state. According to the docs at + # https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RuntimeDirectory= + # "The specified directory names must be relative" ... this + # makes systemd create /var/lib/zkapissuer/ for us: + serviceConfig.StateDirectory = "zkapissuer"; + serviceConfig.StateDirectoryMode = "0750"; + + # Bail if there is still an old (root-owned) DB file on this system. + # If you hit this, and this /var/db/ file is indeed current, move it to + # /var/lib/zkapissuer/vouchers.sqlite3 and chown it to zkapissuer:zkapissuer. + unitConfig.AssertPathExists = "!/var/db/vouchers.sqlite3"; + script = let # Compute the right command line arguments to pass to it. The @@ -158,16 +160,7 @@ in { if cfg.database == "Memory" then "--database Memory" else "--database SQLite3 --database-path ${cfg.databasePath}"; - httpsArgs = - if cfg.tls - then - "--https-port 443 " + - "--https-certificate-path ${certroot}/${domain}/cert.pem " + - "--https-certificate-chain-path ${certroot}/${domain}/chain.pem " + - "--https-key-path ${certroot}/${domain}/privkey.pem" - else - # Only for automated testing. - "--http-port 80"; + httpArgs = "--http-port ${internalHttpPort}"; prefixOption = s: "--cors-origin=" + s; originStrings = map prefixOption cfg.allowedChargeOrigins; @@ -179,33 +172,21 @@ in { "--stripe-endpoint-scheme ${cfg.stripeEndpointScheme} " + "--stripe-endpoint-port ${toString cfg.stripeEndpointPort}"; in - "${cfg.package}/bin/PaymentServer-exe ${originArgs} ${issuerArgs} ${databaseArgs} ${httpsArgs} ${stripeArgs}"; + "${cfg.package.exePath} ${originArgs} ${issuerArgs} ${databaseArgs} ${httpArgs} ${stripeArgs}"; }; - # Certificate renewal. A short-lived service meant to be repeatedly - # activated to request a new certificate be issued, if the current one is - # close to expiring. - systemd.services.${certServiceName} = { - enable = cfg.tls; - description = "Certificate ${domain}"; - # Activate this unit periodically so that certbot can determine if the - # certificate expiration time is close enough to warrant a renewal - # request. - startAt = "weekly"; - - serviceConfig = { - ExecStart = - let - configArgs = "--config-dir /var/lib/letsencrypt --work-dir /var/run/letsencrypt --logs-dir /var/run/log/letsencrypt"; - in - pkgs.writeScript "cert-${domain}-start.sh" '' - #!${pkgs.runtimeShell} -e - # Register if necessary. - ${pkgs.certbot}/bin/certbot register ${configArgs} --non-interactive --agree-tos -m ${cfg.letsEncryptAdminEmail} || true - # Obtain the certificate. - ${pkgs.certbot}/bin/certbot certonly ${configArgs} --non-interactive --standalone --expand --domains ${builtins.concatStringsSep "," cfg.domains} - ''; - }; + # PaymentServer runs as this user and group by default + # Mind the comments in nixpkgs/nixos/modules/misc/ids.nix: "When adding a uid, + # make sure it doesn't match an existing gid. And don't use uids above 399!" + ids.uids.zkapissuer = 397; + ids.gids.zkapissuer = 397; + users.extraGroups.zkapissuer.gid = config.ids.gids.zkapissuer; + users.extraUsers.zkapissuer = { + uid = config.ids.uids.zkapissuer; + isNormalUser = false; + group = "zkapissuer"; + # Let PaymentServer read from keys, if necessary. + extraGroups = [ "keys" ]; }; # Open 80 and 443 for the certbot HTTP server and the PaymentServer HTTPS server. @@ -213,5 +194,38 @@ in { 80 443 ]; + + # NGINX reverse proxy + security.acme.email = cfg.letsEncryptAdminEmail; + security.acme.acceptTerms = true; + services.nginx = { + enable = true; + + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + + virtualHosts."${domain}" = { + serverAliases = builtins.tail cfg.domains; + enableACME = cfg.tls; + forceSSL = cfg.tls; + locations."/v1/" = { + # Only forward requests beginning with /v1/ so + # we pass less scanning spam on to our backend + # Want a regex instead? try locations."~ /v\d+/" + proxyPass = "http://127.0.0.1:${internalHttpPort}"; + }; + locations."/metrics" = { + # Only allow our monitoringvpn subnet + extraConfig = '' + allow 172.23.23.0/24; + deny all; + ''; + proxyPass = "http://127.0.0.1:${internalHttpPort}"; + }; + }; + }; + }; } diff --git a/nixos/modules/monitoring/server/grafana-config/services-overview.json b/nixos/modules/monitoring/server/grafana-config/payments.json similarity index 57% rename from nixos/modules/monitoring/server/grafana-config/services-overview.json rename to nixos/modules/monitoring/server/grafana-config/payments.json index 1606d2e59593fea116323dfaea25448bc4fbc9b6..6bb121e4f0ad377145956fb2d75bb0679524bd8a 100644 --- a/nixos/modules/monitoring/server/grafana-config/services-overview.json +++ b/nixos/modules/monitoring/server/grafana-config/payments.json @@ -12,11 +12,10 @@ } ] }, - "description": "RED: Requests-Errors-Duration for our services", + "description": "PaymentServer and related metrics", "editable": true, "gnetId": null, "graphTooltip": 0, - "id": 2, "links": [], "panels": [ { @@ -28,9 +27,251 @@ "x": 0, "y": 0 }, + "id": 24, + "panels": [], + "title": "Payments", + "type": "row" + }, + { + "aliasColors": { + "Attempts": "yellow", + "Successes": "green" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "description": "Our calls to the Stripe API: Attempted and successful credit card charges.", + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 1 + }, + "hiddenSeries": false, + "id": 22, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "payment_processors_stripe_charge_attempts", + "hide": false, + "interval": "", + "legendFormat": "Attempts", + "refId": "B" + }, + { + "exemplar": true, + "expr": "payment_processors_stripe_charge_successes", + "interval": "", + "legendFormat": "Successes", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Stripe", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:350", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:351", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "Redeemed vouchers": "yellow" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "description": "", + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 1 + }, + "hiddenSeries": false, + "id": 20, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "$$hashKey": "object:223", + "alias": "Redeemed vouchers", + "yaxis": 1 + }, + { + "$$hashKey": "object:230", + "alias": "Issued signatures", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "exemplar": true, + "expr": "payment_redemption_signatures_issued", + "interval": "", + "legendFormat": "Issued signatures", + "refId": "A" + }, + { + "exemplar": true, + "expr": "payment_redemption_voucher_redeemed", + "format": "time_series", + "hide": false, + "interval": "", + "legendFormat": "Redeemed vouchers", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Redemption", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:285", + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "$$hashKey": "object:286", + "decimals": null, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, "id": 18, "panels": [], - "title": "Payments v1/stripe/charge", + "title": "HTTP v1/stripe/charge", "type": "row" }, { @@ -40,14 +281,21 @@ "dashes": false, "datasource": null, "description": "HTTPS responses per second", + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 0, - "y": 1 + "y": 9 }, + "hiddenSeries": false, "id": 4, "legend": { "avg": false, @@ -62,9 +310,10 @@ "linewidth": 1, "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.5.7", "pointradius": 2, "points": false, "renderer": "flot", @@ -74,8 +323,10 @@ "steppedLine": false, "targets": [ { - "expr": "rate(http_responses_total{path=\"v1/stripe/charge\"}[5m])", + "exemplar": true, + "expr": "rate(http_responses_total{path=\"v1/stripe/charge\", instance=\"payments\"}[5m])", "instant": false, + "interval": "", "legendFormat": "{{status}}", "refId": "A" } @@ -84,7 +335,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "v1/stripe/charge RPS", + "title": "Requests per second", "tooltip": { "shared": true, "sort": 0, @@ -100,14 +351,16 @@ }, "yaxes": [ { + "$$hashKey": "object:452", "format": "short", "label": null, "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true }, { + "$$hashKey": "object:453", "format": "short", "label": null, "logBase": 1, @@ -128,14 +381,21 @@ "dashes": false, "datasource": null, "description": "", + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 8, - "y": 1 + "y": 9 }, + "hiddenSeries": false, "id": 15, "legend": { "avg": false, @@ -150,9 +410,10 @@ "linewidth": 1, "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, - "percentage": true, + "percentage": false, + "pluginVersion": "7.5.7", "pointradius": 2, "points": false, "renderer": "flot", @@ -162,12 +423,16 @@ "steppedLine": false, "targets": [ { - "expr": "sum(http_responses_total{path=\"v1/stripe/charge\", status=\"4XX\"}) / sum(http_responses_total{path=\"v1/redeem\"})", + "exemplar": true, + "expr": "sum(http_responses_total{path=\"v1/stripe/charge\", status=\"4XX\"}) / sum(http_responses_total{path=\"v1/stripe/charge\"})", + "interval": "", "legendFormat": "Client error (4XX) rate", "refId": "A" }, { - "expr": "sum(http_responses_total{path=\"v1/stripe/charge\", status=\"5XX\"}) / sum(http_responses_total{path=\"v1/redeem\"})", + "exemplar": true, + "expr": "sum(http_responses_total{path=\"v1/stripe/charge\", status=\"5XX\"}) / sum(http_responses_total{path=\"v1/stripe/charge\"})", + "interval": "", "legendFormat": "Server error (5XX) rate", "refId": "B" } @@ -176,7 +441,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "v1/stripe/charge error rate", + "title": "Error rate", "tooltip": { "shared": true, "sort": 0, @@ -192,14 +457,16 @@ }, "yaxes": [ { + "$$hashKey": "object:576", "format": "percentunit", "label": null, "logBase": 1, - "max": "100", + "max": "1", "min": "0", "show": true }, { + "$$hashKey": "object:577", "format": "percent", "label": null, "logBase": 1, @@ -214,21 +481,33 @@ } }, { - "aliasColors": {}, + "aliasColors": { + "=< 0.1s": "blue", + "=< 1s": "green", + "=< 5s": "yellow", + "> 5s": "orange" + }, "bars": false, "cacheTimeout": null, "dashLength": 10, "dashes": false, "datasource": null, - "description": "Requests taking longer than 1 s, between 1 sec and 10 msec, and 10 msec and below", + "description": "Request durations, stacked", + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, "fill": 2, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 16, - "y": 1 + "y": 9 }, + "hiddenSeries": false, "id": 12, "legend": { "avg": false, @@ -244,51 +523,68 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, - "pluginVersion": "6.4.3", + "pluginVersion": "7.5.7", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, - "stack": false, + "stack": true, "steppedLine": false, "targets": [ { - "expr": "http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"0.01\"}", + "exemplar": true, + "expr": "http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"0.1\"}", "format": "time_series", "hide": false, "instant": false, + "interval": "", "intervalFactor": 1, - "legendFormat": "=< 0.01s", + "legendFormat": "=< 0.1s", "refId": "A" }, { - "expr": "http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"1.0\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"0.01\"}", + "exemplar": true, + "expr": "http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"1.0\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"0.1\"}", "format": "time_series", "hide": false, "instant": false, + "interval": "", "intervalFactor": 1, "legendFormat": "=< 1s", "refId": "D" }, { - "expr": "http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"+Inf\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"1.0\"}", + "exemplar": true, + "expr": "http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"5.0\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"1.0\"}", "format": "time_series", "hide": false, "instant": false, + "interval": "", "intervalFactor": 1, - "legendFormat": "> 1s", + "legendFormat": "=< 5s", "refId": "B" + }, + { + "exemplar": true, + "expr": "http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"+Inf\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/stripe/charge\", le=\"5.0\"}", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "> 5s", + "refId": "C" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "v1/stripe/charge durations", + "title": "Durations", "tooltip": { "shared": true, "sort": 0, @@ -304,14 +600,16 @@ }, "yaxes": [ { + "$$hashKey": "object:625", "format": "short", "label": null, "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true }, { + "$$hashKey": "object:626", "format": "short", "label": null, "logBase": 1, @@ -332,11 +630,11 @@ "h": 1, "w": 24, "x": 0, - "y": 8 + "y": 16 }, "id": 11, "panels": [], - "title": "Payments v1/redeem", + "title": "HTTP v1/redeem", "type": "row" }, { @@ -346,14 +644,21 @@ "dashes": false, "datasource": null, "description": "HTTPS responses per second", + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 0, - "y": 9 + "y": 17 }, + "hiddenSeries": false, "id": 2, "legend": { "avg": false, @@ -368,9 +673,10 @@ "linewidth": 1, "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.5.7", "pointradius": 2, "points": false, "renderer": "flot", @@ -391,7 +697,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "v1/redeem RPS", + "title": "Requests per second", "tooltip": { "shared": true, "sort": 0, @@ -407,14 +713,16 @@ }, "yaxes": [ { + "$$hashKey": "object:751", "format": "short", "label": null, "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true }, { + "$$hashKey": "object:752", "format": "short", "label": null, "logBase": 1, @@ -434,14 +742,21 @@ "dashLength": 10, "dashes": false, "datasource": null, + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 8, - "y": 9 + "y": 17 }, + "hiddenSeries": false, "id": 16, "legend": { "avg": false, @@ -456,9 +771,10 @@ "linewidth": 1, "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, - "percentage": true, + "percentage": false, + "pluginVersion": "7.5.7", "pointradius": 2, "points": false, "renderer": "flot", @@ -482,7 +798,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "v1/redeem error rate", + "title": "Error rate", "tooltip": { "shared": true, "sort": 0, @@ -498,14 +814,16 @@ }, "yaxes": [ { + "$$hashKey": "object:804", "format": "percentunit", "label": null, "logBase": 1, - "max": "100", + "max": "1", "min": "0", "show": true }, { + "$$hashKey": "object:805", "format": "percent", "label": null, "logBase": 1, @@ -520,21 +838,33 @@ } }, { - "aliasColors": {}, + "aliasColors": { + "=< 0.1s": "blue", + "=< 1s": "green", + "=< 5s": "yellow", + "> 5s": "orange" + }, "bars": false, "cacheTimeout": null, "dashLength": 10, "dashes": false, "datasource": null, - "description": "Requests taking longer than 1 s, between 1 sec and 10 msec, and 10 msec and below", + "description": "Request durations, stacked.", + "fieldConfig": { + "defaults": { + "links": [] + }, + "overrides": [] + }, "fill": 2, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 16, - "y": 9 + "y": 17 }, + "hiddenSeries": false, "id": 13, "legend": { "avg": false, @@ -550,51 +880,68 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, - "pluginVersion": "6.4.3", + "pluginVersion": "7.5.7", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, - "stack": false, + "stack": true, "steppedLine": false, "targets": [ { - "expr": "http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"0.01\"}", + "exemplar": true, + "expr": "http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"0.1\"}", "format": "time_series", "hide": false, "instant": false, + "interval": "", "intervalFactor": 1, - "legendFormat": "=< 0.01s", + "legendFormat": "=< 0.1s", "refId": "A" }, { - "expr": "http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"1.0\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"0.01\"}", + "exemplar": true, + "expr": "http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"1.0\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"0.1\"}", "format": "time_series", "hide": false, "instant": false, + "interval": "", "intervalFactor": 1, "legendFormat": "=< 1s", "refId": "D" }, { - "expr": "http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"+Inf\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"1.0\"}", + "exemplar": true, + "expr": "http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"5.0\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"1.0\"}", "format": "time_series", "hide": false, "instant": false, + "interval": "", "intervalFactor": 1, - "legendFormat": "> 1s", + "legendFormat": "=< 5s", "refId": "B" + }, + { + "exemplar": true, + "expr": "http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"+Inf\"} - ignoring(le) http_request_duration_seconds_bucket{path=\"v1/redeem\", le=\"5.0\"}", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "> 5s", + "refId": "C" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "v1/redeem durations", + "title": "Durations", "tooltip": { "shared": true, "sort": 0, @@ -610,14 +957,16 @@ }, "yaxes": [ { + "$$hashKey": "object:853", "format": "short", "label": null, "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true }, { + "$$hashKey": "object:854", "format": "short", "label": null, "logBase": 1, @@ -632,8 +981,8 @@ } } ], - "refresh": "", - "schemaVersion": 20, + "refresh": "5m", + "schemaVersion": 27, "style": "dark", "tags": [], "templating": { @@ -658,7 +1007,7 @@ ] }, "timezone": "", - "title": "Services overview", - "uid": "ServicesOverview", - "version": 6 + "title": "Payments", + "uid": "Payments", + "version": 1 } diff --git a/nixos/modules/monitoring/server/grafana-config/resources-overview.json b/nixos/modules/monitoring/server/grafana-config/resources-overview.json index 8cf342514143d84de1263a3d6debaf8e40b4c922..cb5bc91da7c3adbb1c9377473b053d31d53550f0 100644 --- a/nixos/modules/monitoring/server/grafana-config/resources-overview.json +++ b/nixos/modules/monitoring/server/grafana-config/resources-overview.json @@ -1279,7 +1279,7 @@ "to": "now" }, "timepicker": {}, - "timezone": "utc", + "timezone": "", "title": "Resources overview", "uid": "ResourcesOverview", "version": 1 diff --git a/nixos/modules/monitoring/server/grafana.nix b/nixos/modules/monitoring/server/grafana.nix index 2fd9e7f7c83217afc4943e644f6d3161e56c49f9..c23150238241db561bae52aa50e4878b6961f9e6 100644 --- a/nixos/modules/monitoring/server/grafana.nix +++ b/nixos/modules/monitoring/server/grafana.nix @@ -86,6 +86,8 @@ in { extraOptions = { # Defend against DNS rebinding attacks. SERVER_ENFORCE_DOMAIN = "true"; + # Same time zone for all users by default + DATE_FORMATS_DEFAULT_TIMEZONE = "UTC"; }; auth = { diff --git a/nixos/modules/packages.nix b/nixos/modules/packages.nix new file mode 100644 index 0000000000000000000000000000000000000000..d6518dcf290c27b95e3428434623a63cfbdb8e19 --- /dev/null +++ b/nixos/modules/packages.nix @@ -0,0 +1,8 @@ +# A NixOS module which exposes custom packages to other modules. +{ pkgs, ...}: +{ + config = { + # Expose `nixos/pkgs` as a new module argument `ourpkgs`. + _module.args.ourpkgs = pkgs.callPackage ../../nixos/pkgs {}; + }; +} diff --git a/nixos/modules/private-storage.nix b/nixos/modules/private-storage.nix index fa5fea837c544e66ae8811a2e3c468a67a18759e..c119a3d3417f7d4b7ec07c5652b65122dc5fce12 100644 --- a/nixos/modules/private-storage.nix +++ b/nixos/modules/private-storage.nix @@ -1,6 +1,6 @@ # A NixOS module which can instantiate a Tahoe-LAFS storage server in the # preferred configuration for the Private Storage grid. -{ pkgs, lib, config, ... }: +{ pkgs, ourpkgs, lib, config, ... }: let # Grab the configuration for this module for convenient access below. cfg = config.services.private-storage; @@ -8,9 +8,6 @@ let # TODO: This path copied from tahoe.nix. tahoe-base = "/var/db/tahoe-lafs"; - # Our own nixpkgs fork: - ourpkgs = import ../../nixpkgs-ps.nix {}; - # The full path to the directory where the storage server will write # incident reports. incidents-dir = "${tahoe-base}/${storage-node-name}/logs/incidents"; @@ -30,8 +27,6 @@ let in { imports = [ - # Give it a good SSH configuration. - ./ssh.nix # Load our tahoe-lafs module. It is configurable in the way I want it to # be configurable. ./tahoe.nix diff --git a/nixos/modules/tests/tahoe.nix b/nixos/modules/tests/tahoe.nix deleted file mode 100644 index df7acdf3cde3e8101a1119dbce127b17a68ef589..0000000000000000000000000000000000000000 --- a/nixos/modules/tests/tahoe.nix +++ /dev/null @@ -1,72 +0,0 @@ -{ ... }: { - nodes = { - storage = { config, pkgs, ... }: { - imports = [ - ../tahoe.nix - ]; - - services.tahoe.nodes.storage = { - package = pkgs.privatestorage; - sections = { - node = { - nickname = "storage"; - "web.port" = "tcp:4000:interface=127.0.0.1"; - "tub.port" = "tcp:4001"; - "tub.location" = "tcp:127.0.0.1:4001"; - }; - storage = { - enabled = true; - }; - }; - }; - }; - }; - testScript = '' - startAll; - - # After the service starts, destroy the "created" marker to force it to - # re-create its internal state. - $storage->waitForOpenPort(4001); - $storage->succeed("systemctl stop tahoe.storage"); - $storage->succeed("rm /var/db/tahoe-lafs/storage.created"); - $storage->succeed("systemctl start tahoe.storage"); - - # After it starts up again, verify it has consistent internal state and a - # backup of the prior state. - $storage->waitForOpenPort(4001); - $storage->succeed("[ -e /var/db/tahoe-lafs/storage ]"); - $storage->succeed("[ -e /var/db/tahoe-lafs/storage.created ]"); - $storage->succeed("[ -e /var/db/tahoe-lafs/storage.1 ]"); - $storage->succeed("[ -e /var/db/tahoe-lafs/storage.1/private/node.privkey ]"); - $storage->succeed("[ -e /var/db/tahoe-lafs/storage.1/private/node.pem ]"); - $storage->succeed("[ ! -e /var/db/tahoe-lafs/storage.2 ]"); - - # Stop it again, once again destroy the "created" marker, and this time also - # jam some partial state in the way that will need cleanup. - $storage->succeed("systemctl stop tahoe.storage"); - $storage->succeed("rm /var/db/tahoe-lafs/storage.created"); - $storage->succeed("mkdir -p /var/db/tahoe-lafs/storage.atomic/partial"); - eval { - $storage->succeed("systemctl start tahoe.storage"); - 1; - } or do { - my ($x, $y) = $storage->execute("journalctl -u tahoe.storage"); - $storage->log($y); - die $@; - }; - - # After it starts up again, verify it has consistent internal state and - # backups of the prior two states. It also has no copy of the inconsistent - # state because it could never have been used. - $storage->waitForOpenPort(4001); - $storage->succeed("[ -e /var/db/tahoe-lafs/storage ]"); - $storage->succeed("[ -e /var/db/tahoe-lafs/storage.created ]"); - $storage->succeed("[ -e /var/db/tahoe-lafs/storage.1 ]"); - $storage->succeed("[ -e /var/db/tahoe-lafs/storage.2 ]"); - $storage->succeed("[ -e /var/db/tahoe-lafs/storage.2/private/node.privkey ]"); - $storage->succeed("[ -e /var/db/tahoe-lafs/storage.2/private/node.pem ]"); - $storage->succeed("[ ! -e /var/db/tahoe-lafs/storage.atomic ]"); - $storage->succeed("[ ! -e /var/db/tahoe-lafs/storage/partial ]"); - $storage->succeed("[ ! -e /var/db/tahoe-lafs/storage.3 ]"); - ''; -} diff --git a/nixos/modules/update-deployment b/nixos/modules/update-deployment index d8d32ff64eb52123be448ed598d00ab2bc1850da..1c8960588f418e57eeaadb7ad29db4285369cbdd 100755 --- a/nixos/modules/update-deployment +++ b/nixos/modules/update-deployment @@ -13,14 +13,10 @@ shift # configuration that controls what value is actually passed when an update is # triggered. case "${GRIDNAME}" in - "local") + "local"|"testing") BRANCH="develop" ;; - "testing") - BRANCH="staging" - ;; - "production") BRANCH="production" ;; @@ -50,10 +46,10 @@ fi # Get us to a pristine checkout of the right branch. git -C "${CHECKOUT}" reset --hard "origin/${BRANCH}" -# If we happen to be on the local grid then fix the undefined key. +# If we happen to be on the local grid then add the required user.nix file +# containing ssh-keys. if [ "${GRIDNAME}" = "local" ]; then - KEY="$(cat /etc/ssh/authorized_keys.d/vagrant)" - sed -i "s_undefined_\"${KEY}\"_" "${CHECKOUT}"/morph/grid/${GRIDNAME}/public-keys/users.nix + echo "import /etc/nixos/ssh-users.nix" > "${CHECKOUT}"/morph/grid/"${GRIDNAME}"/public-keys/users.nix fi # Compute a log message explaining what we're doing. diff --git a/nixos/pkgs/default.nix b/nixos/pkgs/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..3d534430377cb5fbbf0739d60a8a7ca9bb0419f6 --- /dev/null +++ b/nixos/pkgs/default.nix @@ -0,0 +1,24 @@ +# Expose all our locally defined packages as attributes. +# In `gridlib.base`, we expose this as a new `ourpkgs` module argument. +# To access this directly, you can call this as:: +# +# pkgs.callPackage ./nixos/pkgs +{buildPlatform, hostPlatform, callPackage}: +let + # Our own nixpkgs fork: + ourpkgs = import ../../nixpkgs-ps.nix { + # Ensure that the fork is configured for the same system + # as we were called with. + localSystem = buildPlatform; + crossSystem = hostPlatform; + # Ensure that configuration of the system where this runs + # doesn't leak into what we build. + # See https://github.com/NixOS/nixpkgs/issues/62513 + config = {}; + overlays = []; + }; +in +{ + zkapissuer = callPackage ./zkapissuer {}; + inherit (ourpkgs) privatestorage leasereport; +} diff --git a/nixos/pkgs/zkapissuer/default.nix b/nixos/pkgs/zkapissuer/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..b4f90d3582cd686fbdf62a6267cb1070c05e9c57 --- /dev/null +++ b/nixos/pkgs/zkapissuer/default.nix @@ -0,0 +1,6 @@ +{ callPackage }: +let + repo = callPackage ./repo.nix { }; + PaymentServer = (import "${repo}/nix").PaymentServer; +in + PaymentServer.components.exes."PaymentServer-exe" diff --git a/nixos/pkgs/zkapissuer/repo.nix b/nixos/pkgs/zkapissuer/repo.nix new file mode 100644 index 0000000000000000000000000000000000000000..6646a2e32eb8e5a747e4491ce43f706fee65724c --- /dev/null +++ b/nixos/pkgs/zkapissuer/repo.nix @@ -0,0 +1,7 @@ +{ fetchFromGitHub }: +fetchFromGitHub { + owner = "PrivateStorageio"; + repo = "PaymentServer"; + rev = "ff30e85c231a3b5ad76426bbf8801f8f76884367"; + sha256 = "1spz19f5z96shmfpazj0rv6877xvchf3gl49a4xahjbbsz39x34x"; +} diff --git a/nixos/system-tests.nix b/nixos/system-tests.nix index 5f51d01dd57267b75b3742c76c03c1393676d426..73b6665ab91e4d9a8a2200fb0eec7ff596f79b39 100644 --- a/nixos/system-tests.nix +++ b/nixos/system-tests.nix @@ -1,7 +1,7 @@ # The overall system test suite for PrivateStorageio NixOS configuration. let - pkgs = import ../nixpkgs-ps.nix { }; + pkgs = import ../nixpkgs-2105.nix { }; in { - private-storage = pkgs.nixosTest ./modules/tests/private-storage.nix; - tahoe = pkgs.nixosTest ./modules/tests/tahoe.nix; + private-storage = pkgs.nixosTest ./tests/private-storage.nix; + tahoe = pkgs.nixosTest ./tests/tahoe.nix; } diff --git a/nixos/modules/tests/exercise-storage.py b/nixos/tests/exercise-storage.py similarity index 100% rename from nixos/modules/tests/exercise-storage.py rename to nixos/tests/exercise-storage.py diff --git a/nixos/modules/tests/get-passes.py b/nixos/tests/get-passes.py similarity index 100% rename from nixos/modules/tests/get-passes.py rename to nixos/tests/get-passes.py diff --git a/nixos/modules/tests/node.pem b/nixos/tests/node.pem similarity index 100% rename from nixos/modules/tests/node.pem rename to nixos/tests/node.pem diff --git a/nixos/modules/tests/private-storage.nix b/nixos/tests/private-storage.nix similarity index 51% rename from nixos/modules/tests/private-storage.nix rename to nixos/tests/private-storage.nix index 353abc891fafd1cc988e47a1befa530a012470dc..6fb85a6713b4668ef4bdfa239480485bfbb52a18 100644 --- a/nixos/modules/tests/private-storage.nix +++ b/nixos/tests/private-storage.nix @@ -14,7 +14,7 @@ let "ssh" "-oStrictHostKeyChecking=no" "-i" "/tmp/ssh_key" "${username}@${hostname}" ":" ]; - # Separate helper programs so we can write as little perl inside a string + # Separate helper programs so we can write as little python inside a string # inside a nix expression as possible. run-introducer = ./run-introducer.py; run-client = ./run-client.py; @@ -72,33 +72,30 @@ let networking.dhcpcd.enable = false; }; - # Return a Perl program fragment to run a shell command on one of the nodes. + # Return a python program fragment to run a shell command on one of the nodes. # The first argument is the name of the node. The second is a list of the # argv to run. # - # The program's output is piped to systemd-cat and the Perl fragment + # The program's output is piped to systemd-cat and the python fragment # evaluates to success if the command exits with a success status. runOnNode = node: argv: let command = builtins.concatStringsSep " " argv; in - " - \$${node}->succeed('set -eo pipefail; ${command} | systemd-cat'); - # succeed() is not success but 1 is. - 1; - "; + "${node}.succeed('set -eo pipefail; ${command} | systemd-cat')"; in { # https://nixos.org/nixos/manual/index.html#sec-nixos-tests # https://nixos.mayflower.consulting/blog/2019/07/11/leveraging-nixos-tests-in-your-project/ nodes = rec { # Get a machine where we can run a Tahoe-LAFS client node. client = - { config, pkgs, ... }: - { environment.systemPackages = [ + { config, pkgs, ourpkgs, ... }: + { imports = [ ../modules/packages.nix ]; + environment.systemPackages = [ pkgs.daemonize # A Tahoe-LAFS configuration capable of using the right storage # plugin. - pkgs.privatestorage + ourpkgs.privatestorage # Support for the tests we'll run. (pkgs.python3.withPackages (ps: [ ps.requests ps.hyperlink ])) ]; @@ -111,7 +108,9 @@ in { storage = { config, pkgs, ... }: { imports = - [ ../private-storage.nix + [ ../modules/packages.nix + ../modules/private-storage.nix + ../modules/ssh.nix ]; services.private-storage = { enable = true; @@ -128,7 +127,9 @@ in { issuer = { config, pkgs, ... }: { imports = - [ ../issuer.nix + [ ../modules/packages.nix + ../modules/issuer.nix + ../modules/ssh.nix ]; services.private-storage.sshUsers = sshUsers; @@ -174,138 +175,135 @@ in { }; }; - # Test the machines with a Perl program (sobbing). - testScript = - '' - # Boot the VMs. We used to do them all in parallel but the boot - # sequence got flaky at some point for some reason I don't - # understand. :/ It might be related to this: - # - # https://discourse.nixos.org/t/nixos-ppc64le-vm-does-not-have-dev-vda-device/11548/9 - # - # See <nixpkgs/nixos/modules/virtualisation/qemu-vm.nix> for the Nix - # that constructs the QEMU command that gets run. - # - # Boot them one at a time for now. - $issuer->connect(); - $introducer->connect(); - $storage->connect(); - $client->connect(); - $api_stripe_com->connect(); - - # The issuer and the storage server should accept SSH connections. This - # doesn't prove it is so but if it fails it's a pretty good indication - # it isn't so. - $storage->waitForOpenPort(22); - ${runOnNode "issuer" (ssh "probeuser" "storage")} - ${runOnNode "issuer" (ssh "root" "storage")} - $issuer->waitForOpenPort(22); - ${runOnNode "storage" (ssh "probeuser" "issuer")} - ${runOnNode "storage" (ssh "root" "issuer")} - - # Set up a Tahoe-LAFS introducer. - $introducer->copyFileFromHost( - '${pemFile}', - '/tmp/node.pem' - ); - - eval { + # Test the machines with a Python program. + testScript = '' + # Boot the VMs. We used to do them all in parallel but the boot + # sequence got flaky at some point for some reason I don't + # understand. :/ It might be related to this: + # + # https://discourse.nixos.org/t/nixos-ppc64le-vm-does-not-have-dev-vda-device/11548/9 + # + # See <nixpkgs/nixos/modules/virtualisation/qemu-vm.nix> for the Nix + # that constructs the QEMU command that gets run. + # + # Boot them one at a time for now. + issuer.connect() + introducer.connect() + storage.connect() + client.connect() + api_stripe_com.connect() + + # The issuer and the storage server should accept SSH connections. This + # doesn't prove it is so but if it fails it's a pretty good indication + # it isn't so. + storage.wait_for_open_port(22) + ${runOnNode "issuer" (ssh "probeuser" "storage")} + ${runOnNode "issuer" (ssh "root" "storage")} + issuer.wait_for_open_port(22) + ${runOnNode "storage" (ssh "probeuser" "issuer")} + ${runOnNode "storage" (ssh "root" "issuer")} + + # Set up a Tahoe-LAFS introducer. + introducer.copy_from_host('${pemFile}', '/tmp/node.pem') + + try: ${runOnNode "introducer" [ run-introducer "/tmp/node.pem" (toString introducerPort) introducerFURL ]} - } or do { - my ($code, $log) = $introducer->execute('cat /tmp/stdout /tmp/stderr'); - $introducer->log($log); - die $@; - }; - - # - # Get a Tahoe-LAFS storage server up. - # - my ($code, $version) = $storage->execute('tahoe --version'); - $storage->log($version); - - # The systemd unit should reach the running state. - $storage->waitForUnit('tahoe.storage.service'); - - # Some while after that the Tahoe-LAFS node should listen on the web API - # port. The port number here has to agree with the port number set in - # the private-storage.nix module. - $storage->waitForOpenPort(3456); - - # Once the web API is listening it should be possible to scrape some - # status from the node if it is really working. - $storage->succeed('tahoe -d /var/db/tahoe-lafs/storage status'); - - # It should have Eliot logging turned on as well. - $storage->succeed('[ -e /var/db/tahoe-lafs/storage/logs/eliot.json ]'); - - # - # Storage appears to be working so try to get a client to speak with it. - # - ${runOnNode "client" [ run-client "/tmp/client" introducerFURL issuerURL ]} - $client->waitForOpenPort(3456); - - # Make sure the fake Stripe API server is ready for requests. - eval { - $api_stripe_com->waitForUnit("api.stripe.com"); - 1; - } or do { - my ($code, $log) = $api_stripe_com->execute('journalctl -u api.stripe.com'); - $api_stripe_com->log($log); - die $@; - }; - - # Get some ZKAPs from the issuer. - eval { - ${runOnNode "client" [ - get-passes - "http://127.0.0.1:3456" - "/tmp/client/private/api_auth_token" - issuerURL - voucher - ]} - } or do { - my ($code, $log) = $client->execute('cat /tmp/stdout /tmp/stderr'); - $client->log($log); - - # Dump the fake Stripe API server logs, too, since the error may arise - # from a PaymentServer/Stripe interaction. - my ($code, $log) = $api_stripe_com->execute('journalctl -u api.stripe.com'); - $api_stripe_com->log($log); - die $@; - }; - - # The client should be prepped now. Make it try to use some storage. - eval { - ${runOnNode "client" [ exercise-storage "/tmp/client" ]} - } or do { - my ($code, $log) = $client->execute('cat /tmp/stdout /tmp/stderr'); - $client->log($log); - die $@; - }; - - # It should be possible to restart the storage service without the - # storage node fURL changing. - eval { - my $furlfile = '/var/db/tahoe-lafs/storage/private/storage-plugin.privatestorageio-zkapauthz-v1.furl'; - my $before = $storage->execute('cat ' . $furlfile); - ${runOnNode "storage" [ "systemctl" "restart" "tahoe.storage" ]} - my $after = $storage->execute('cat ' . $furlfile); - if ($before != $after) { - die 'fURL changes after storage node restart'; - } - 1; - } or do { - my ($code, $log) = $storage->execute('cat /tmp/stdout /tmp/stderr'); - $storage->log($log); - die $@; - }; - - # The client should actually still work, too. - eval { - ${runOnNode "client" [ exercise-storage "/tmp/client" ]} - } or do { - my ($code, $log) = $client->execute('cat /tmp/stdout /tmp/stderr'); - $client->log($log); - die $@; - }; - ''; } + except: + code, log = introducer.execute('cat /tmp/stdout /tmp/stderr') + introducer.log(log) + raise + + # + # Get a Tahoe-LAFS storage server up. + # + code, version = storage.execute('tahoe --version') + storage.log(version) + + # The systemd unit should reach the running state. + storage.wait_for_unit('tahoe.storage.service') + + # Some while after that the Tahoe-LAFS node should listen on the web API + # port. The port number here has to agree with the port number set in + # the private-storage.nix module. + storage.wait_for_open_port(3456) + + # Once the web API is listening it should be possible to scrape some + # status from the node if it is really working. + storage.succeed('tahoe -d /var/db/tahoe-lafs/storage status') + + # It should have Eliot logging turned on as well. + storage.succeed('[ -e /var/db/tahoe-lafs/storage/logs/eliot.json ]') + + # + # Storage appears to be working so try to get a client to speak with it. + # + ${runOnNode "client" [ run-client "/tmp/client" introducerFURL issuerURL ]} + client.wait_for_open_port(3456) + + # Make sure the fake Stripe API server is ready for requests. + try: + api_stripe_com.wait_for_unit("api.stripe.com") + except: + code, log = api_stripe_com.execute('journalctl -u api.stripe.com') + api_stripe_com.log(log) + raise + + # Get some ZKAPs from the issuer. + try: + ${runOnNode "client" [ + get-passes + "http://127.0.0.1:3456" + "/tmp/client/private/api_auth_token" + issuerURL + voucher + ]} + except: + code, log = client.execute('cat /tmp/stdout /tmp/stderr'); + client.log(log) + + # Dump the fake Stripe API server logs, too, since the error may arise + # from a PaymentServer/Stripe interaction. + code, log = api_stripe_com.execute('journalctl -u api.stripe.com') + api_stripe_com.log(log) + raise + + # The client should be prepped now. Make it try to use some storage. + try: + ${runOnNode "client" [ exercise-storage "/tmp/client" ]} + except: + code, log = client.execute('cat /tmp/stdout /tmp/stderr') + client.log(log) + raise + + # It should be possible to restart the storage service without the + # storage node fURL changing. + try: + furlfile = '/var/db/tahoe-lafs/storage/private/storage-plugin.privatestorageio-zkapauthz-v1.furl' + before = storage.execute('cat ' + furlfile) + ${runOnNode "storage" [ "systemctl" "restart" "tahoe.storage" ]} + after = storage.execute('cat ' + furlfile) + if (before != after): + raise Exception('fURL changes after storage node restart') + except: + code, log = storage.execute('cat /tmp/stdout /tmp/stderr') + storage.log(log) + raise + + # The client should actually still work, too. + try: + ${runOnNode "client" [ exercise-storage "/tmp/client" ]} + except: + code, log = client.execute('cat /tmp/stdout /tmp/stderr') + client.log(log) + raise + + # The issuer metrics should be accessible from the monitoring network. + issuer.execute('ifconfig lo:fauxvpn 172.23.23.2/24') + issuer.wait_until_succeeds("nc -z 172.23.23.2 80") + issuer.succeed('curl --silent --insecure --fail --output /dev/null http://172.23.23.2/metrics') + # The issuer metrics should NOT be accessible from any other network. + issuer.fail('curl --silent --insecure --fail --output /dev/null http://localhost/metrics') + client.fail('curl --silent --insecure --fail --output /dev/null http://issuer/metrics') + issuer.execute('ifconfig lo:fauxvpn down') + ''; +} diff --git a/nixos/modules/tests/probeuser_ed25519 b/nixos/tests/probeuser_ed25519 similarity index 100% rename from nixos/modules/tests/probeuser_ed25519 rename to nixos/tests/probeuser_ed25519 diff --git a/nixos/modules/tests/probeuser_ed25519.pub b/nixos/tests/probeuser_ed25519.pub similarity index 100% rename from nixos/modules/tests/probeuser_ed25519.pub rename to nixos/tests/probeuser_ed25519.pub diff --git a/nixos/modules/tests/run-client.py b/nixos/tests/run-client.py similarity index 83% rename from nixos/modules/tests/run-client.py rename to nixos/tests/run-client.py index bcd01e1b04a9b41cb7aa75f29fd3247d995d2527..e6cde321bdeb8a2b2493c984cce116a0287b16d1 100755 --- a/nixos/modules/tests/run-client.py +++ b/nixos/tests/run-client.py @@ -33,6 +33,10 @@ def main(): config.add_section(u"storageclient.plugins.privatestorageio-zkapauthz-v1") config.set(u"storageclient.plugins.privatestorageio-zkapauthz-v1", u"redeemer", u"ristretto") config.set(u"storageclient.plugins.privatestorageio-zkapauthz-v1", u"ristretto-issuer-root-url", issuerURL) + # This has to agree with the PaymentServer configuration at the configured + # issuer location. Presently PaymentServer has 50000 hard-coded as the + # correct value. + config.set(u"storageclient.plugins.privatestorageio-zkapauthz-v1", u"default-token-count", u"50000") with open("/tmp/client/tahoe.cfg", "wt") as cfg: config.write(cfg) diff --git a/nixos/modules/tests/run-introducer.py b/nixos/tests/run-introducer.py similarity index 100% rename from nixos/modules/tests/run-introducer.py rename to nixos/tests/run-introducer.py diff --git a/nixos/modules/tests/stripe-api-double.py b/nixos/tests/stripe-api-double.py similarity index 100% rename from nixos/modules/tests/stripe-api-double.py rename to nixos/tests/stripe-api-double.py diff --git a/nixos/tests/tahoe.nix b/nixos/tests/tahoe.nix new file mode 100644 index 0000000000000000000000000000000000000000..e39fd6d3fcb776e8e5215bb1264e08e2b7306c1f --- /dev/null +++ b/nixos/tests/tahoe.nix @@ -0,0 +1,72 @@ +{ ... }: + { + nodes = { + storage = { config, pkgs, ourpkgs, ... }: { + imports = [ + ../modules/packages.nix + ../modules/tahoe.nix + ]; + + services.tahoe.nodes.storage = { + package = ourpkgs.privatestorage; + sections = { + node = { + nickname = "storage"; + "web.port" = "tcp:4000:interface=127.0.0.1"; + "tub.port" = "tcp:4001"; + "tub.location" = "tcp:127.0.0.1:4001"; + }; + storage = { + enabled = true; + }; + }; + }; + }; + }; + testScript = '' + start_all() + + # After the service starts, destroy the "created" marker to force it to + # re-create its internal state. + storage.wait_for_open_port(4001) + storage.succeed("systemctl stop tahoe.storage") + storage.succeed("rm /var/db/tahoe-lafs/storage.created") + storage.succeed("systemctl start tahoe.storage") + + # After it starts up again, verify it has consistent internal state and a + # backup of the prior state. + storage.wait_for_open_port(4001) + storage.succeed("[ -e /var/db/tahoe-lafs/storage ]") + storage.succeed("[ -e /var/db/tahoe-lafs/storage.created ]") + storage.succeed("[ -e /var/db/tahoe-lafs/storage.1 ]") + storage.succeed("[ -e /var/db/tahoe-lafs/storage.1/private/node.privkey ]") + storage.succeed("[ -e /var/db/tahoe-lafs/storage.1/private/node.pem ]") + storage.succeed("[ ! -e /var/db/tahoe-lafs/storage.2 ]") + + # Stop it again, once again destroy the "created" marker, and this time also + # jam some partial state in the way that will need cleanup. + storage.succeed("systemctl stop tahoe.storage") + storage.succeed("rm /var/db/tahoe-lafs/storage.created") + storage.succeed("mkdir -p /var/db/tahoe-lafs/storage.atomic/partial") + try: + storage.succeed("systemctl start tahoe.storage") + except: + x, y = storage.execute("journalctl -u tahoe.storage") + storage.log(y) + raise + + # After it starts up again, verify it has consistent internal state and + # backups of the prior two states. It also has no copy of the inconsistent + # state because it could never have been used. + storage.wait_for_open_port(4001) + storage.succeed("[ -e /var/db/tahoe-lafs/storage ]") + storage.succeed("[ -e /var/db/tahoe-lafs/storage.created ]") + storage.succeed("[ -e /var/db/tahoe-lafs/storage.1 ]") + storage.succeed("[ -e /var/db/tahoe-lafs/storage.2 ]") + storage.succeed("[ -e /var/db/tahoe-lafs/storage.2/private/node.privkey ]") + storage.succeed("[ -e /var/db/tahoe-lafs/storage.2/private/node.pem ]") + storage.succeed("[ ! -e /var/db/tahoe-lafs/storage.atomic ]") + storage.succeed("[ ! -e /var/db/tahoe-lafs/storage/partial ]") + storage.succeed("[ ! -e /var/db/tahoe-lafs/storage.3 ]") + ''; +} diff --git a/nixpkgs-2105.json b/nixpkgs-2105.json index 76950db1870cb62d68e655f5ca4be90f3fcbf6be..f79aa88bc0bb97b26c4668ac1d2c4efcdb25b9fb 100644 --- a/nixpkgs-2105.json +++ b/nixpkgs-2105.json @@ -1,4 +1,5 @@ -{ "name": "release2105" -, "url": "https://releases.nixos.org/nixos/21.05/nixos-21.05.804.5de44c15758/nixexprs.tar.xz" -, "sha256": "002zvc16hyrbs0icx1qj255c9dqjpdxx4bhhfjndlj3kwn40by0m" +{ + "name": "release2105", + "url": "https://releases.nixos.org/nixos/21.05/nixos-21.05.3065.b3083bc6933/nixexprs.tar.xz", + "sha256": "186vni8rij8bhd6n5n9h55jf2x78v9zdy2gn9v4cpjhajp4pvzm0" } diff --git a/shell.nix b/shell.nix index f3d2750edd68e4861e6d0700e0259c1ce86f817a..a5741377eec5ebd4b8862a0ea47e15edfdac2731 100644 --- a/shell.nix +++ b/shell.nix @@ -1,12 +1,28 @@ let release2105 = import ./nixpkgs-2105.nix { }; in -{ pkgs ? release2105 }: +{ pkgs ? release2105, lib ? pkgs.lib, python ? pkgs.python3 }: +let + tools = pkgs.callPackage ./tools {}; +in pkgs.mkShell { - NIX_PATH = "nixpkgs=${pkgs.path}"; + # When a path (such as `pkgs.path`) is interpolated into a string then nix + # first adds that path to the store, and then interpolates the store path + # into the string. We use `builtins.toString` to convert the path to a + # string without copying it to the store before interpolating. Either the + # path is already in the store (e.g. when `pkgs` is `release2105`) so we + # avoid making a second copy with a longer name, or the user passed in local + # path (e.g. a checkout of nixpkgs) and we point at it directly, rather than + # a snapshot of it. + # See https://github.com/NixOS/nix/issues/200 and https://github.com/NixOS/nix/issues/1728 + shellHook = '' + export NIX_PATH="nixpkgs=${builtins.toString pkgs.path}"; + ''; + # Run the shellHook from tools + inputsFrom = [tools]; buildInputs = [ + tools pkgs.morph - pkgs.vagrant pkgs.jp ]; } diff --git a/tools/default.nix b/tools/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..f9a0b1ff8d902f3072886939ad11e1e223ffbb7e --- /dev/null +++ b/tools/default.nix @@ -0,0 +1,49 @@ +{ pkgs, lib, makeWrapper, ... }: +let + python = pkgs.python3; + # This is a python envionment that has the dependencies + # for the development python scripts we use, and the + # helper library. + python-env = python.buildEnv.override { + extraLibs = [ python.pkgs.httpx ]; + # Add `.pth` file pointing at the directory containg our helper library. + # This will get added to `sys.path` by `site.py`. + # See https://docs.python.org/3/library/site.html + postBuild = '' + echo ${lib.escapeShellArg ./pylib} > $out/${lib.escapeShellArg python.sitePackages}/tools.pth + ''; + }; + python-commands = [ + ./update-nixpkgs + ]; +in + # This derivation creates a package that wraps our tools to setup an environment + # with there dependencies available. +pkgs.runCommand "ps_tools" { + nativeBuildInputs = [ makeWrapper ]; + shellHook = '' + # Only display the help if we are running an interactive shell. + if [[ $- == *i* ]]; then + cat <<MOTD + Tools (pass --help for details): + ${lib.concatStringsSep "\n" (map (path: + "- ${baseNameOf path}" + ) python-commands)} + MOTD + fi + ''; + } '' + mkdir -p $out/bin + ${lib.concatStringsSep "\n" (map (path: + let + baseName = baseNameOf path; + # We use toString so that we wrap the in-tree scripts, rather than copying + # them to the nix-store. This means that we don't need to run nix-shell again + # to pick up changes. + sourcePath = toString path; + in + # makeWrapper <executable> <wrapperfile> <args> + # See https://nixos.org/manual/nixpkgs/stable/#fun-makeWrapper + "makeWrapper ${python-env}/bin/python $out/bin/${baseName} --add-flags ${sourcePath}" + ) python-commands)} + '' diff --git a/tools/pylib/README.rst b/tools/pylib/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..083f6620d93ac30303b805cb4098e93804323aef --- /dev/null +++ b/tools/pylib/README.rst @@ -0,0 +1,2 @@ +This directory contains a python package of helper functions used by the scripts in ``tools/``. +To get this on the python path, run ``nix-shell`` in the root of the repository. diff --git a/tools/pylib/ps_tools/__init__.py b/tools/pylib/ps_tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..278ef0f1a765a39f5e8a46adf99621b6b238dc5e --- /dev/null +++ b/tools/pylib/ps_tools/__init__.py @@ -0,0 +1,35 @@ +""" +Helpers for development and CI scripts. +""" +from __future__ import annotations + +import subprocess + + +def get_url_hash(hash_type, name, url) -> dict[str, str]: + """ + Get the nix hash of the given URL. + + :returns: Dictionary of arguments suitable to pass to :nix:`pkgs.fetchzip` + or a function derived from it (such as :nix:`pkgs.fetchFromGitLab`) + to specify the hash. + """ + output = subprocess.run( + [ + "nix-prefetch-url", + "--type", + hash_type, + "--unpack", + "--name", + name, + url, + ], + capture_output=True, + check=True, + encoding="utf-8", + ) + + return { + "outputHashAlgo": hash_type, + "outputHash": output.stdout.strip(), + } diff --git a/tools/update-nixpkgs b/tools/update-nixpkgs new file mode 100755 index 0000000000000000000000000000000000000000..09c823b0a419b5937d4953337b94a26c4b502e32 --- /dev/null +++ b/tools/update-nixpkgs @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +import argparse +import json +from pathlib import Path + +import httpx +from ps_tools import get_url_hash + +# We pass this to builtins.fetchTarball which only supports sha256 +HASH_TYPE = "sha256" + +DEFAULT_CHANNEL = "nixos-21.05" +CHANNEL_URL_TEMPLATE = "https://channels.nixos.org/{channel}/nixexprs.tar.xz" + + +def get_nixos_channel_url(*, channel): + """ + Get the URL for the current release of the given nixos channel. + + `https://channels.nixos.org/<channel>` redirects to the path on + `https://releases.nixos.org` that corresponds to the current release + of that channel. This captures that redirect, so we can pin against + the release. + """ + response = httpx.head( + CHANNEL_URL_TEMPLATE.format(channel=channel), allow_redirects=False + ) + response.raise_for_status() + assert response.is_redirect + return str(response.next_request.url) + + +def main(): + parser = argparse.ArgumentParser(description="Update a pinned nixos repository.") + parser.add_argument( + "repo_file", + metavar="repo-file", + nargs="?", + default=Path(__file__).parent.with_name("nixpkgs-2105.json"), + type=Path, + help="JSON file with pinned configuration.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + ) + parser.set_defaults(channel=DEFAULT_CHANNEL) + args = parser.parse_args() + + repo_file = args.repo_file + config = json.loads(repo_file.read_text()) + + config["url"] = get_nixos_channel_url(channel=args.channel) + hash_data = get_url_hash(HASH_TYPE, name=config["name"], url=config["url"]) + config["sha256"] = hash_data["outputHash"] + + output = json.dumps(config, indent=2) + if args.dry_run: + print(output) + else: + repo_file.write_text(output) + + +if __name__ == "__main__": + main()