From c92e85f70734aabf2442f20a239cdc02af2121be Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:11 +0200 Subject: [PATCH 01/27] test: rename base.py -> tests.py --- test/test.nix | 2 +- test/{base.py => tests.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test/{base.py => tests.py} (100%) diff --git a/test/test.nix b/test/test.nix index 15bbd69..808e694 100644 --- a/test/test.nix +++ b/test/test.nix @@ -72,5 +72,5 @@ import ./make-test.nix rec { ''; }; testScript = - builtins.readFile ./base.py + "\n\n" + builtins.readFile "${./.}/scenarios/${scenario}.py"; + builtins.readFile ./tests.py + "\n\n" + builtins.readFile "${./.}/scenarios/${scenario}.py"; } diff --git a/test/base.py b/test/tests.py similarity index 100% rename from test/base.py rename to test/tests.py From 45bcbf683d600e75b00aab4f4e8f015224ab1a9a Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:12 +0200 Subject: [PATCH 02/27] test: rename test.nix -> tests.nix The plural is consistent with tests.py and run-tests.sh --- test/run-tests.sh | 6 +++--- test/{test.nix => tests.nix} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename test/{test.nix => tests.nix} (100%) diff --git a/test/run-tests.sh b/test/run-tests.sh index 7e42465..2263d7d 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Modules integration test runner. -# The test (./test.nix) uses the NixOS testing framework and is executed in a VM. +# The tests (./tests.nix) use the NixOS testing framework and are executed in a VM. # # Usage: # Run all tests @@ -67,7 +67,7 @@ run() { export TMPDIR=$(mktemp -d /tmp/nix-bitcoin-test.XXX) trap "rm -rf $TMPDIR" EXIT - nix-build --out-link $TMPDIR/driver -E "import \"$scriptDir/test.nix\" { scenario = \"$scenario\"; }" -A driver + nix-build --out-link $TMPDIR/driver -E "import \"$scriptDir/tests.nix\" { scenario = \"$scenario\"; }" -A driver # Variable 'tests' contains the Python code that is executed by the driver on startup if [[ $1 == --interactive ]]; then @@ -135,7 +135,7 @@ exprForCI() { vmTestNixExpr() { cat < Date: Sun, 27 Sep 2020 12:43:13 +0200 Subject: [PATCH 03/27] electrs: use consistent args formatting One line per arg. --- modules/electrs.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/electrs.nix b/modules/electrs.nix index 0d67230..2b2fdc8 100644 --- a/modules/electrs.nix +++ b/modules/electrs.nix @@ -91,9 +91,11 @@ in { else "--jsonrpc-import --index-batch-size=10" } \ - --db-dir '${cfg.dataDir}' --daemon-dir '${config.services.bitcoind.dataDir}' \ + --db-dir='${cfg.dataDir}' \ + --daemon-dir='${config.services.bitcoind.dataDir}' \ --electrum-rpc-addr=${toString cfg.address}:${toString cfg.port} \ - --daemon-rpc-addr=${toString cfg.daemonrpc} ${cfg.extraArgs} + --daemon-rpc-addr=${toString cfg.daemonrpc} \ + ${cfg.extraArgs} ''; User = cfg.user; Group = cfg.group; From a19d3b07c2db9cb314dac9d3cc484f59210ec574 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:14 +0200 Subject: [PATCH 04/27] electrs: add variable 'bitcoind' --- modules/electrs.nix | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/electrs.nix b/modules/electrs.nix index 2b2fdc8..b5a103e 100644 --- a/modules/electrs.nix +++ b/modules/electrs.nix @@ -5,6 +5,7 @@ let cfg = config.services.electrs; inherit (config) nix-bitcoin-services; secretsDir = config.nix-bitcoin.secretsDir; + bitcoind = config.services.bitcoind; in { options.services.electrs = { enable = mkEnableOption "electrs"; @@ -57,7 +58,7 @@ in { config = mkIf cfg.enable { assertions = [ - { assertion = config.services.bitcoind.prune == 0; + { assertion = bitcoind.prune == 0; message = "electrs does not support bitcoind pruning."; } ]; @@ -74,7 +75,7 @@ in { requires = [ "bitcoind.service" ]; after = [ "bitcoind.service" ]; preStart = '' - echo "cookie = \"${config.services.bitcoind.rpc.users.public.name}:$(cat ${secretsDir}/bitcoin-rpcpassword-public)\"" \ + echo "cookie = \"${bitcoind.rpc.users.public.name}:$(cat ${secretsDir}/bitcoin-rpcpassword-public)\"" \ > electrs.toml ''; serviceConfig = nix-bitcoin-services.defaultHardening // { @@ -84,7 +85,7 @@ in { ExecStart = '' ${pkgs.nix-bitcoin.electrs}/bin/electrs -vvv \ ${if cfg.high-memory then - traceIf (!config.services.bitcoind.dataDirReadableByGroup) '' + traceIf (!bitcoind.dataDirReadableByGroup) '' Warning: For optimal electrs syncing performance, enable services.bitcoind.dataDirReadableByGroup. Note that this disables wallet support in bitcoind. '' "" @@ -92,7 +93,7 @@ in { "--jsonrpc-import --index-batch-size=10" } \ --db-dir='${cfg.dataDir}' \ - --daemon-dir='${config.services.bitcoind.dataDir}' \ + --daemon-dir='${bitcoind.dataDir}' \ --electrum-rpc-addr=${toString cfg.address}:${toString cfg.port} \ --daemon-rpc-addr=${toString cfg.daemonrpc} \ ${cfg.extraArgs} @@ -101,7 +102,7 @@ in { Group = cfg.group; Restart = "on-failure"; RestartSec = "10s"; - ReadWritePaths = "${cfg.dataDir} ${if cfg.high-memory then "${config.services.bitcoind.dataDir}" else ""}"; + ReadWritePaths = "${cfg.dataDir} ${if cfg.high-memory then "${bitcoind.dataDir}" else ""}"; } // (if cfg.enforceTor then nix-bitcoin-services.allowTor else nix-bitcoin-services.allowAnyIP From 611cfe5a28b63a074aca6e8df0190001e519a14f Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:15 +0200 Subject: [PATCH 05/27] electrs: remove redundant daemonrpc option --- modules/electrs.nix | 9 +-------- modules/netns-isolation.nix | 5 +---- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/modules/electrs.nix b/modules/electrs.nix index b5a103e..f594783 100644 --- a/modules/electrs.nix +++ b/modules/electrs.nix @@ -41,13 +41,6 @@ in { default = 50001; description = "RPC port."; }; - daemonrpc = mkOption { - type = types.str; - default = "127.0.0.1:8332"; - description = '' - Bitcoin daemon JSONRPC 'addr:port' to connect - ''; - }; extraArgs = mkOption { type = types.separatedString " "; default = ""; @@ -95,7 +88,7 @@ in { --db-dir='${cfg.dataDir}' \ --daemon-dir='${bitcoind.dataDir}' \ --electrum-rpc-addr=${toString cfg.address}:${toString cfg.port} \ - --daemon-rpc-addr=${toString cfg.daemonrpc} \ + --daemon-rpc-addr=${builtins.elemAt bitcoind.rpcbind 0}:${toString bitcoind.rpc.port} \ ${cfg.extraArgs} ''; User = cfg.user; diff --git a/modules/netns-isolation.nix b/modules/netns-isolation.nix index d456fa4..cfeb253 100644 --- a/modules/netns-isolation.nix +++ b/modules/netns-isolation.nix @@ -298,10 +298,7 @@ in { cliExec = mkCliExec "liquidd"; }; - services.electrs = { - address = netns.electrs.address; - daemonrpc = "${netns.bitcoind.address}:${toString config.services.bitcoind.rpc.port}"; - }; + services.electrs.address = netns.electrs.address; services.spark-wallet = { host = netns.spark-wallet.address; From 24069aa2c6982fb3e6c492379a486c1ce3aebcbd Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:16 +0200 Subject: [PATCH 06/27] electrs: add option 'monitoringPort' --- modules/electrs.nix | 10 ++++++++-- test/scenarios/withnetns.py | 5 ++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/electrs.nix b/modules/electrs.nix index f594783..4804eb7 100644 --- a/modules/electrs.nix +++ b/modules/electrs.nix @@ -34,13 +34,18 @@ in { address = mkOption { type = types.str; default = "127.0.0.1"; - description = "RPC listening address."; + description = "RPC and monitoring listening address."; }; port = mkOption { type = types.port; default = 50001; description = "RPC port."; }; + monitoringPort = mkOption { + type = types.port; + default = 4224; + description = "Prometheus monitoring port."; + }; extraArgs = mkOption { type = types.separatedString " "; default = ""; @@ -87,7 +92,8 @@ in { } \ --db-dir='${cfg.dataDir}' \ --daemon-dir='${bitcoind.dataDir}' \ - --electrum-rpc-addr=${toString cfg.address}:${toString cfg.port} \ + --electrum-rpc-addr=${cfg.address}:${toString cfg.port} \ + --monitoring-addr=${cfg.address}:${toString cfg.monitoringPort} \ --daemon-rpc-addr=${builtins.elemAt bitcoind.rpcbind 0}:${toString bitcoind.rpc.port} \ ${cfg.extraArgs} ''; diff --git a/test/scenarios/withnetns.py b/test/scenarios/withnetns.py index 5f3be5a..8aec4f5 100644 --- a/test/scenarios/withnetns.py +++ b/test/scenarios/withnetns.py @@ -15,9 +15,8 @@ btcpayserver_ip = "169.254.1.24" def electrs(): - machine.wait_until_succeeds( - "ip netns exec nb-electrs nc -z localhost 4224" - ) # prometeus metrics provider + # prometeus metrics provider + machine.wait_until_succeeds(f"nc -z {electrs_ip} 4224") def nbxplorer(): From fcda69e8b616728cb74fd47ec1672c1b27eb6605 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:17 +0200 Subject: [PATCH 07/27] netns test: connect from main netns All services are reachable from the main netns, no need to enter service network namespaces. This allows us to remove extra_tests. --- test/scenarios/default.py | 53 +------------- test/scenarios/withnetns.py | 138 +++++++++++------------------------- test/tests.py | 55 +++++++++----- 3 files changed, 82 insertions(+), 164 deletions(-) diff --git a/test/scenarios/default.py b/test/scenarios/default.py index e5c1eb0..d9ad19e 100644 --- a/test/scenarios/default.py +++ b/test/scenarios/default.py @@ -1,56 +1,5 @@ -def electrs(): - machine.wait_for_open_port(4224) # prometeus metrics provider - - -def nbxplorer(): - machine.wait_for_open_port(24444) - - -def btcpayserver(): - machine.wait_for_open_port(23000) - # test lnd custom macaroon - assert_matches( - 'sudo -u btcpayserver curl -s --cacert /secrets/lnd-cert --header "Grpc-Metadata-macaroon: $(xxd -ps -u -c 1000 /run/lnd/btcpayserver.macaroon)" -X GET https://127.0.0.1:8080/v1/getinfo | jq', - '"version"', - ) - - -def spark_wallet(): - machine.wait_for_open_port(9737) - spark_auth = re.search("login=(.*)", succeed("cat /secrets/spark-wallet-login"))[1] - assert_matches(f"curl -s {spark_auth}@localhost:9737", "Spark") - - -def lightning_charge(): - machine.wait_for_open_port(9112) - charge_auth = re.search("API_TOKEN=(.*)", succeed("cat /secrets/lightning-charge-env"))[1] - assert_matches(f"curl -s api-token:{charge_auth}@localhost:9112/info | jq", '"id"') - - -def nanopos(): - machine.wait_for_open_port(9116) - assert_matches("curl localhost:9116", "tshirt") - - -def web_index(): - machine.wait_for_open_port(80) - assert_matches("curl localhost", "nix-bitcoin") - assert_matches("curl -L localhost/store", "tshirt") - - def prestop(): pass -extra_tests = { - "electrs": electrs, - "nbxplorer": nbxplorer, - "btcpayserver": btcpayserver, - "spark-wallet": spark_wallet, - "lightning-charge": lightning_charge, - "nanopos": nanopos, - "web-index": web_index, - "prestop": prestop, -} - -run_tests(extra_tests) +run_tests() diff --git a/test/scenarios/withnetns.py b/test/scenarios/withnetns.py index 8aec4f5..5c84fbd 100644 --- a/test/scenarios/withnetns.py +++ b/test/scenarios/withnetns.py @@ -1,65 +1,22 @@ -# netns IP addresses -bitcoind_ip = "169.254.1.12" -clightning_ip = "169.254.1.13" -lnd_ip = "169.254.1.14" -liquidd_ip = "169.254.1.15" -electrs_ip = "169.254.1.16" -sparkwallet_ip = "169.254.1.17" -lightningcharge_ip = "169.254.1.18" -nanopos_ip = "169.254.1.19" -recurringdonations_ip = "169.254.1.20" -nginx_ip = "169.254.1.21" -lightningloop_ip = "169.254.1.22" -nbxplorer_ip = "169.254.1.23" -btcpayserver_ip = "169.254.1.24" +netns_ips = { + "bitcoind": "169.254.1.12", + "clightning": "169.254.1.13", + "lnd": "169.254.1.14", + "liquidd": "169.254.1.15", + "electrs": "169.254.1.16", + "spark-wallet": "169.254.1.17", + "lightning-charge": "169.254.1.18", + "nanopos": "169.254.1.19", + "recurring-donations": "169.254.1.20", + "nginx": "169.254.1.21", + "lightning-loop": "169.254.1.22", + "nbxplorer": "169.254.1.23", + "btcpayserver": "169.254.1.24", +} -def electrs(): - # prometeus metrics provider - machine.wait_until_succeeds(f"nc -z {electrs_ip} 4224") - - -def nbxplorer(): - machine.wait_until_succeeds("ip netns exec nb-nbxplorer nc -z %s 24444" % nbxplorer_ip) - - -def btcpayserver(): - machine.wait_until_succeeds("ip netns exec nb-btcpayserver nc -z %s 23000" % btcpayserver_ip) - # test lnd custom macaroon - assert_matches( - 'ip netns exec nb-btcpayserver sudo -u btcpayserver curl -s --cacert /secrets/lnd-cert --header "Grpc-Metadata-macaroon: $(xxd -ps -u -c 1000 /run/lnd/btcpayserver.macaroon)" -X GET https://%s:8080/v1/getinfo | jq' - % lnd_ip, - '"version"', - ) - - -def spark_wallet(): - machine.wait_until_succeeds("ip netns exec nb-spark-wallet nc -z %s 9737" % sparkwallet_ip) - spark_auth = re.search("login=(.*)", succeed("cat /secrets/spark-wallet-login"))[1] - assert_matches( - f"ip netns exec nb-spark-wallet curl -s {spark_auth}@%s:9737" % sparkwallet_ip, "Spark" - ) - - -def lightning_charge(): - machine.wait_until_succeeds("ip netns exec nb-nanopos nc -z %s 9112" % lightningcharge_ip) - charge_auth = re.search("API_TOKEN=(.*)", succeed("cat /secrets/lightning-charge-env"))[1] - assert_matches( - f"ip netns exec nb-nanopos curl -s api-token:{charge_auth}@%s:9112/info | jq" - % lightningcharge_ip, - '"id"', - ) - - -def nanopos(): - machine.wait_until_succeeds("ip netns exec nb-lightning-charge nc -z %s 9116" % nanopos_ip) - assert_matches("ip netns exec nb-lightning-charge curl %s:9116" % nanopos_ip, "tshirt") - - -def web_index(): - machine.wait_until_succeeds("ip netns exec nb-nginx nc -z localhost 80") - assert_matches("ip netns exec nb-nginx curl localhost", "nix-bitcoin") - assert_matches("ip netns exec nb-nginx curl -L localhost/store", "tshirt") +def ip(netns): + return netns_ips[netns] def prestop(): @@ -69,34 +26,34 @@ def prestop(): # Positive ping tests (non-exhaustive) machine.succeed( - "%s %s &&" % (ping_bitcoind, bitcoind_ip) - + "%s %s &&" % (ping_bitcoind, clightning_ip) - + "%s %s &&" % (ping_bitcoind, lnd_ip) - + "%s %s &&" % (ping_bitcoind, liquidd_ip) - + "%s %s &&" % (ping_bitcoind, nbxplorer_ip) - + "%s %s &&" % (ping_nbxplorer, btcpayserver_ip) - + "%s %s &&" % (ping_nanopos, lightningcharge_ip) - + "%s %s &&" % (ping_nanopos, nanopos_ip) - + "%s %s" % (ping_nanopos, nginx_ip) + "%s %s &&" % (ping_bitcoind, ip("bitcoind")) + + "%s %s &&" % (ping_bitcoind, ip("clightning")) + + "%s %s &&" % (ping_bitcoind, ip("lnd")) + + "%s %s &&" % (ping_bitcoind, ip("liquidd")) + + "%s %s &&" % (ping_bitcoind, ip("nbxplorer")) + + "%s %s &&" % (ping_nbxplorer, ip("btcpayserver")) + + "%s %s &&" % (ping_nanopos, ip("lightning-charge")) + + "%s %s &&" % (ping_nanopos, ip("nanopos")) + + "%s %s" % (ping_nanopos, ip("nginx")) ) # Negative ping tests (non-exhaustive) machine.fail( - "%s %s ||" % (ping_bitcoind, sparkwallet_ip) - + "%s %s ||" % (ping_bitcoind, lightningloop_ip) - + "%s %s ||" % (ping_bitcoind, lightningcharge_ip) - + "%s %s ||" % (ping_bitcoind, nanopos_ip) - + "%s %s ||" % (ping_bitcoind, recurringdonations_ip) - + "%s %s ||" % (ping_bitcoind, nginx_ip) - + "%s %s ||" % (ping_nanopos, bitcoind_ip) - + "%s %s ||" % (ping_nanopos, clightning_ip) - + "%s %s ||" % (ping_nanopos, lnd_ip) - + "%s %s ||" % (ping_nanopos, lightningloop_ip) - + "%s %s ||" % (ping_nanopos, liquidd_ip) - + "%s %s ||" % (ping_nanopos, electrs_ip) - + "%s %s ||" % (ping_nanopos, sparkwallet_ip) - + "%s %s ||" % (ping_nanopos, recurringdonations_ip) - + "%s %s" % (ping_nanopos, btcpayserver_ip) + "%s %s ||" % (ping_bitcoind, ip("spark-wallet")) + + "%s %s ||" % (ping_bitcoind, ip("lightning-loop")) + + "%s %s ||" % (ping_bitcoind, ip("lightning-charge")) + + "%s %s ||" % (ping_bitcoind, ip("nanopos")) + + "%s %s ||" % (ping_bitcoind, ip("recurring-donations")) + + "%s %s ||" % (ping_bitcoind, ip("nginx")) + + "%s %s ||" % (ping_nanopos, ip("bitcoind")) + + "%s %s ||" % (ping_nanopos, ip("clightning")) + + "%s %s ||" % (ping_nanopos, ip("lnd")) + + "%s %s ||" % (ping_nanopos, ip("lightning-loop")) + + "%s %s ||" % (ping_nanopos, ip("liquidd")) + + "%s %s ||" % (ping_nanopos, ip("electrs")) + + "%s %s ||" % (ping_nanopos, ip("spark-wallet")) + + "%s %s ||" % (ping_nanopos, ip("recurring-donations")) + + "%s %s" % (ping_nanopos, ip("btcpayserver")) ) # test that netns-exec can't be run for unauthorized namespace @@ -111,15 +68,4 @@ def prestop(): machine.fail("sudo -u clightning netns-exec nb-bitcoind ip a") -extra_tests = { - "electrs": electrs, - "nbxplorer": nbxplorer, - "btcpayserver": btcpayserver, - "spark-wallet": spark_wallet, - "lightning-charge": lightning_charge, - "nanopos": nanopos, - "web-index": web_index, - "prestop": prestop, -} - -run_tests(extra_tests) +run_tests() diff --git a/test/tests.py b/test/tests.py index a2faf79..284dfd8 100644 --- a/test/tests.py +++ b/test/tests.py @@ -32,11 +32,16 @@ def assert_running(unit): assert_no_failure(unit) -def run_tests(extra_tests): - """ - :param extra_tests: Test functions that hook into the testing code below - :type extra_tests: Dict[str, Callable[]] - """ +def wait_for_open_port(address, port): + def is_port_open(_): + status, _ = machine.execute(f"nc -z {address} {port}") + return status == 0 + + with log.nested(f"Waiting for TCP port {address}:{port}"): + retry(is_port_open) + + +def run_tests(): # Don't execute the following test suite when this script is running in interactive mode if is_interactive: raise Exception() @@ -55,7 +60,7 @@ def run_tests(extra_tests): ) assert_running("electrs") - extra_tests.pop("electrs")() + wait_for_open_port(ip("electrs"), 4224) # prometeus metrics provider # Check RPC connection to bitcoind machine.wait_until_succeeds(log_has_string("electrs", "NetworkInfo")) # Stop electrs from spamming the test log with 'wait for bitcoind sync' messages @@ -86,19 +91,34 @@ def run_tests(extra_tests): assert_running("nbxplorer") machine.wait_until_succeeds(log_has_string("nbxplorer", "BTC: RPC connection successful")) - extra_tests.pop("nbxplorer")() + wait_for_open_port(ip("nbxplorer"), 24444) assert_running("btcpayserver") machine.wait_until_succeeds(log_has_string("btcpayserver", "Listening on")) - extra_tests.pop("btcpayserver")() + wait_for_open_port(ip("btcpayserver"), 23000) + # test lnd custom macaroon + assert_matches( + "sudo -u btcpayserver curl -s --cacert /secrets/lnd-cert " + '--header "Grpc-Metadata-macaroon: $(xxd -ps -u -c 1000 /run/lnd/btcpayserver.macaroon)" ' + f"-X GET https://{ip('lnd')}:8080/v1/getinfo | jq", + '"version"', + ) assert_running("spark-wallet") - extra_tests.pop("spark-wallet")() + wait_for_open_port(ip("spark-wallet"), 9737) + spark_auth = re.search("login=(.*)", succeed("cat /secrets/spark-wallet-login"))[1] + assert_matches(f"curl -s {spark_auth}@{ip('spark-wallet')}:9737", "Spark") assert_running("lightning-charge") - extra_tests.pop("lightning-charge")() + wait_for_open_port(ip("lightning-charge"), 9112) + machine.wait_until_succeeds(f"nc -z {ip('lightning-charge')} 9112") + charge_auth = re.search("API_TOKEN=(.*)", succeed("cat /secrets/lightning-charge-env"))[1] + assert_matches( + f"curl -s api-token:{charge_auth}@{ip('lightning-charge')}:9112/info | jq", '"id"' + ) assert_running("nanopos") - extra_tests.pop("nanopos")() + wait_for_open_port(ip("nanopos"), 9116) + assert_matches(f"curl {ip('nanopos')}:9116", "tshirt") assert_running("onion-chef") @@ -115,7 +135,9 @@ def run_tests(extra_tests): # 'create-web-index' implicitly tests 'nodeinfo'. machine.wait_for_unit("create-web-index") assert_running("nginx") - extra_tests.pop("web-index")() + wait_for_open_port(ip("nginx"), 80) + assert_matches(f"curl {ip('nginx')}", "nix-bitcoin") + assert_matches(f"curl -L {ip('nginx')}/store", "tshirt") machine.wait_until_succeeds(log_has_string("bitcoind-import-banlist", "Importing node banlist")) assert_no_failure("bitcoind-import-banlist") @@ -137,7 +159,7 @@ def run_tests(extra_tests): ) assert_no_failure("bitcoind-import-banlist") - extra_tests.pop("prestop")() + prestop() ### Test duplicity @@ -171,9 +193,6 @@ def run_tests(extra_tests): "var/backup/postgresql/btcpaydb.sql.gz", ) - ### Check that all extra_tests have been run - assert len(extra_tests) == 0 - def test_security(): assert_running("setup-secrets") @@ -191,3 +210,7 @@ def test_security(): ) # The 'operator' with group 'proc' has full access assert_full_match("sudo -u operator systemctl status bitcoind 2>&1 >/dev/null", "") + + +def ip(_): + return "127.0.0.1" From 9bf77ee3e80bd7d23f29640f268aabb75a267667 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:18 +0200 Subject: [PATCH 08/27] backups test: simplify and speed up --- test/tests.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/test/tests.py b/test/tests.py index 284dfd8..c0f6d8c 100644 --- a/test/tests.py +++ b/test/tests.py @@ -166,32 +166,19 @@ def run_tests(): succeed("systemctl stop bitcoind") succeed("systemctl start duplicity") machine.wait_until_succeeds(log_has_string("duplicity", "duplicity.service: Succeeded.")) - # Make sure files in duplicity backup and /var/lib are identical + run_duplicity = "export $(cat /secrets/backup-encryption-env); duplicity" + # Files in backup and /var/lib should be identical assert_matches( - "export $(cat /secrets/backup-encryption-env); duplicity verify '--archive-dir' '/var/lib/duplicity' 'file:///var/lib/localBackups' '/var/lib'", + f"{run_duplicity} verify --archive-dir /var/lib/duplicity file:///var/lib/localBackups /var/lib", "0 differences found", ) - # Make sure duplicity backup includes important files - assert_matches( - "export $(cat /secrets/backup-encryption-env); duplicity list-current-files 'file:///var/lib/localBackups'", - "var/lib/clightning/bitcoin/hsm_secret", - ) - assert_matches( - "export $(cat /secrets/backup-encryption-env); duplicity list-current-files 'file:///var/lib/localBackups'", - "secrets/lnd-seed-mnemonic", - ) - assert_matches( - "export $(cat /secrets/backup-encryption-env); duplicity list-current-files 'file:///var/lib/localBackups'", - "secrets/jm-wallet-seed", - ) - assert_matches( - "export $(cat /secrets/backup-encryption-env); duplicity list-current-files 'file:///var/lib/localBackups'", - "var/lib/bitcoind/wallet.dat", - ) - assert_matches( - "export $(cat /secrets/backup-encryption-env); duplicity list-current-files 'file:///var/lib/localBackups'", - "var/backup/postgresql/btcpaydb.sql.gz", - ) + # Backup should include important files + files = succeed(f"{run_duplicity} list-current-files file:///var/lib/localBackups") + assert "var/lib/clightning/bitcoin/hsm_secret" in files + assert "secrets/lnd-seed-mnemonic" in files + assert "secrets/jm-wallet-seed" in files + assert "var/lib/bitcoind/wallet.dat" in files + assert "var/backup/postgresql/btcpaydb.sql.gz" in files def test_security(): From 14d2d97ba6e460ff0f322890dd9a829681b45fb7 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:19 +0200 Subject: [PATCH 09/27] test: rename scenario withnetns -> netns This makes the naming consistent with scenarios added in later commits. --- .travis.yml | 2 +- test/run-tests.sh | 2 +- test/scenarios/{withnetns.py => netns.py} | 0 test/tests.nix | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename test/scenarios/{withnetns.py => netns.py} (100%) diff --git a/.travis.yml b/.travis.yml index 8a487e4..c3fe269 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ env: - secure: "xXCFZ7g+k5YmCGm8R8l3bZElVmt+RD1KscG3kGr5w4HyyDPTzFetPo+sT8bUpysDU0u3HWhfVhHtpog2mhNhwVl3tQwKXea3dHKC1i6ypBg3gjDngmJRR5wo++ocYDpK8qPaU7m/jHQTNFnTA4CbmMcc05GcYx/1Ai/ZGkNwWFjdIcVeOUoiol33gykMOXIGDg2qlXudt33wP53FHbX8L4fxzodWfAuxKK4AoGprxy5eSnU7LCaXxxJmu4HwuV+Ux2U1NfE/E33cvhlUvTQCswVSZFG06mg8rwhMG1ozsDvlL2itZlu/BeUQH5y3XMMlnJIUXUazkRBibf1w/ebVjpOF+anqkqmq8tcbFEa7T+RJeVTIsvP+L8rE8fcmuZtdg9hNmgRnLmaeT0vVwD1L2UqW9HdRyujdoS0jPYuoc1W7f1JQWfAPhBPQ1SrtKyNNqcbVJ34aN7b+4vCzRpQL1JTbmjzQIWhkiKN1qMo1v/wbIydW8yka4hc4JOfdQLaAJEPI1eAC1MLotSAegMnwKWE1dzm66MuPSipksYjZrvsB28cV4aCVUffIuRhrSr1i2afRHwTpNbK9U4/576hah15ftUdR79Sfkcoi1ekSQTFGRvkRIPYtkKLYwFa3jVA41qz7+IIZCf4TsApy3XDdFx91cRub7yPq9BeZ83A+qYQ=" jobs: - TestModules=1 STABLE=1 SCENARIO=default - - TestModules=1 STABLE=1 SCENARIO=withnetns + - TestModules=1 STABLE=1 SCENARIO=netns - PKG=hwi STABLE=1 - PKG=hwi STABLE=0 - PKG=lightning-charge STABLE=1 diff --git a/test/run-tests.sh b/test/run-tests.sh index 2263d7d..d1bb80e 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -149,7 +149,7 @@ build() { buildTest "$@" else scenario=default buildTest "$@" - scenario=withnetns buildTest "$@" + scenario=netns buildTest "$@" fi } diff --git a/test/scenarios/withnetns.py b/test/scenarios/netns.py similarity index 100% rename from test/scenarios/withnetns.py rename to test/scenarios/netns.py diff --git a/test/tests.nix b/test/tests.nix index 808e694..9d7020b 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -22,7 +22,7 @@ import ./make-test.nix rec { # needed because duplicity requires 270 MB of free temp space, regardless of backup size. virtualisation.diskSize = 1024; - nix-bitcoin.netns-isolation.enable = (scenario == "withnetns"); + nix-bitcoin.netns-isolation.enable = (scenario == "netns"); services.bitcoind.extraConfig = mkForce "connect=0"; From 1e18d3ea3b38528b784faa581bfcdc9484517f63 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:20 +0200 Subject: [PATCH 10/27] test: improve modularization This improves debugging and experimenting by making it easy to compose fine-grained scenarios that have specific tests and features enabled. The VM test output now includes the subtest name and duration. Remove the 'raise Exception()' hack for interactive mode. Run 'banlist-and-restart' test before 'backups'. This speeds up the test by avoiding an extra shutdown of all bitcoin-related services. --- test/{make-test.nix => lib/make-test-vm.nix} | 0 test/lib/make-test.nix | 41 ++++ test/lib/test-lib.nix | 30 +++ test/run-tests.sh | 23 +- test/scenarios/default.py | 5 - test/scenarios/netns.py | 71 ------ test/tests.nix | 176 ++++++++++----- test/tests.py | 220 +++++++++++++++---- 8 files changed, 385 insertions(+), 181 deletions(-) rename test/{make-test.nix => lib/make-test-vm.nix} (100%) create mode 100644 test/lib/make-test.nix create mode 100644 test/lib/test-lib.nix delete mode 100644 test/scenarios/default.py delete mode 100644 test/scenarios/netns.py diff --git a/test/make-test.nix b/test/lib/make-test-vm.nix similarity index 100% rename from test/make-test.nix rename to test/lib/make-test-vm.nix diff --git a/test/lib/make-test.nix b/test/lib/make-test.nix new file mode 100644 index 0000000..c3b5edc --- /dev/null +++ b/test/lib/make-test.nix @@ -0,0 +1,41 @@ +scenario: testConfig: + +{ + vm = import ./make-test-vm.nix { + name = "nix-bitcoin-${scenario}"; + + machine = { + imports = [ testConfig ]; + # Needed because duplicity requires 270 MB of free temp space, regardless of backup size + virtualisation.diskSize = 1024; + }; + + testScript = nodes: let + cfg = nodes.nodes.machine.config; + data = { + data = cfg.test.data; + tests = cfg.tests; + }; + dataFile = builtins.toFile "test-data" (builtins.toJSON data); + initData = '' + import json + + with open("${dataFile}") as f: + data = json.load(f) + + enabled_tests = set(test for (test, enabled) in data["tests"].items() if enabled) + test_data = data["data"] + ''; + in + builtins.concatStringsSep "\n\n" [ + initData + (builtins.readFile ./../tests.py) + # Don't run tests in interactive mode. + # is_interactive is set in ../run-tests.sh + '' + if not "is_interactive" in vars(): + run_tests() + '' + ]; + }; +} diff --git a/test/lib/test-lib.nix b/test/lib/test-lib.nix new file mode 100644 index 0000000..5490643 --- /dev/null +++ b/test/lib/test-lib.nix @@ -0,0 +1,30 @@ +{ config, lib, ... }: +with lib; +{ + options = { + test = { + noConnections = mkOption { + type = types.bool; + default = true; + description = '' + Whether services should be configured to not connect to external hosts. + This can silence some warnings while running the test in an offline environment. + ''; + }; + data = mkOption { + type = types.attrs; + default = {}; + description = '' + Attrs that are available in the Python test script under the global + dictionary variable 'test_data'. The data is exported via JSON. + ''; + }; + }; + + tests = mkOption { + type = with types; attrsOf bool; + default = {}; + description = "Python tests that should be run."; + }; + }; +} diff --git a/test/run-tests.sh b/test/run-tests.sh index d1bb80e..13a5d5c 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -8,7 +8,10 @@ # ./run-tests.sh # # Test specific scenario -# ./run-tests.sh --scenario +# ./run-tests.sh --scenario|-s +# +# When is undefined, the test is run with an adhoc scenario +# where services. is enabled. # # Run test and link results to avoid garbage collection # ./run-tests.sh [--scenario ] --out-link-prefix /tmp/nix-bitcoin-test build @@ -19,8 +22,10 @@ # Run interactive test debugging # ./run-tests.sh [--scenario ] debug # -# This starts the testing VM and drops you into a Python REPL where you can -# manually execute the tests from ./test-script.py +# This starts the testing VM and drops you into a Python REPL where you can +# manually execute the tests from ./tests.py +# +# To add custom scenarios, set the environment variable `scenarioOverridesFile`. set -eo pipefail @@ -67,20 +72,14 @@ run() { export TMPDIR=$(mktemp -d /tmp/nix-bitcoin-test.XXX) trap "rm -rf $TMPDIR" EXIT - nix-build --out-link $TMPDIR/driver -E "import \"$scriptDir/tests.nix\" { scenario = \"$scenario\"; }" -A driver + nix-build --out-link $TMPDIR/driver -E "(import \"$scriptDir/tests.nix\" { scenario = \"$scenario\"; }).vm" -A driver # Variable 'tests' contains the Python code that is executed by the driver on startup if [[ $1 == --interactive ]]; then echo "Running interactive testing environment" tests=$( echo 'is_interactive = True' - # The test script raises an error when 'is_interactive' is defined so - # that it just loads the initial helper functions and stops before - # executing the actual tests - echo 'try:' - echo ' exec(os.environ["testScript"])' - echo 'except:' - echo ' pass' + echo 'exec(os.environ["testScript"])' # Start VM echo 'start_all()' # Start REPL @@ -135,7 +134,7 @@ exprForCI() { vmTestNixExpr() { cat < ]; - security.allowUserNamespaces = true; # re-enable disabled option - }; - - machine = { pkgs, lib, ... }: with lib; { + baseConfig = { imports = [ - ../modules/presets/secure-node.nix + ./lib/test-lib.nix + ../modules/modules.nix ../modules/secrets/generate-secrets.nix - # using the hardened profile increases total test duration by ~50%, so disable it for now - # hardened ]; - # needed because duplicity requires 270 MB of free temp space, regardless of backup size. - virtualisation.diskSize = 1024; + config = { + tests.bitcoind = cfg.bitcoind.enable; + services.bitcoind = { + enable = true; + extraConfig = mkIf config.test.noConnections (mkForce "connect=0"); + }; - nix-bitcoin.netns-isolation.enable = (scenario == "netns"); + tests.clightning = cfg.clightning.enable; - services.bitcoind.extraConfig = mkForce "connect=0"; + tests.spark-wallet = cfg.spark-wallet.enable; - services.clightning.enable = true; - services.spark-wallet.enable = true; - services.lightning-charge.enable = true; - services.nanopos.enable = true; + tests.nanopos = cfg.nanopos.enable; - services.lnd.enable = true; - services.lnd.listenPort = 9736; - services.lightning-loop.enable = true; + tests.lnd = cfg.lnd.enable; + services.lnd.listenPort = 9736; - services.electrs.enable = true; + tests.lightning-loop = cfg.lightning-loop.enable; - services.liquidd = { - enable = true; - listen = mkForce false; - extraConfig = "noconnect=1"; - }; + tests.electrs = cfg.electrs.enable; - services.nix-bitcoin-webindex.enable = true; + tests.liquidd = cfg.liquidd.enable; + services.liquidd = optionalAttrs config.test.noConnections { + listen = mkForce false; + extraConfig = "noconnect=1"; + }; - services.hardware-wallets = { - trezor = true; - ledger = true; - }; + tests.btcpayserver = cfg.btcpayserver.enable; + services.btcpayserver.lightningBackend = "lnd"; + # Needed to test macaroon creation + environment.systemPackages = mkIfTest "btcpayserver" (with pkgs; [ openssl xxd ]); - services.backups.enable = true; + tests.joinmarket = cfg.joinmarket.enable; + services.joinmarket.yieldgenerator = { + enable = config.services.joinmarket.enable; + customParameters = '' + txfee = 200 + cjfee_a = 300 + ''; + }; - services.btcpayserver.enable = true; - services.btcpayserver.lightningBackend = "lnd"; - # needed to test macaroon creation - environment.systemPackages = with pkgs; [ openssl xxd ]; - - services.joinmarket.enable = true; - services.joinmarket.yieldgenerator = { - enable = true; - customParameters = '' - txfee = 200 - cjfee_a = 300 + tests.backups = cfg.backups.enable; + + # To test that unused secrets are made inaccessible by 'setup-secrets' + systemd.services.generate-secrets.postStart = mkIfTest "security" '' + install -o nobody -g nogroup -m777 <(:) /secrets/dummy ''; }; - - # to test that unused secrets are made inaccessible by 'setup-secrets' - systemd.services.generate-secrets.postStart = '' - install -o nobody -g nogroup -m777 <(:) /secrets/dummy - ''; }; - testScript = - builtins.readFile ./tests.py + "\n\n" + builtins.readFile "${./.}/scenarios/${scenario}.py"; -} + + scenarios = { + base = baseConfig; # Included in all scenarios + + default = scenarios.secureNode; + + # All available basic services and tests + full = { + tests.security = true; + + services.clightning.enable = true; + services.spark-wallet.enable = true; + services.lightning-charge.enable = true; + services.nanopos.enable = true; + services.lnd.enable = true; + services.lightning-loop.enable = true; + services.electrs.enable = true; + services.liquidd.enable = true; + services.btcpayserver.enable = true; + services.joinmarket.enable = true; + services.backups.enable = true; + + services.hardware-wallets = { + trezor = true; + ledger = true; + }; + }; + + secureNode = { + imports = [ + scenarios.full + ../modules/presets/secure-node.nix + ]; + services.nix-bitcoin-webindex.enable = true; + tests.secure-node = true; + tests.banlist-and-restart = true; + }; + + netns = { + imports = [ scenarios.secureNode ]; + nix-bitcoin.netns-isolation.enable = true; + tests.netns-isolation = true; + }; + + ## Examples / debug helper + + # Run a selection of tests in scenario 'netns' + selectedTests = { + imports = [ scenarios.netns ]; + tests = mkForce { + btcpayserver = true; + netns-isolation = true; + }; + }; + + adhoc = { + # + # You can also set the env var `scenarioOverridesFile` (used below) to define custom scenarios. + }; + }; +}; +in + let + overrides = builtins.getEnv "scenarioOverridesFile"; + scenarios = testEnv.scenarios // (optionalAttrs (overrides != "") (import overrides { + inherit testEnv config pkgs lib; + })); + autoScenario = { + services.${scenario}.enable = true; + }; + in { + imports = [ + scenarios.base + (scenarios.${scenario} or autoScenario) + ]; + } +) diff --git a/test/tests.py b/test/tests.py index c0f6d8c..e324436 100644 --- a/test/tests.py +++ b/test/tests.py @@ -1,4 +1,4 @@ -is_interactive = "is_interactive" in vars() +from collections import OrderedDict def succeed(*cmds): @@ -28,7 +28,8 @@ def assert_no_failure(unit): def assert_running(unit): - machine.wait_for_unit(unit) + with machine.nested(f"waiting for unit: {unit}"): + machine.wait_for_unit(unit) assert_no_failure(unit) @@ -41,13 +42,63 @@ def wait_for_open_port(address, port): retry(is_port_open) +### Test runner + +tests = OrderedDict() + + +def test(name): + def x(fn): + tests[name] = fn + + return x + + def run_tests(): - # Don't execute the following test suite when this script is running in interactive mode - if is_interactive: - raise Exception() + enabled = enabled_tests.copy() + to_run = [] + for test in tests: + if test in enabled: + enabled.remove(test) + to_run.append(test) + if enabled: + raise RuntimeError(f"The following tests are enabled but not defined: {enabled}") + machine.connect() # Visually separate boot output from the test output + for test in to_run: + with log.nested(f"test: {test}"): + tests[test]() - test_security() +def run_test(test): + tests[test]() + + +### Tests +# All tests are executed in the order they are defined here + + +@test("security") +def _(): + assert_running("setup-secrets") + # Unused secrets should be inaccessible + succeed('[[ $(stat -c "%U:%G %a" /secrets/dummy) = "root:root 440" ]]') + + if "secure-node" in enabled_tests: + # Access to '/proc' should be restricted + machine.succeed("grep -Fq hidepid=2 /proc/mounts") + + machine.wait_for_unit("bitcoind") + # `systemctl status` run by unprivileged users shouldn't leak cgroup info + assert_matches( + "sudo -u electrs systemctl status bitcoind 2>&1 >/dev/null", + "Failed to dump process list for 'bitcoind.service', ignoring: Access denied", + ) + # The 'operator' with group 'proc' has full access + assert_full_match("sudo -u operator systemctl status bitcoind 2>&1 >/dev/null", "") + + +@test("bitcoind") +def _(): assert_running("bitcoind") machine.wait_until_succeeds("bitcoin-cli getnetworkinfo") assert_matches("su operator -c 'bitcoin-cli getnetworkinfo' | jq", '"version"') @@ -59,6 +110,10 @@ def run_tests(): log_has_string("bitcoind", "RPC User public not allowed to call method stop") ) + +# Impure: Stops electrs +@test("electrs") +def _(): assert_running("electrs") wait_for_open_port(ip("electrs"), 4224) # prometeus metrics provider # Check RPC connection to bitcoind @@ -66,18 +121,30 @@ def run_tests(): # Stop electrs from spamming the test log with 'wait for bitcoind sync' messages succeed("systemctl stop electrs") + +@test("liquidd") +def _(): assert_running("liquidd") machine.wait_until_succeeds("elements-cli getnetworkinfo") assert_matches("su operator -c 'elements-cli getnetworkinfo' | jq", '"version"') succeed("su operator -c 'liquidswap-cli --help'") + +@test("clightning") +def _(): assert_running("clightning") assert_matches("su operator -c 'lightning-cli getinfo' | jq", '"id"') + +@test("lnd") +def _(): assert_running("lnd") assert_matches("su operator -c 'lncli getinfo' | jq", '"version"') assert_no_failure("lnd") + +@test("lightning-loop") +def _(): assert_running("lightning-loop") assert_matches("su operator -c 'loop --version'", "version") # Check that lightning-loop fails with the right error, making sure @@ -89,6 +156,9 @@ def run_tests(): ) ) + +@test("btcpayserver") +def _(): assert_running("nbxplorer") machine.wait_until_succeeds(log_has_string("nbxplorer", "BTC: RPC connection successful")) wait_for_open_port(ip("nbxplorer"), 24444) @@ -103,11 +173,17 @@ def run_tests(): '"version"', ) + +@test("spark-wallet") +def _(): assert_running("spark-wallet") wait_for_open_port(ip("spark-wallet"), 9737) spark_auth = re.search("login=(.*)", succeed("cat /secrets/spark-wallet-login"))[1] assert_matches(f"curl -s {spark_auth}@{ip('spark-wallet')}:9737", "Spark") + +@test("lightning-charge") +def _(): assert_running("lightning-charge") wait_for_open_port(ip("lightning-charge"), 9112) machine.wait_until_succeeds(f"nc -z {ip('lightning-charge')} 9112") @@ -116,12 +192,16 @@ def run_tests(): f"curl -s api-token:{charge_auth}@{ip('lightning-charge')}:9112/info | jq", '"id"' ) + +@test("nanopos") +def _(): assert_running("nanopos") wait_for_open_port(ip("nanopos"), 9116) assert_matches(f"curl {ip('nanopos')}:9116", "tshirt") - assert_running("onion-chef") +@test("joinmarket") +def _(): assert_running("joinmarket") machine.wait_until_succeeds( log_has_string("joinmarket", "P2EPDaemonServerProtocolFactory starting on 27184") @@ -130,6 +210,11 @@ def run_tests(): log_has_string("joinmarket-yieldgenerator", "Failure to get blockheight",) ) + +@test("secure-node") +def _(): + assert_running("onion-chef") + # FIXME: use 'wait_for_unit' because 'create-web-index' always fails during startup due # to incomplete unit dependencies. # 'create-web-index' implicitly tests 'nodeinfo'. @@ -139,30 +224,62 @@ def run_tests(): assert_matches(f"curl {ip('nginx')}", "nix-bitcoin") assert_matches(f"curl -L {ip('nginx')}/store", "tshirt") - machine.wait_until_succeeds(log_has_string("bitcoind-import-banlist", "Importing node banlist")) - assert_no_failure("bitcoind-import-banlist") - ### Additional tests +# Run this test before the following tests that shut down services +# (and their corresponding network namespaces). +@test("netns-isolation") +def _(): + ping_bitcoind = "ip netns exec nb-bitcoind ping -c 1 -w 1" + ping_nanopos = "ip netns exec nb-nanopos ping -c 1 -w 1" + ping_nbxplorer = "ip netns exec nb-nbxplorer ping -c 1 -w 1" - # Current time in µs - pre_restart = succeed("date +%s.%6N").rstrip() - - # Sanity-check system by restarting all services - succeed( - "systemctl restart bitcoind clightning lnd lightning-loop spark-wallet lightning-charge nanopos liquidd" + # Positive ping tests (non-exhaustive) + machine.succeed( + "%s %s &&" % (ping_bitcoind, ip("bitcoind")) + + "%s %s &&" % (ping_bitcoind, ip("clightning")) + + "%s %s &&" % (ping_bitcoind, ip("lnd")) + + "%s %s &&" % (ping_bitcoind, ip("liquidd")) + + "%s %s &&" % (ping_bitcoind, ip("nbxplorer")) + + "%s %s &&" % (ping_nbxplorer, ip("btcpayserver")) + + "%s %s &&" % (ping_nanopos, ip("lightning-charge")) + + "%s %s &&" % (ping_nanopos, ip("nanopos")) + + "%s %s" % (ping_nanopos, ip("nginx")) ) - # Now that the bitcoind restart triggered a banlist import restart, check that - # re-importing already banned addresses works - machine.wait_until_succeeds( - log_has_string(f"bitcoind-import-banlist --since=@{pre_restart}", "Importing node banlist") + # Negative ping tests (non-exhaustive) + machine.fail( + "%s %s ||" % (ping_bitcoind, ip("spark-wallet")) + + "%s %s ||" % (ping_bitcoind, ip("lightning-loop")) + + "%s %s ||" % (ping_bitcoind, ip("lightning-charge")) + + "%s %s ||" % (ping_bitcoind, ip("nanopos")) + + "%s %s ||" % (ping_bitcoind, ip("recurring-donations")) + + "%s %s ||" % (ping_bitcoind, ip("nginx")) + + "%s %s ||" % (ping_nanopos, ip("bitcoind")) + + "%s %s ||" % (ping_nanopos, ip("clightning")) + + "%s %s ||" % (ping_nanopos, ip("lnd")) + + "%s %s ||" % (ping_nanopos, ip("lightning-loop")) + + "%s %s ||" % (ping_nanopos, ip("liquidd")) + + "%s %s ||" % (ping_nanopos, ip("electrs")) + + "%s %s ||" % (ping_nanopos, ip("spark-wallet")) + + "%s %s ||" % (ping_nanopos, ip("recurring-donations")) + + "%s %s" % (ping_nanopos, ip("btcpayserver")) ) - assert_no_failure("bitcoind-import-banlist") - prestop() + # test that netns-exec can't be run for unauthorized namespace + machine.fail("netns-exec nb-electrs ip a") - ### Test duplicity + # test that netns-exec drops capabilities + assert_full_match( + "su operator -c 'netns-exec nb-bitcoind capsh --print | grep Current '", "Current: =\n" + ) + # test that netns-exec can not be executed by users that are not operator + machine.fail("sudo -u clightning netns-exec nb-bitcoind ip a") + + +# Impure: stops bitcoind (and dependent services) +@test("backups") +def _(): succeed("systemctl stop bitcoind") succeed("systemctl start duplicity") machine.wait_until_succeeds(log_has_string("duplicity", "duplicity.service: Succeeded.")) @@ -181,23 +298,50 @@ def run_tests(): assert "var/backup/postgresql/btcpaydb.sql.gz" in files -def test_security(): - assert_running("setup-secrets") - # Unused secrets should be inaccessible - succeed('[[ $(stat -c "%U:%G %a" /secrets/dummy) = "root:root 440" ]]') +# Impure: restarts services +@test("banlist-and-restart") +def _(): + machine.wait_until_succeeds(log_has_string("bitcoind-import-banlist", "Importing node banlist")) + assert_no_failure("bitcoind-import-banlist") - # Access to '/proc' should be restricted - machine.succeed("grep -Fq hidepid=2 /proc/mounts") + # Current time in µs + pre_restart = succeed("date +%s.%6N").rstrip() - machine.wait_for_unit("bitcoind") - # `systemctl status` run by unprivileged users shouldn't leak cgroup info - assert_matches( - "sudo -u electrs systemctl status bitcoind 2>&1 >/dev/null", - "Failed to dump process list for 'bitcoind.service', ignoring: Access denied", + # Sanity-check system by restarting all services + succeed( + "systemctl restart bitcoind clightning lnd lightning-loop spark-wallet lightning-charge nanopos liquidd" ) - # The 'operator' with group 'proc' has full access - assert_full_match("sudo -u operator systemctl status bitcoind 2>&1 >/dev/null", "") + + # Now that the bitcoind restart triggered a banlist import restart, check that + # re-importing already banned addresses works + machine.wait_until_succeeds( + log_has_string(f"bitcoind-import-banlist --since=@{pre_restart}", "Importing node banlist") + ) + assert_no_failure("bitcoind-import-banlist") -def ip(_): - return "127.0.0.1" +if "netns-isolation" in enabled_tests: + netns_ips = { + "bitcoind": "169.254.1.12", + "clightning": "169.254.1.13", + "lnd": "169.254.1.14", + "liquidd": "169.254.1.15", + "electrs": "169.254.1.16", + "spark-wallet": "169.254.1.17", + "lightning-charge": "169.254.1.18", + "nanopos": "169.254.1.19", + "recurring-donations": "169.254.1.20", + "nginx": "169.254.1.21", + "lightning-loop": "169.254.1.22", + "nbxplorer": "169.254.1.23", + "btcpayserver": "169.254.1.24", + } + + def ip(netns): + return netns_ips[netns] + + +else: + + def ip(_): + return "127.0.0.1" From be2127ae5be17b89868ff98f3df4c25bfbf4a8d6 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:21 +0200 Subject: [PATCH 11/27] test: fix noConnections configs - bitcoind: remove mkForce because otherwise the whole extraConfig is replaced by the value of mkForce. - liquidd: don't disable 'listen' because it is entirely benign in offline mode, we also allow it for bitcoind. --- test/tests.nix | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/tests.nix b/test/tests.nix index 945d319..cb8aa4e 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -19,7 +19,7 @@ let testEnv = rec { tests.bitcoind = cfg.bitcoind.enable; services.bitcoind = { enable = true; - extraConfig = mkIf config.test.noConnections (mkForce "connect=0"); + extraConfig = mkIf config.test.noConnections "connect=0"; }; tests.clightning = cfg.clightning.enable; @@ -36,10 +36,7 @@ let testEnv = rec { tests.electrs = cfg.electrs.enable; tests.liquidd = cfg.liquidd.enable; - services.liquidd = optionalAttrs config.test.noConnections { - listen = mkForce false; - extraConfig = "noconnect=1"; - }; + services.liquidd.extraConfig = mkIf config.test.noConnections "connect=0"; tests.btcpayserver = cfg.btcpayserver.enable; services.btcpayserver.lightningBackend = "lnd"; From ac6cee5c128990e7a5c661bb634b461812a92f70 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:23 +0200 Subject: [PATCH 12/27] pkgs: add extra-container --- examples/shell.nix | 7 +------ pkgs/default.nix | 1 + pkgs/extra-container/default.nix | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 pkgs/extra-container/default.nix diff --git a/examples/shell.nix b/examples/shell.nix index 2298134..66d0f3b 100644 --- a/examples/shell.nix +++ b/examples/shell.nix @@ -11,11 +11,6 @@ let nixpkgs = import nixpkgs-path {}; nix-bitcoin = nixpkgs.callPackage nix-bitcoin-path {}; - extraContainer = nixpkgs.callPackage (builtins.fetchTarball { - url = "https://github.com/erikarvstedt/extra-container/archive/6cced2c26212cc1c8cc7cac3547660642eb87e71.tar.gz"; - sha256 = "0qr41mma2iwxckdhqfabw3vjcbp2ffvshnc3k11kwriwj14b766v"; - }) {}; - nix-bitcoin-unpacked = (import {}).runCommand "nix-bitcoin-src" {} '' mkdir $out; tar xf ${builtins.fetchurl nix-bitcoin-release} -C $out ''; @@ -25,7 +20,7 @@ with nixpkgs; stdenv.mkDerivation rec { name = "nix-bitcoin-environment"; - buildInputs = [ nix-bitcoin.nixops19_09 figlet extraContainer ]; + buildInputs = [ nix-bitcoin.nixops19_09 nix-bitcoin.extra-container figlet ]; shellHook = '' export NIX_PATH="nixpkgs=${nixpkgs-path}:nix-bitcoin=${toString nix-bitcoin-path}:." diff --git a/pkgs/default.nix b/pkgs/default.nix index fa30d02..26031b4 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -13,6 +13,7 @@ nixops19_09 = pkgs.callPackage ./nixops { }; netns-exec = pkgs.callPackage ./netns-exec { }; lightning-loop = pkgs.callPackage ./lightning-loop { }; + extra-container = pkgs.callPackage ./extra-container { }; pinned = import ./pinned.nix; diff --git a/pkgs/extra-container/default.nix b/pkgs/extra-container/default.nix new file mode 100644 index 0000000..0cccec0 --- /dev/null +++ b/pkgs/extra-container/default.nix @@ -0,0 +1,25 @@ +{ stdenv, lib }: + +stdenv.mkDerivation rec { + name = "extra-container-${version}"; + version = "0.3"; + + src = builtins.fetchTarball { + url = "https://github.com/erikarvstedt/extra-container/archive/6cced2c26212cc1c8cc7cac3547660642eb87e71.tar.gz"; + sha256 = "0qr41mma2iwxckdhqfabw3vjcbp2ffvshnc3k11kwriwj14b766v"; + }; + + buildCommand = '' + install -D $src/extra-container $out/bin/extra-container + patchShebangs $out/bin + install $src/eval-config.nix -Dt $out/src + sed -i "s|evalConfig=.*|evalConfig=$out/src/eval-config.nix|" $out/bin/extra-container + ''; + + meta = with lib; { + description = "Run declarative containers without full system rebuilds"; + homepage = https://github.com/erikarvstedt/extra-container; + license = licenses.mit; + maintainers = [ maintainers.earvstedt ]; + }; +} From 572967d3adc5f7be9d9fff1bddcf8f048021ccb8 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:24 +0200 Subject: [PATCH 13/27] extra-container: pre-release -> 0.5-pre --- pkgs/extra-container/default.nix | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pkgs/extra-container/default.nix b/pkgs/extra-container/default.nix index 0cccec0..9f93667 100644 --- a/pkgs/extra-container/default.nix +++ b/pkgs/extra-container/default.nix @@ -1,19 +1,30 @@ -{ stdenv, lib }: +{ stdenv, lib, nixos-container, openssh +, glibcLocales +}: stdenv.mkDerivation rec { name = "extra-container-${version}"; - version = "0.3"; + version = "0.5-pre"; src = builtins.fetchTarball { - url = "https://github.com/erikarvstedt/extra-container/archive/6cced2c26212cc1c8cc7cac3547660642eb87e71.tar.gz"; - sha256 = "0qr41mma2iwxckdhqfabw3vjcbp2ffvshnc3k11kwriwj14b766v"; + url = "https://github.com/erikarvstedt/extra-container/archive/${version}.tar.gz"; + sha256 = "0gdy2dpqrdv7f4kyqz88j34x1p2fpav04kznv41hwqq88hmzap90"; }; buildCommand = '' install -D $src/extra-container $out/bin/extra-container patchShebangs $out/bin - install $src/eval-config.nix -Dt $out/src - sed -i "s|evalConfig=.*|evalConfig=$out/src/eval-config.nix|" $out/bin/extra-container + share=$out/share/extra-container + install $src/eval-config.nix -Dt $share + + # Use existing PATH for systemctl and machinectl (for nixos-container) + scriptPath="export PATH=${lib.makeBinPath [ nixos-container openssh ]}:\$PATH" + + sed -i \ + -e "s|evalConfig=.*|evalConfig=$share/eval-config.nix|" \ + -e "s|LOCALE_ARCHIVE=.*|LOCALE_ARCHIVE=${glibcLocales}/lib/locale/locale-archive|" \ + -e "2i$scriptPath" \ + $out/bin/extra-container ''; meta = with lib; { From b552d17d553f99307d290b8c783adb52407db263 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:25 +0200 Subject: [PATCH 14/27] run-tests: fix arg error messages $1 was not substituted due to single quotes. --- test/run-tests.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/run-tests.sh b/test/run-tests.sh index 13a5d5c..2275060 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -39,7 +39,7 @@ while :; do shift shift else - >&2 echo 'Error: "$1" requires an argument.' + >&2 echo "Error: $1 requires an argument." exit 1 fi ;; @@ -49,7 +49,7 @@ while :; do shift shift else - >&2 echo 'Error: "$1" requires an argument.' + >&2 echo "Error: $1 requires an argument." exit 1 fi ;; From e7c397a48518d89825a52293812bef6360b53e7b Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:26 +0200 Subject: [PATCH 15/27] run-tests: rename scriptDir -> testDir Needed for container support. --- test/run-tests.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/run-tests.sh b/test/run-tests.sh index 2275060..410f566 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -62,9 +62,9 @@ numCPUs=${numCPUs:-$(nproc)} # Min. 800 MiB needed to avoid 'out of memory' errors memoryMiB=${memoryMiB:-2048} -scriptDir=$(cd "${BASH_SOURCE[0]%/*}" && pwd) +testDir=$(cd "${BASH_SOURCE[0]%/*}" && pwd) -export NIX_PATH=nixpkgs=$(nix eval --raw -f "$scriptDir/../pkgs/nixpkgs-pinned.nix" nixpkgs) +export NIX_PATH=nixpkgs=$(nix eval --raw -f "$testDir/../pkgs/nixpkgs-pinned.nix" nixpkgs) # Run the test. No temporary files are left on the host system. run() { @@ -72,7 +72,7 @@ run() { export TMPDIR=$(mktemp -d /tmp/nix-bitcoin-test.XXX) trap "rm -rf $TMPDIR" EXIT - nix-build --out-link $TMPDIR/driver -E "(import \"$scriptDir/tests.nix\" { scenario = \"$scenario\"; }).vm" -A driver + nix-build --out-link $TMPDIR/driver -E "(import \"$testDir/tests.nix\" { scenario = \"$scenario\"; }).vm" -A driver # Variable 'tests' contains the Python code that is executed by the driver on startup if [[ $1 == --interactive ]]; then @@ -134,7 +134,7 @@ exprForCI() { vmTestNixExpr() { cat < Date: Sun, 27 Sep 2020 12:43:27 +0200 Subject: [PATCH 16/27] run-tests: pass script args verbatim to command without word splitting Needed for the upcoming 'container' command --- test/run-tests.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/run-tests.sh b/test/run-tests.sh index 410f566..24ef18d 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -157,4 +157,6 @@ if [[ $1 && $1 != build ]]; then : ${scenario:=default} fi -eval "${@:-build}" +command="${1:-build}" +shift || true +$command "$@" From fcc67da9f42928a8cd8e18b76a6f1cf702ca555a Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:28 +0200 Subject: [PATCH 17/27] test: add container support --- test/lib/make-container.sh | 89 ++++++++++++++++++++++++++++++++++++++ test/lib/make-test.nix | 10 +++++ test/lib/test-lib.nix | 11 ++++- test/run-tests.sh | 12 +++++ test/tests.nix | 8 ++++ 5 files changed, 129 insertions(+), 1 deletion(-) create mode 100755 test/lib/make-container.sh diff --git a/test/lib/make-container.sh b/test/lib/make-container.sh new file mode 100755 index 0000000..327f13d --- /dev/null +++ b/test/lib/make-container.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +# Usage: +# +# run-tests.sh [--scenario|-s ] container +# +# Start container and start a shell session with helper commands +# for accessing the container. +# A short command documentation is printed at the start of the session. +# The container is destroyed after exiting the shell. +# An existing container is destroyed before starting. +# +# Supported arguments: +# +# --destroy|-d to destroy +# +# When `run-tests.sh container` from inside an existing shell session, +# the current container is updated without restarting by switching +# its NixOS configuration. +# Use this arg to destroy and restart the container instead. +# +# --no-destroy|-n +# +# By default, all commands destroy an existing container before starting and, +# when appropriate, before exiting. +# This ensures that containers start with no leftover filesystem state from +# previous runs and that containers don't consume system resources after use. +# This args disables auto-destructing containers. +# +# +# run-tests.sh container --run|-r c systemctl status bitcoind +# +# Run a command in the shell session environmentand exit. +# Destroy the container afterwards. +# All arguments following `--run` are used as a command. +# Supports argument '--no-destroy|-n' (see above for an explanation). +# +# Example: Start shell inside container +# run-tests.sh container --run c +# +# +# run-tests.sh [--scenario|-s ] container --command|--c +# +# Provide a custom extra-container command. +# +# Example: +# run-tests.sh container --command create -s +# Create and start a container without a shell. +# +# +# All extra args are passed to extra-container (unless --command is used): +# run-tests.sh container --build-args --builders 'ssh://worker - - 8' + +set -euo pipefail + +if [[ $EUID != 0 ]]; then + # NixOS containers require root permissions. + # By using sudo here and not at the user's call-site extra-container can detect if it is running + # inside an existing shell session (by checking an internal environment variable). + exec sudo scenario="$scenario" testDir="$testDir" NIX_PATH="$NIX_PATH" PATH="$PATH" \ + scenarioOverridesFile="${scenarioOverridesFile:-}" "$testDir/lib/make-container.sh" "$@" +fi + +export containerName=nb-test +containerCommand=shell + +while [[ $# > 0 ]]; do + case $1 in + --command|-c) + shift + containerCommand=$1 + shift + ;; + *) + break + esac +done + +containerBin=$(type -P extra-container) || true +if [[ ! ($containerBin && $(realpath $containerBin) == *extra-container-0.5*) ]]; then + echo "Building extra-container. Skip this step by adding extra-container 0.5 to PATH." + nix-build --out-link /tmp/extra-container "$testDir"/../pkgs -A extra-container >/dev/null + export PATH="/tmp/extra-container/bin${PATH:+:}$PATH" +fi + +read -d '' src <] container +# +# This is useful for quick experiments; containers start much faster than VMs. +# Running the Python test suite in containers is not yet supported. +# For now, creating NixOS containers requires root permissions. +# See ./lib/make-container.sh for a complete documentation. +# # To add custom scenarios, set the environment variable `scenarioOverridesFile`. set -eo pipefail @@ -108,6 +116,10 @@ debug() { run --interactive } +container() { + . "$testDir/lib/make-container.sh" "$@" +} + # Run the test by building the test derivation buildTest() { if [[ $outLinkPrefix ]]; then diff --git a/test/tests.nix b/test/tests.nix index cb8aa4e..db073a1 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -115,6 +115,14 @@ let testEnv = rec { }; }; + # Container-specific features + containerFeatures = { + # Container has WAN access and bitcoind connects to external nodes + test.container.enableWAN = true; + # See ./lib/test-lib.nix for a description + test.container.exposeLocalhost = true; + }; + adhoc = { # # You can also set the env var `scenarioOverridesFile` (used below) to define custom scenarios. From 84744f38d7ac67c1db98700be2de9e8b8a3fc8ab Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:29 +0200 Subject: [PATCH 18/27] netns test: disable backup test --- test/tests.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/tests.nix b/test/tests.nix index db073a1..cd29bc0 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -102,6 +102,9 @@ let testEnv = rec { imports = [ scenarios.secureNode ]; nix-bitcoin.netns-isolation.enable = true; tests.netns-isolation = true; + + # This test is rather slow and unaffected by netns settings + tests.backups = mkForce false; }; ## Examples / debug helper From ac95fe7c82550b6bf9b35f44cf7b9e57db2da023 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:30 +0200 Subject: [PATCH 19/27] netns test: don't test recurring-donations This service is not enabled and its netns doesn't exist. --- test/tests.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/tests.py b/test/tests.py index e324436..be591a0 100644 --- a/test/tests.py +++ b/test/tests.py @@ -252,7 +252,6 @@ def _(): + "%s %s ||" % (ping_bitcoind, ip("lightning-loop")) + "%s %s ||" % (ping_bitcoind, ip("lightning-charge")) + "%s %s ||" % (ping_bitcoind, ip("nanopos")) - + "%s %s ||" % (ping_bitcoind, ip("recurring-donations")) + "%s %s ||" % (ping_bitcoind, ip("nginx")) + "%s %s ||" % (ping_nanopos, ip("bitcoind")) + "%s %s ||" % (ping_nanopos, ip("clightning")) @@ -261,7 +260,6 @@ def _(): + "%s %s ||" % (ping_nanopos, ip("liquidd")) + "%s %s ||" % (ping_nanopos, ip("electrs")) + "%s %s ||" % (ping_nanopos, ip("spark-wallet")) - + "%s %s ||" % (ping_nanopos, ip("recurring-donations")) + "%s %s" % (ping_nanopos, ip("btcpayserver")) ) @@ -330,7 +328,6 @@ if "netns-isolation" in enabled_tests: "spark-wallet": "169.254.1.17", "lightning-charge": "169.254.1.18", "nanopos": "169.254.1.19", - "recurring-donations": "169.254.1.20", "nginx": "169.254.1.21", "lightning-loop": "169.254.1.22", "nbxplorer": "169.254.1.23", From 5a565dff6607a26698c93e18e8218a02eb618565 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:31 +0200 Subject: [PATCH 20/27] netns test: use netns ips from config --- test/tests.nix | 1 + test/tests.py | 18 ++---------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/test/tests.nix b/test/tests.nix index cd29bc0..3e90c0d 100644 --- a/test/tests.nix +++ b/test/tests.nix @@ -101,6 +101,7 @@ let testEnv = rec { netns = { imports = [ scenarios.secureNode ]; nix-bitcoin.netns-isolation.enable = true; + test.data.netns = config.nix-bitcoin.netns-isolation.netns; tests.netns-isolation = true; # This test is rather slow and unaffected by netns settings diff --git a/test/tests.py b/test/tests.py index be591a0..6d1f2e3 100644 --- a/test/tests.py +++ b/test/tests.py @@ -319,23 +319,9 @@ def _(): if "netns-isolation" in enabled_tests: - netns_ips = { - "bitcoind": "169.254.1.12", - "clightning": "169.254.1.13", - "lnd": "169.254.1.14", - "liquidd": "169.254.1.15", - "electrs": "169.254.1.16", - "spark-wallet": "169.254.1.17", - "lightning-charge": "169.254.1.18", - "nanopos": "169.254.1.19", - "nginx": "169.254.1.21", - "lightning-loop": "169.254.1.22", - "nbxplorer": "169.254.1.23", - "btcpayserver": "169.254.1.24", - } - def ip(netns): - return netns_ips[netns] + def ip(name): + return test_data["netns"][name]["address"] else: From c9251e72a1ccd8c9d325427417c55e0698672428 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:32 +0200 Subject: [PATCH 21/27] README: add run-tests.sh to examples --- README.md | 26 ++++++++++++++++++++++++-- test/run-tests.sh | 3 +++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 04e4f36..b505016 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,11 @@ The goal is to make it easy to deploy a reasonably secure Bitcoin node with a us It should allow managing bitcoin (the currency) effectively and providing public infrastructure. It should be a reproducible and extensible platform for applications building on Bitcoin. -Example +Examples --- The easiest way to try out nix-bitcoin is to use one of the provided examples. -``` +```bash git clone https://github.com/fort-nix/nix-bitcoin cd nix-bitcoin/examples/ nix-shell @@ -48,6 +48,28 @@ shut down immediately. They leave no traces (outside of `/nix/store`) on the hos NixOps can be used to deploy to various other backends like cloud providers.\ Requires: [Nix](https://nixos.org/nix/), [VirtualBox](https://www.virtualbox.org) +#### Tests +The internal test suite is also useful for exploring features. +The following `run-tests.sh` commands leave no traces (outside of `/nix/store`) on +the host system. + +```bash +git clone https://github.com/fort-nix/nix-bitcoin +cd nix-bitcoin/test + +# Run a Python test shell inside a VM node +./run-tests.sh debug +print(succeed("systemctl status bitcoind")) + +# Run a node in a container. Requires systemd and root privileges. +./run-tests.sh container +c systemctl status bitcoind + +# Explore a single feature +./run-tests.sh --scenario electrs container +``` +See [`run-tests.sh`](test/run-tests.sh) for a complete documentation. + Available modules --- By default the `configuration.nix` provides: diff --git a/test/run-tests.sh b/test/run-tests.sh index d360419..8608318 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -13,6 +13,9 @@ # When is undefined, the test is run with an adhoc scenario # where services. is enabled. # +# Example: +# ./run-tests.sh -s electrs +# # Run test and link results to avoid garbage collection # ./run-tests.sh [--scenario ] --out-link-prefix /tmp/nix-bitcoin-test build # From 36358066e440d48378130adaf6507174bf60e1a3 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:33 +0200 Subject: [PATCH 22/27] spark-wallet: don't disable tor when onion-service is disabled This fixes modules-only usage. We can leave enabling tor and tor.client to secure-node.nix, on which spark-wallet has a strict dependency. --- modules/spark-wallet.nix | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/spark-wallet.nix b/modules/spark-wallet.nix index 5887eb0..8a0f6a1 100644 --- a/modules/spark-wallet.nix +++ b/modules/spark-wallet.nix @@ -71,9 +71,6 @@ in { }; users.groups.spark-wallet = {}; - services.tor.enable = cfg.onion-service; - # requires client functionality for Bitcoin rate lookup - services.tor.client.enable = true; services.tor.hiddenServices.spark-wallet = mkIf cfg.onion-service { map = [{ port = 80; toPort = 9737; toHost = cfg.host; From bb763d6a2613f2295077ee5e1619ba04deba0382 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:34 +0200 Subject: [PATCH 23/27] run-tests: add 'eval' command --- test/run-tests.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/run-tests.sh b/test/run-tests.sh index 8608318..5f99609 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -119,6 +119,11 @@ debug() { run --interactive } +evalTest() { + nix eval --raw "($(vmTestNixExpr)).outPath" + echo # nix eval doesn't print a newline +} + container() { . "$testDir/lib/make-container.sh" "$@" } @@ -174,4 +179,7 @@ fi command="${1:-build}" shift || true +if [[ $command == eval ]]; then + command=evalTest +fi $command "$@" From 03f8dbba472747f77ce3a14b4ee60f79f9531537 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:35 +0200 Subject: [PATCH 24/27] test: add non-secure-node eval test This tests that the modules work without the secure-node template. The test currently fails at runtime, but evaluating already helps catching module-related errors. --- .travis.yml | 7 +++++++ test/run-tests.sh | 1 + 2 files changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index c3fe269..907f19b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ env: jobs: - TestModules=1 STABLE=1 SCENARIO=default - TestModules=1 STABLE=1 SCENARIO=netns + - EvalModules=1 STABLE=1 - PKG=hwi STABLE=1 - PKG=hwi STABLE=0 - PKG=lightning-charge STABLE=1 @@ -34,6 +35,12 @@ env: - PKG=joinmarket STABLE=0 script: - printf '%s (%s)\n' "$NIX_PATH" "$VER" + # + - | + if [[ $EvalModules ]]; then + test/run-tests.sh --scenario full eval + travis_terminate 0 + fi - | getBuildExpr() { if [[ $TestModules ]]; then diff --git a/test/run-tests.sh b/test/run-tests.sh index 5f99609..8e9b5e6 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -169,6 +169,7 @@ build() { else scenario=default buildTest "$@" scenario=netns buildTest "$@" + scenario=full evalTest "$@" fi } From 04075b108ca3c219bbb6c625b9938b1d1fbc8232 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 27 Sep 2020 12:43:37 +0200 Subject: [PATCH 25/27] test: use QEMU from stable nixpkgs --- test/lib/make-test-vm.nix | 10 ---------- test/run-tests.sh | 9 +++++++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/test/lib/make-test-vm.nix b/test/lib/make-test-vm.nix index 860751a..26c85ad 100644 --- a/test/lib/make-test-vm.nix +++ b/test/lib/make-test-vm.nix @@ -3,11 +3,6 @@ testArgs: let pkgs = import { config = {}; overlays = []; }; - pkgs19_09 = import (pkgs.fetchzip { - url = "https://github.com/NixOS/nixpkgs-channels/archive/a7ceb2536ab11973c59750c4c48994e3064a75fa.tar.gz"; - sha256 = "0hka65f31njqpq7i07l22z5rs7lkdfcl4pbqlmlsvnysb74ynyg1"; - }) { config = {}; overlays = []; }; - test = (import "${pkgs.path}/nixos/tests/make-test-python.nix") testArgs; fixedTest = { system ? builtins.currentSystem, ... }@args: @@ -23,11 +18,6 @@ let exec ${pkgs.python3Packages.black}/bin/black $extraArgs "$@" ''; }; - - # QEMU 4.20 from unstable fails on Travis build nodes with message - # "error: failed to set MSR 0x48b to 0x159ff00000000" - # Use version 4.0.1 instead. - inherit (pkgs19_09) qemu_test; }; in test (args // { pkgs = pkgsFixed; }); diff --git a/test/run-tests.sh b/test/run-tests.sh index 8e9b5e6..dd1bed6 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -149,14 +149,19 @@ exprForCI() { ((memAvailableMiB < memoryMiB)) && memoryMiB=$memAvailableMiB >&2 echo "VM stats: CPUs: $numCPUs, memory: $memoryMiB MiB" >&2 echo "Host memory total: $((memTotalKiB / 1024)) MiB, available: $memAvailableMiB MiB" - vmTestNixExpr + + # VMX is usually not available on CI nodes due to recursive virtualisation. + # Explicitly disable VMX, otherwise QEMU 4.20 fails with message + # "error: failed to set MSR 0x48b to 0x159ff00000000" + vmTestNixExpr "-cpu host,-vmx" } vmTestNixExpr() { + extraQEMUOpts="$1" cat < Date: Sun, 11 Oct 2020 20:02:24 +0200 Subject: [PATCH 26/27] examples: enable running outside of working dir --- examples/deploy-container.sh | 1 + examples/deploy-nixops.sh | 1 + examples/deploy-qemu-vm.sh | 1 + 3 files changed, 3 insertions(+) diff --git a/examples/deploy-container.sh b/examples/deploy-container.sh index f9fc494..5ec5439 100755 --- a/examples/deploy-container.sh +++ b/examples/deploy-container.sh @@ -19,6 +19,7 @@ fi if [[ ! -v IN_NIX_SHELL ]]; then echo "Running script in nix shell env..." + cd "${BASH_SOURCE[0]%/*}" exec nix-shell --run "${BASH_SOURCE[0]}" fi diff --git a/examples/deploy-nixops.sh b/examples/deploy-nixops.sh index 155cc5e..780fc00 100755 --- a/examples/deploy-nixops.sh +++ b/examples/deploy-nixops.sh @@ -10,6 +10,7 @@ set -euo pipefail if [[ ! -v IN_NIX_SHELL ]]; then echo "Running script in nix shell env..." + cd "${BASH_SOURCE[0]%/*}" exec nix-shell --run "${BASH_SOURCE[0]}" fi diff --git a/examples/deploy-qemu-vm.sh b/examples/deploy-qemu-vm.sh index 01ad0df..7545807 100755 --- a/examples/deploy-qemu-vm.sh +++ b/examples/deploy-qemu-vm.sh @@ -13,6 +13,7 @@ set -euo pipefail if [[ ! -v IN_NIX_SHELL ]]; then echo "Running script in nix shell env..." + cd "${BASH_SOURCE[0]%/*}" exec nix-shell --run "${BASH_SOURCE[0]}" fi From 1cc432a1363eaafcb200b4ad3a4e7cad989fde26 Mon Sep 17 00:00:00 2001 From: Erik Arvstedt Date: Sun, 11 Oct 2020 20:02:25 +0200 Subject: [PATCH 27/27] examples/deploy-container: use new extra-container features --- README.md | 2 +- examples/deploy-container.sh | 88 ++++++++++++++---------------------- 2 files changed, 34 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index b505016..b7242f7 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ shut down immediately. They leave no traces (outside of `/nix/store`) on the hos - [`./deploy-container.sh`](examples/deploy-container.sh) creates a [NixOS container](https://github.com/erikarvstedt/extra-container).\ This is the fastest way to set up a node.\ - Requires: [NixOS](https://nixos.org/) + Requires: [Nix](https://nixos.org/), a systemd-based Linux distro and root privileges - [`./deploy-qemu-vm.sh`](examples/deploy-qemu-vm.sh) creates a QEMU VM.\ Requires: [Nix](https://nixos.org/nix/) diff --git a/examples/deploy-container.sh b/examples/deploy-container.sh index 5ec5439..535e246 100755 --- a/examples/deploy-container.sh +++ b/examples/deploy-container.sh @@ -9,11 +9,8 @@ set -euo pipefail # script in the interactive shell. if [[ $(sysctl -n net.ipv4.ip_forward) != 1 ]]; then - echo "Error: IP forwarding (net.ipv4.ip_forward) is not enabled" - exit 1 -fi -if [[ ! -e /run/current-system/nixos-version ]]; then - echo "Error: This script needs NixOS to run" + echo "Error: IP forwarding (net.ipv4.ip_forward) is not enabled." + echo "Needed for container WAN access." exit 1 fi @@ -23,54 +20,11 @@ if [[ ! -v IN_NIX_SHELL ]]; then exec nix-shell --run "${BASH_SOURCE[0]}" fi -# Cleanup on exit -cleanup() { - echo - echo "Deleting container..." - sudo extra-container destroy demo-node -} -trap "cleanup" EXIT - -# Build container. -# You can re-run this command with a changed container config. -# The running container is then switched to the new config. -# Learn more: https://github.com/erikarvstedt/extra-container -# -sudo extra-container create --start <<'EOF' -{ pkgs, lib, ... }: let - containerName = "demo-node"; # container name length is limited to 11 chars - localAddress = "10.250.0.2"; # container address - hostAddress = "10.250.0.1"; -in { - containers.${containerName} = { - privateNetwork = true; - inherit localAddress hostAddress; - config = { pkgs, config, lib, ... }: { - imports = [ - - - ]; - # Speed up evaluation - documentation.nixos.enable = false; - }; - }; - # Allow WAN access - systemd.services."container@${containerName}" = { - preStart = "${pkgs.iptables}/bin/iptables -w -t nat -A POSTROUTING -s ${localAddress} -j MASQUERADE"; - # Delete rule - postStop = "${pkgs.iptables}/bin/iptables -w -t nat -D POSTROUTING -s ${localAddress} -j MASQUERADE || true"; - }; -} -EOF -# Run command in container -c() { - if [[ $# > 0 ]]; then - sudo extra-container run demo-node -- "$@" | cat; - else - sudo nixos-container root-login demo-node - fi -} +# Uncomment to start a container shell session +# interactive=1 +# These commands can also be executed interactively in a shell session +demoCmds=' echo echo "Bitcoind service:" c systemctl status bitcoind @@ -86,8 +40,32 @@ c nodeinfo echo echo "Bitcoind data dir:" sudo ls -al /var/lib/containers/demo-node/var/lib/bitcoind +' -# Uncomment to start a shell session here -# . start-bash-session.sh +if [[ ${interactive:-} ]]; then + runCmd= +else + runCmd=(--run bash -c "$demoCmds") +fi -# Cleanup happens at exit (see above) +# Build container. +# Learn more: https://github.com/erikarvstedt/extra-container +# +read -d '' src <<'EOF' || true +{ pkgs, lib, ... }: { + containers.demo-node = { + extra.addressPrefix = "10.250.0"; + extra.enableWAN = true; + config = { pkgs, config, lib, ... }: { + imports = [ + + + ]; + }; + }; +} +EOF +$([[ $EUID = 0 ]] || echo sudo "PATH=$PATH" "NIX_PATH=$NIX_PATH") \ + $(type -P extra-container) shell -E "$src" "${runCmd[@]}" + +# The container is automatically deleted at exit