Compare commits

..
Author SHA1 Message Date
dboreham 758ec168bd Setup command execution (#469) 2023-07-29 22:38:46 -06:00
dboreham 63a94bae0e Use latest stable lighthouse release (#468) 2023-07-28 09:56:14 -06:00
dboreham 29fc611885 Dboreham/mainnet eth (#464) 2023-07-25 10:16:19 -06:00
dboreham 1f9131ff5a Add deployment scripting (#444) 2023-07-23 20:54:05 -06:00
dboreham 950857fa84 Add --delete-volumes option to the down command (#461) 2023-07-20 06:25:31 -06:00
dboreham 54f50aa09e Move files into config subdirectory (#460) 2023-07-20 05:37:51 -06:00
telackey a5945c9e59 Back to Github-based actions. (#453)
* Back to Github-based actions.

* Use fork.

* Use new release plugin tag.
2023-07-19 20:40:04 -06:00
dboreham c8b4f89335 Copy all referenced config files into deployment dir (#452) 2023-07-18 08:59:07 -06:00
37 changed files with 775 additions and 217 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- name: "Clone project repository"
uses: actions/checkout@v3
- name: "Install Python"
uses: cerc-io/setup-python@v4
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: "Print Python version"
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
build_tag=$(./scripts/create_build_tag_file.sh)
echo "build-tag=v${build_tag}" >> $GITHUB_OUTPUT
- name: "Install Python"
uses: cerc-io/setup-python@v4
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: "Print Python version"
@@ -37,7 +37,7 @@ jobs:
run: |
cp ${{ steps.build.outputs.package-file }} ./laconic-so
- name: "Create release"
uses: cerc-io/action-gh-release@gitea-v1
uses: https://gitea.com/cerc-io/action-gh-release@gitea-v2
with:
tag_name: ${{ steps.build-info.outputs.build-tag }}
# On the publish test branch, mark our release as a draft
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- name: "Clone project repository"
uses: actions/checkout@v3
- name: "Install Python"
uses: cerc-io/setup-python@v4
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: "Print Python version"
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- name: "Clone project repository"
uses: actions/checkout@v3
- name: "Install Python"
uses: cerc-io/setup-python@v4
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: "Print Python version"
+5 -6
View File
@@ -15,9 +15,8 @@
import os
from abc import ABC, abstractmethod
from app.deploy import get_stack_status
from decouple import config
from .deploy import get_stack_status
from .util import _log
def get_stack(config, stack):
@@ -50,7 +49,7 @@ class package_registry_stack(base_stack):
url_from_environment = os.environ.get("CERC_NPM_REGISTRY_URL")
if url_from_environment:
if self.config.verbose:
_log(f"Using package registry url from CERC_NPM_REGISTRY_URL: {url_from_environment}")
print(f"Using package registry url from CERC_NPM_REGISTRY_URL: {url_from_environment}")
self.url = url_from_environment
else:
# Otherwise we expect to use the local package-registry stack
@@ -59,13 +58,13 @@ class package_registry_stack(base_stack):
if registry_running:
# If it is available, get its mapped port and construct its URL
if self.config.debug:
_log("Found local package registry stack is up")
print("Found local package registry stack is up")
# TODO: get url from deploy-stack
self.url = "http://gitea.local:3000/api/packages/cerc-io/npm/"
else:
# If not, print a message about how to start it and return fail to the caller
_log("ERROR: The package-registry stack is not running, and no external registry specified with CERC_NPM_REGISTRY_URL")
_log("ERROR: Start the local package registry with: laconic-so --stack package-registry deploy-system up")
print("ERROR: The package-registry stack is not running, and no external registry specified with CERC_NPM_REGISTRY_URL")
print("ERROR: Start the local package registry with: laconic-so --stack package-registry deploy-system up")
return False
return True
+18 -18
View File
@@ -27,8 +27,8 @@ import subprocess
import click
import importlib.resources
from pathlib import Path
from .util import _log, include_exclude_check, get_parsed_stack_config
from .base import get_npm_registry_url
from app.util import include_exclude_check, get_parsed_stack_config
from app.base import get_npm_registry_url
# TODO: find a place for this
# epilog="Config provided either in .env or settings.ini or env vars: CERC_REPO_BASE_DIR (defaults to ~/cerc)"
@@ -56,18 +56,18 @@ def command(ctx, include, exclude, force_rebuild, extra_build_args):
if local_stack:
dev_root_path = os.getcwd()[0:os.getcwd().rindex("stack-orchestrator")]
_log(f'Local stack dev_root_path (CERC_REPO_BASE_DIR) overridden to: {dev_root_path}')
print(f'Local stack dev_root_path (CERC_REPO_BASE_DIR) overridden to: {dev_root_path}')
else:
dev_root_path = os.path.expanduser(config("CERC_REPO_BASE_DIR", default="~/cerc"))
if not quiet:
_log(f'Dev Root is: {dev_root_path}')
print(f'Dev Root is: {dev_root_path}')
if not os.path.isdir(dev_root_path):
_log('Dev root directory doesn\'t exist, creating')
print('Dev root directory doesn\'t exist, creating')
# See: https://stackoverflow.com/a/20885799/1701505
from . import data
from app import data
with importlib.resources.open_text(data, "container-image-list.txt") as container_list_file:
all_containers = container_list_file.read().splitlines()
@@ -79,9 +79,9 @@ def command(ctx, include, exclude, force_rebuild, extra_build_args):
containers_in_scope = all_containers
if verbose:
_log(f'Containers: {containers_in_scope}')
print(f'Containers: {containers_in_scope}')
if stack:
_log(f"Stack: {stack}")
print(f"Stack: {stack}")
# TODO: make this configurable
container_build_env = {
@@ -102,16 +102,16 @@ def command(ctx, include, exclude, force_rebuild, extra_build_args):
def process_container(container):
if not quiet:
_log(f"Building: {container}")
print(f"Building: {container}")
build_dir = os.path.join(container_build_dir, container.replace("/", "-"))
build_script_filename = os.path.join(build_dir, "build.sh")
if verbose:
_log(f"Build script filename: {build_script_filename}")
print(f"Build script filename: {build_script_filename}")
if os.path.exists(build_script_filename):
build_command = build_script_filename
else:
if verbose:
_log(f"No script file found: {build_script_filename}, using default build script")
print(f"No script file found: {build_script_filename}, using default build script")
repo_dir = container.split('/')[1]
# TODO: make this less of a hack -- should be specified in some metadata somewhere
# Check if we have a repo for this container. If not, set the context dir to the container-build subdir
@@ -120,23 +120,23 @@ def command(ctx, include, exclude, force_rebuild, extra_build_args):
build_command = os.path.join(container_build_dir, "default-build.sh") + f" {container}:local {repo_dir_or_build_dir}"
if not dry_run:
if verbose:
_log(f"Executing: {build_command} with environment: {container_build_env}")
print(f"Executing: {build_command} with environment: {container_build_env}")
build_result = subprocess.run(build_command, shell=True, env=container_build_env)
if verbose:
_log(f"Return code is: {build_result.returncode}")
print(f"Return code is: {build_result.returncode}")
if build_result.returncode != 0:
_log(f"Error running build for {container}")
print(f"Error running build for {container}")
if not continue_on_error:
_log("FATAL Error: container build failed and --continue-on-error not set, exiting")
print("FATAL Error: container build failed and --continue-on-error not set, exiting")
sys.exit(1)
else:
_log("****** Container Build Error, continuing because --continue-on-error is set")
print("****** Container Build Error, continuing because --continue-on-error is set")
else:
_log("Skipped")
print("Skipped")
for container in containers_in_scope:
if include_exclude_check(container, include, exclude):
process_container(container)
else:
if verbose:
_log(f"Excluding: {container}")
print(f"Excluding: {container}")
+3 -3
View File
@@ -25,8 +25,8 @@ from decouple import config
import click
import importlib.resources
from python_on_whales import docker, DockerException
from .base import get_stack
from .util import include_exclude_check, get_parsed_stack_config
from app.base import get_stack
from app.util import include_exclude_check, get_parsed_stack_config
builder_js_image_name = "cerc/builder-js:local"
@@ -81,7 +81,7 @@ def command(ctx, include, exclude, force_rebuild, extra_build_args):
os.makedirs(build_root_path)
# See: https://stackoverflow.com/a/20885799/1701505
from . import data
from app import data
with importlib.resources.open_text(data, "npm-package-list.txt") as package_list_file:
all_packages = package_list_file.read().splitlines()
@@ -23,10 +23,10 @@ services:
command: |
"./wait-for-it.sh -h ${CERC_L1_HOST:-$${DEFAULT_CERC_L1_HOST}} -p ${CERC_L1_PORT:-$${DEFAULT_CERC_L1_PORT}} -s -t 60 -- ./run.sh"
volumes:
- ../config/wait-for-it.sh:/app/packages/contracts-bedrock/wait-for-it.sh
- ../container-build/cerc-optimism-contracts/hardhat-tasks/verify-contract-deployment.ts:/app/packages/contracts-bedrock/tasks/verify-contract-deployment.ts
- ../container-build/cerc-optimism-contracts/hardhat-tasks/rekey-json.ts:/app/packages/contracts-bedrock/tasks/rekey-json.ts
- ../container-build/cerc-optimism-contracts/hardhat-tasks/send-balance.ts:/app/packages/contracts-bedrock/tasks/send-balance.ts
- ../config/network/wait-for-it.sh:/app/packages/contracts-bedrock/wait-for-it.sh
- ../config/optimism-contracts/hardhat-tasks/verify-contract-deployment.ts:/app/packages/contracts-bedrock/tasks/verify-contract-deployment.ts
- ../config/optimism-contracts/hardhat-tasks/rekey-json.ts:/app/packages/contracts-bedrock/tasks/rekey-json.ts
- ../config/optimism-contracts/hardhat-tasks/send-balance.ts:/app/packages/contracts-bedrock/tasks/send-balance.ts
- ../config/fixturenet-optimism/optimism-contracts/update-config.js:/app/packages/contracts-bedrock/update-config.js
- ../config/fixturenet-optimism/optimism-contracts/run.sh:/app/packages/contracts-bedrock/run.sh
- l2_accounts:/l2-accounts
@@ -120,7 +120,7 @@ services:
CERC_SCRIPT_DEBUG: ${CERC_SCRIPT_DEBUG}
CERC_L1_RPC: ${CERC_L1_RPC}
volumes:
- ../config/wait-for-it.sh:/wait-for-it.sh
- ../config/network/wait-for-it.sh:/wait-for-it.sh
- ../config/fixturenet-optimism/run-op-batcher.sh:/run-op-batcher.sh
- l2_accounts:/l2-accounts:ro
entrypoint: ["sh", "-c"]
@@ -145,7 +145,7 @@ services:
CERC_SCRIPT_DEBUG: ${CERC_SCRIPT_DEBUG}
CERC_L1_RPC: ${CERC_L1_RPC}
volumes:
- ../config/wait-for-it.sh:/wait-for-it.sh
- ../config/network/wait-for-it.sh:/wait-for-it.sh
- ../config/fixturenet-optimism/run-op-proposer.sh:/run-op-proposer.sh
- l1_deployment:/contracts-bedrock:ro
- l2_accounts:/l2-accounts:ro
@@ -0,0 +1,56 @@
services:
mainnet-eth-geth-1:
restart: always
hostname: mainnet-eth-geth-1
cap_add:
- SYS_PTRACE
environment:
CERC_REMOTE_DEBUG: "true"
CERC_RUN_STATEDIFF: ${CERC_RUN_STATEDIFF:-detect}
CERC_STATEDIFF_DB_NODE_ID: 1
CERC_SCRIPT_DEBUG: ${CERC_SCRIPT_DEBUG}
image: cerc/go-ethereum:local
entrypoint: /bin/sh
command: -c "/opt/run-geth.sh"
volumes:
- mainnet_eth_geth_1_data:/root/ethdata
- mainnet_eth_config_data:/etc/mainnet-eth
- ../config/mainnet-eth/scripts/run-geth.sh:/opt/run-geth.sh
healthcheck:
test: ["CMD", "nc", "-v", "localhost", "8545"]
interval: 30s
timeout: 10s
retries: 10
start_period: 3s
ports:
- "8545"
- "40000"
- "6060"
mainnet-eth-lighthouse-1:
restart: always
hostname: mainnet-eth-lighthouse-1
healthcheck:
test: ["CMD", "wget", "--tries=1", "--connect-timeout=1", "--quiet", "-O", "-", "http://localhost:8001/eth/v2/beacon/blocks/head"]
interval: 30s
timeout: 10s
retries: 10
start_period: 30s
environment:
EXECUTION_ENDPOINT: "http://mainnet-eth-geth-1:8551"
image: cerc/lighthouse:local
entrypoint: /bin/sh
command: -c "/opt/run-lighthouse.sh"
volumes:
- mainnet_eth_lighthouse_1_data:/var/lighthouse-data-dir
- mainnet_eth_config_data:/etc/mainnet-eth
- ../config/mainnet-eth/scripts/run-lighthouse.sh:/opt/run-lighthouse.sh
ports:
- "8001"
volumes:
mainnet_eth_config_data:
mainnet_eth_geth_1_data:
mainnet_eth_lighthouse_1_data:
@@ -2,7 +2,7 @@ services:
laconicd:
restart: no
image: cerc/laconicd:local
command: ["sh", "/docker-entrypoint-scripts.d/create-fixturenet.sh"]
command: ["/bin/sh", "-c", "while :; do sleep 600; done"]
volumes:
# The cosmos-sdk node's database directory:
- laconicd-data:/root/.laconicd/data
@@ -18,7 +18,7 @@ services:
working_dir: /scripts
command: ["sh", "mobymask-app-start.sh"]
volumes:
- ../config/wait-for-it.sh:/scripts/wait-for-it.sh
- ../config/network/wait-for-it.sh:/scripts/wait-for-it.sh
- ../config/watcher-mobymask-v2/mobymask-app-start.sh:/scripts/mobymask-app-start.sh
- peers_ids:/peers
- mobymask_deployment:/server
@@ -50,7 +50,7 @@ services:
working_dir: /scripts
command: ["sh", "mobymask-app-start.sh"]
volumes:
- ../config/wait-for-it.sh:/scripts/wait-for-it.sh
- ../config/network/wait-for-it.sh:/scripts/wait-for-it.sh
- ../config/watcher-mobymask-v2/mobymask-app-start.sh:/scripts/mobymask-app-start.sh
- peers_ids:/peers
- mobymask_deployment:/server
@@ -14,7 +14,7 @@ services:
CERC_DENY_MULTIADDRS: ${CERC_DENY_MULTIADDRS}
command: ["sh", "test-app-start.sh"]
volumes:
- ../config/wait-for-it.sh:/scripts/wait-for-it.sh
- ../config/network/wait-for-it.sh:/scripts/wait-for-it.sh
- ../config/watcher-mobymask-v2/test-app-start.sh:/scripts/test-app-start.sh
- peers_ids:/peers
ports:
@@ -44,7 +44,7 @@ services:
CERC_L2_NODE_PORT: ${CERC_L2_NODE_PORT}
command: ["sh", "deploy-and-generate-invite.sh"]
volumes:
- ../config/wait-for-it.sh:/app/packages/server/wait-for-it.sh
- ../config/network/wait-for-it.sh:/app/packages/server/wait-for-it.sh
- ../config/watcher-mobymask-v2/secrets-template.json:/app/packages/server/secrets-template.json
- ../config/watcher-mobymask-v2/deploy-and-generate-invite.sh:/app/packages/server/deploy-and-generate-invite.sh
- mobymask_deployment:/app/packages/server
+52
View File
@@ -0,0 +1,52 @@
#!/bin/sh
if [[ -n "$CERC_SCRIPT_DEBUG" ]]; then
set -x
fi
CERC_ETH_DATADIR=/root/ethdata
START_CMD="geth"
if [ "true" == "$CERC_REMOTE_DEBUG" ] && [ -x "/usr/local/bin/dlv" ]; then
START_CMD="/usr/local/bin/dlv --listen=:40000 --headless=true --api-version=2 --accept-multiclient exec /usr/local/bin/geth --continue --"
fi
# See https://linuxconfig.org/how-to-propagate-a-signal-to-child-processes-from-a-bash-script
cleanup() {
echo "Signal received, cleaning up..."
# Kill the child process first (CERC_REMOTE_DEBUG=true uses dlv which starts geth as a child process)
pkill -P ${geth_pid}
sleep 2
kill $(jobs -p)
wait
echo "Done"
}
$START_CMD \
--datadir="${CERC_ETH_DATADIR}" \
--authrpc.addr="0.0.0.0" \
--authrpc.port 8551 \
--authrpc.vhosts="*" \
--authrpc.jwtsecret="/etc/mainnet-eth/jwtsecret" \
--ws \
--ws.addr="0.0.0.0" \
--ws.origins="*" \
--ws.api="${CERC_GETH_WS_APIS:-eth,web3,net,admin,personal,debug,statediff}" \
--http.corsdomain="*" \
--gcmode full \
--txlookuplimit=0 \
--cache.preimages \
--syncmode=snap \
&
geth_pid=$!
wait $geth_pid
if [ "true" == "$CERC_KEEP_RUNNING_AFTER_GETH_EXIT" ]; then
while [ 1 -eq 1 ]; do
sleep 60
done
fi
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
if [[ -n "$CERC_SCRIPT_DEBUG" ]]; then
set -x
fi
DEBUG_LEVEL=${CERC_LIGHTHOUSE_DEBUG_LEVEL:-info}
data_dir=/var/lighthouse-data-dir
network_port=9001
http_port=8001
authrpc_port=8551
exec lighthouse \
bn \
--debug-level $DEBUG_LEVEL \
--datadir $data_dir \
--network mainnet \
--execution-endpoint $EXECUTION_ENDPOINT \
--execution-jwt /etc/mainnet-eth/jwtsecret \
--disable-deposit-contract-sync \
--checkpoint-sync-url https://beaconstate.ethstaker.cc
@@ -1,5 +1,5 @@
ARG TAG_SUFFIX="-modern"
FROM sigp/lighthouse:v4.1.0${TAG_SUFFIX}
FROM sigp/lighthouse:v4.3.0${TAG_SUFFIX}
RUN apt-get update; apt-get install bash netcat curl less jq -y;
+104
View File
@@ -0,0 +1,104 @@
# mainnet-eth
Deploys a "head-tracking" mainnet Ethereum stack comprising a [go-ethereum](https://github.com/cerc-io/go-ethereum) execution layer node and a [lighthouse](https://github.com/sigp/lighthouse) consensus layer node.
## Clone required repositories
```
$ laconic-so --stack mainnet-eth setup-repositories
```
## Build containers
```
$ laconic-so --stack mainnet-eth build-containers
```
## Create a deployment
```
$ laconic-so --stack mainnet-eth deploy init --output mainnet-eth-spec.yml
$ laconic-so deploy create --spec-file mainnet-eth-spec.yml --deployment-dir mainnet-eth-deployment
```
## Start the stack
```
$ laconic-so deployment --dir mainnet-eth-deployment start
```
Display stack status:
```
$ laconic-so deployment --dir mainnet-eth-deployment ps
Running containers:
id: f39608eca04d72d6b0f1f3acefc5ebb52908da06e221d20c7138f7e3dff5e423, name: laconic-ef641b4d13eb61ed561b19be67063241-foundry-1, ports:
id: 4052b1eddd886ae0d6b41f9ff22e68a70f267b2bfde10f4b7b79b5bd1eeddcac, name: laconic-ef641b4d13eb61ed561b19be67063241-mainnet-eth-geth-1-1, ports: 30303/tcp, 30303/udp, 0.0.0.0:49184->40000/tcp, 0.0.0.0:49185->6060/tcp, 0.0.0.0:49186->8545/tcp, 8546/tcp
id: ac331232e597944b621b3b8942ace5dafb14524302cab338ff946c7f6e5a1d52, name: laconic-ef641b4d13eb61ed561b19be67063241-mainnet-eth-lighthouse-1-1, ports: 0.0.0.0:49187->8001/tcp
```
See stack logs:
```
$ laconic-so deployment --dir mainnet-eth-deployment logs
time="2023-07-25T09:46:29-06:00" level=warning msg="The \"CERC_SCRIPT_DEBUG\" variable is not set. Defaulting to a blank string."
laconic-ef641b4d13eb61ed561b19be67063241-mainnet-eth-lighthouse-1-1 | Jul 25 15:45:13.362 INFO Logging to file path: "/var/lighthouse-data-dir/beacon/logs/beacon.log"
laconic-ef641b4d13eb61ed561b19be67063241-mainnet-eth-lighthouse-1-1 | Jul 25 15:45:13.365 INFO Lighthouse started version: Lighthouse/v4.1.0-693886b
laconic-ef641b4d13eb61ed561b19be67063241-mainnet-eth-lighthouse-1-1 | Jul 25 15:45:13.365 INFO Configured for network name: mainnet
laconic-ef641b4d13eb61ed561b19be67063241-mainnet-eth-lighthouse-1-1 | Jul 25 15:45:13.366 INFO Data directory initialised datadir: /var/lighthouse-data-dir
laconic-ef641b4d13eb61ed561b19be67063241-mainnet-eth-lighthouse-1-1 | Jul 25 15:45:13.366 INFO Deposit contract address: 0x00000000219ab540356cbb839cbe05303d7705fa, deploy_block: 11184524
laconic-ef641b4d13eb61ed561b19be67063241-mainnet-eth-lighthouse-1-1 | Jul 25 15:45:13.424 INFO Starting checkpoint sync remote_url: https://beaconstate.ethstaker.cc/, service: beacon
```
## Monitoring stack sync progress
Both go-ethereum and lighthouse will engage in an initial chain sync phase that will last up to several hours depending on hardware performance and network capacity.
Syncing can be monitored by looking for these log messages:
```
Jul 24 12:34:17.001 INFO Downloading historical blocks est_time: 5 days 11 hrs, speed: 14.67 slots/sec, distance: 6932481 slots (137 weeks 3 days), service: slot_notifier
INFO [07-24|12:14:52.493] Syncing beacon headers downloaded=145,920 left=17,617,968 eta=1h23m32.815s
INFO [07-24|12:33:15.238] Syncing: chain download in progress synced=1.86% chain=148.94MiB headers=368,640@95.03MiB bodies=330,081@40.56MiB receipts=330,081@13.35MiB eta=37m54.505s
INFO [07-24|12:35:13.028] Syncing: state download in progress synced=1.32% state=4.64GiB accounts=2,850,314@677.57MiB slots=18,663,070@3.87GiB codes=26662@111.14MiB eta=3h18m0.699s
```
Once synced up these log messages will be observed:
```
INFO Synced slot: 6952515, block: 0x5bcb…f6d9, epoch: 217266, finalized_epoch: 217264, finalized_root: 0x6342…2c5c, exec_hash: 0x8d8c…2443 (verified), peers: 31, service: slot_notifier
INFO [07-25|03:04:48.941] Imported new potential chain segment number=17,767,316 hash=84f6e7..bc2cb0 blocks=1 txs=137 mgas=16.123 elapsed=57.087ms mgasps=282.434 dirty=461.46MiB
INFO [07-25|03:04:49.042] Chain head was updated number=17,767,316 hash=84f6e7..bc2cb0 root=ca58b2..8258c1 elapsed=2.480111ms
```
## Clean up
Stop the stack:
```
$ laconic-so deployment --dir mainnet-eth-deployment stop
```
This leaves data volumes in place, allowing the stack to be subsequently re-started.
To permanently *delete* the stack's data volumes run:
```
$ laconic-so deployment --dir mainnet-eth-deployment stop --delete-data-volumes
```
After deleting the volumes, any subsequent re-start will begin chain sync from cold.
## Data volumes
Container data volumes are bind-mounted to specified paths in the host filesystem.
The default setup (generated by `laconic-so deploy init`) places the volumes in the `./data` subdirectory of the deployment directory:
```
$ cat mainnet-eth-spec.yml
stack: mainnet-eth
volumes:
mainnet_eth_config_data: ./data/mainnet_eth_config_data
mainnet_eth_geth_1_data: ./data/mainnet_eth_geth_1_data
mainnet_eth_lighthouse_1_data: ./data/mainnet_eth_lighthouse_1_data
```
A synced-up stack will consume around 900GB of data volume space:
```
$ sudo du -h mainnet-eth-deployment/data/
150M mainnet-eth-deployment/data/mainnet_eth_lighthouse_1_data/beacon/freezer_db
25G mainnet-eth-deployment/data/mainnet_eth_lighthouse_1_data/beacon/chain_db
16K mainnet-eth-deployment/data/mainnet_eth_lighthouse_1_data/beacon/network
368M mainnet-eth-deployment/data/mainnet_eth_lighthouse_1_data/beacon/logs
26G mainnet-eth-deployment/data/mainnet_eth_lighthouse_1_data/beacon
26G mainnet-eth-deployment/data/mainnet_eth_lighthouse_1_data
8.0K mainnet-eth-deployment/data/mainnet_eth_config_data
4.0K mainnet-eth-deployment/data/mainnet_eth_geth_1_data/keystore
527G mainnet-eth-deployment/data/mainnet_eth_geth_1_data/geth/chaindata/ancient/chain
527G mainnet-eth-deployment/data/mainnet_eth_geth_1_data/geth/chaindata/ancient
859G mainnet-eth-deployment/data/mainnet_eth_geth_1_data/geth/chaindata
4.8M mainnet-eth-deployment/data/mainnet_eth_geth_1_data/geth/nodes
242M mainnet-eth-deployment/data/mainnet_eth_geth_1_data/geth/ethash
669M mainnet-eth-deployment/data/mainnet_eth_geth_1_data/geth/triecache
860G mainnet-eth-deployment/data/mainnet_eth_geth_1_data/geth
860G mainnet-eth-deployment/data/mainnet_eth_geth_1_data
885G mainnet-eth-deployment/data/
```
@@ -0,0 +1,31 @@
# Copyright © 2023 Cerc
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
from secrets import token_hex
def init(ctx):
return None
def setup(ctx):
return None
def create(ctx):
# Generate the JWT secret and save to its config file
secret = token_hex(32)
jwt_file_path = ctx.deployment_dir.joinpath("data", "mainnet_eth_config_data", "jwtsecret")
with open(jwt_file_path, 'w+') as jwt_file:
jwt_file.write(secret)
+15
View File
@@ -0,0 +1,15 @@
version: "1.1"
name: mainnet-eth
decription: "Ethereum Mainnet"
repos:
- github.com/cerc-io/go-ethereum
- github.com/cerc-io/lighthouse
- github.com/dboreham/foundry
containers:
- cerc/go-ethereum
- cerc/lighthouse
- cerc/lighthouse-cli
- cerc/foundry
pods:
- mainnet-eth
- foundry
@@ -13,45 +13,39 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
import click
import os
from shutil import copyfile
import sys
from .util import get_stack_config_filename, get_parsed_deployment_spec
from app.util import get_yaml
from app.deploy_types import DeployCommandContext, DeploymentContext
from app.stack_state import State
from app.deploy_util import VolumeMapping, run_container_command
default_spec_file_content = """stack: mainnet-laconic
data_dir: /my/path
node_name: my-node-name
default_spec_file_content = """config:
node_moniker: my-node-name
chain_id: my-chain-id
"""
init_help_text = """Add helpful text here on setting config variables.
"""
def make_default_deployment_dir():
return "deployment-001"
@click.command()
@click.option("--output", required=True, help="Write yaml spec file here")
@click.pass_context
def init(ctx, output):
with open(output, "w") as output_file:
output_file.write(default_spec_file_content)
def setup(command_context: DeployCommandContext):
node_moniker = "dbdb-node"
chain_id = "laconic_81337-1"
mounts = [
VolumeMapping("./path", "~/.laconicd")
]
output, status = run_container_command(command_context.cluster_context, "laconicd", f"laconicd init {node_moniker} --chain-id {chain_id}", mounts)
@click.command()
@click.option("--spec-file", required=True, help="Spec file to use to create this deployment")
@click.option("--deployment-dir", help="Create deployment files in this directory")
@click.pass_context
def create(ctx, spec_file, deployment_dir):
# This function fails with a useful error message if the file doens't exist
parsed_spec = get_parsed_deployment_spec(spec_file)
if ctx.debug:
print(f"parsed spec: {parsed_spec}")
if deployment_dir is None:
deployment_dir = make_default_deployment_dir()
if os.path.exists(deployment_dir):
print(f"Error: {deployment_dir} already exists")
sys.exit(1)
os.mkdir(deployment_dir)
# Copy spec file and the stack file into the deployment dir
copyfile(spec_file, os.path.join(deployment_dir, os.path.basename(spec_file)))
stack_file = get_stack_config_filename(parsed_spec.stack)
copyfile(stack_file, os.path.join(deployment_dir, os.path.basename(stack_file)))
def init(command_context: DeployCommandContext):
print(init_help_text)
yaml = get_yaml()
return yaml.load(default_spec_file_content)
def get_state(command_context: DeployCommandContext):
print("Here we get state")
return State.CONFIGURED
def change_state(command_context: DeployCommandContext):
pass
+1 -4
View File
@@ -25,7 +25,4 @@ containers:
pods:
- mainnet-laconicd
- fixturenet-laconic-console
config:
cli:
key: laconicd.mykey
address: laconicd.myaddress
+54
View File
@@ -0,0 +1,54 @@
# Copyright © 2022, 2023 Cerc
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
from app.util import get_yaml
from app.deploy_types import DeployCommandContext, DeploymentContext
from app.stack_state import State
from app.deploy_util import VolumeMapping, run_container_command
import os
from pathlib import Path
default_spec_file_content = """config:
config_variable: test-value
"""
init_help_text = """Add helpful text here on setting config variables.
"""
# Output a known string to a know file in the bind mounted directory ./container-output-dir
# for test purposes -- test checks that the file was written.
def setup(command_context: DeployCommandContext, extra_args):
host_directory = "./container-output-dir"
host_directory_absolute = Path(extra_args[0]).absolute().joinpath(host_directory)
host_directory_absolute.mkdir(parents=True, exist_ok=True)
mounts = [
VolumeMapping(host_directory_absolute, "/data")
]
output, status = run_container_command(command_context, "test", "echo output-data > /data/output-file && echo success", mounts)
def init(command_context: DeployCommandContext):
print(init_help_text)
yaml = get_yaml()
return yaml.load(default_spec_file_content)
def get_state(command_context: DeployCommandContext):
print("Here we get state")
return State.CONFIGURED
def change_state(command_context: DeployCommandContext):
pass
+38 -51
View File
@@ -26,15 +26,11 @@ import subprocess
from python_on_whales import DockerClient, DockerException
import click
from pathlib import Path
from .util import _log, include_exclude_check, get_parsed_stack_config, global_options2
from .deployment_create import create as deployment_create
from .deployment_create import init as deployment_init
class DeployCommandContext(object):
def __init__(self, cluster_context, docker):
self.cluster_context = cluster_context
self.docker = docker
from app.util import include_exclude_check, get_parsed_stack_config, global_options2
from app.deploy_types import ClusterContext, DeployCommandContext
from app.deployment_create import create as deployment_create
from app.deployment_create import init as deployment_init
from app.deployment_create import setup as deployment_setup
@click.group()
@@ -57,10 +53,10 @@ def create_deploy_context(global_context, stack, include, exclude, cluster, env_
# See: https://gabrieldemarmiesse.github.io/python-on-whales/sub-commands/compose/
docker = DockerClient(compose_files=cluster_context.compose_files, compose_project_name=cluster_context.cluster,
compose_env_file=cluster_context.env_file)
return DeployCommandContext(cluster_context, docker)
return DeployCommandContext(stack, cluster_context, docker)
def up_operation(ctx, services_list):
def up_operation(ctx, services_list, stay_attached=False):
global_context = ctx.parent.parent.obj
deploy_context = ctx.obj
if not global_context.dry_run:
@@ -69,10 +65,10 @@ def up_operation(ctx, services_list):
for attr, value in container_exec_env.items():
os.environ[attr] = value
if global_context.verbose:
_log(f"Running compose up with container_exec_env: {container_exec_env}, extra_args: {services_list}")
print(f"Running compose up with container_exec_env: {container_exec_env}, extra_args: {services_list}")
for pre_start_command in cluster_context.pre_start_commands:
_run_command(global_context, cluster_context.cluster, pre_start_command)
deploy_context.docker.compose.up(detach=True, services=services_list)
deploy_context.docker.compose.up(detach=not stay_attached, services=services_list)
for post_start_command in cluster_context.post_start_commands:
_run_command(global_context, cluster_context.cluster, post_start_command)
_orchestrate_cluster_config(global_context, cluster_context.config, deploy_context.docker, container_exec_env)
@@ -82,7 +78,7 @@ def down_operation(ctx, delete_volumes, extra_args_list):
global_context = ctx.parent.parent.obj
if not global_context.dry_run:
if global_context.verbose:
_log("Running compose down")
print("Running compose down")
timeout_arg = None
if extra_args_list:
timeout_arg = extra_args_list[0]
@@ -94,7 +90,7 @@ def ps_operation(ctx):
global_context = ctx.parent.parent.obj
if not global_context.dry_run:
if global_context.verbose:
_log("Running compose ps")
print("Running compose ps")
container_list = ctx.obj.docker.compose.ps()
if len(container_list) > 0:
print("Running containers:")
@@ -120,12 +116,12 @@ def port_operation(ctx, extra_args):
extra_args_list = list(extra_args) or None
if not global_context.dry_run:
if extra_args_list is None or len(extra_args_list) < 2:
_log("Usage: port <service> <exposed-port>")
print("Usage: port <service> <exposed-port>")
sys.exit(1)
service_name = extra_args_list[0]
exposed_port = extra_args_list[1]
if global_context.verbose:
_log(f"Running compose port {service_name} {exposed_port}")
print(f"Running compose port {service_name} {exposed_port}")
mapped_port_data = ctx.obj.docker.compose.port(service_name, exposed_port)
print(f"{mapped_port_data[0]}:{mapped_port_data[1]}")
@@ -135,17 +131,17 @@ def exec_operation(ctx, extra_args):
extra_args_list = list(extra_args) or None
if not global_context.dry_run:
if extra_args_list is None or len(extra_args_list) < 2:
_log("Usage: exec <service> <cmd>")
print("Usage: exec <service> <cmd>")
sys.exit(1)
service_name = extra_args_list[0]
command_to_exec = ["sh", "-c"] + extra_args_list[1:]
container_exec_env = _make_runtime_env(global_context)
if global_context.verbose:
_log(f"Running compose exec {service_name} {command_to_exec}")
print(f"Running compose exec {service_name} {command_to_exec}")
try:
ctx.obj.docker.compose.execute(service_name, command_to_exec, envs=container_exec_env)
except DockerException as error:
_log(f"container command returned error exit status")
print(f"container command returned error exit status")
def logs_operation(ctx, extra_args):
@@ -153,7 +149,7 @@ def logs_operation(ctx, extra_args):
extra_args_list = list(extra_args) or None
if not global_context.dry_run:
if global_context.verbose:
_log("Running compose logs")
print("Running compose logs")
logs_output = ctx.obj.docker.compose.logs(services=extra_args_list if extra_args_list is not None else [])
print(logs_output)
@@ -211,15 +207,15 @@ def get_stack_status(ctx, stack):
docker = DockerClient(compose_files=cluster_context.compose_files, compose_project_name=cluster_context.cluster)
# TODO: refactor to avoid duplicating this code above
if ctx.verbose:
_log("Running compose ps")
print("Running compose ps")
container_list = docker.compose.ps()
if len(container_list) > 0:
if ctx.debug:
_log(f"Container list from compose ps: {container_list}")
print(f"Container list from compose ps: {container_list}")
return True
else:
if ctx.debug:
_log("No containers found from compose ps")
print("No containers found from compose ps")
False
@@ -237,7 +233,7 @@ def _make_cluster_context(ctx, stack, include, exclude, cluster, env_file):
if ctx.local_stack:
dev_root_path = os.getcwd()[0:os.getcwd().rindex("stack-orchestrator")]
_log(f'Local stack dev_root_path (CERC_REPO_BASE_DIR) overridden to: {dev_root_path}')
print(f'Local stack dev_root_path (CERC_REPO_BASE_DIR) overridden to: {dev_root_path}')
else:
dev_root_path = os.path.expanduser(config("CERC_REPO_BASE_DIR", default="~/cerc"))
@@ -256,14 +252,14 @@ def _make_cluster_context(ctx, stack, include, exclude, cluster, env_file):
path = os.path.realpath(sys.argv[0])
unique_cluster_descriptor = f"{path},{stack},{include},{exclude}"
if ctx.debug:
_log(f"pre-hash descriptor: {unique_cluster_descriptor}")
print(f"pre-hash descriptor: {unique_cluster_descriptor}")
hash = hashlib.md5(unique_cluster_descriptor.encode()).hexdigest()
cluster = f"laconic-{hash}"
if ctx.verbose:
_log(f"Using cluster name: {cluster}")
print(f"Using cluster name: {cluster}")
# See: https://stackoverflow.com/a/20885799/1701505
from . import data
from app import data
with resources.open_text(data, "pod-list.txt") as pod_list_file:
all_pods = pod_list_file.read().splitlines()
@@ -281,7 +277,7 @@ def _make_cluster_context(ctx, stack, include, exclude, cluster, env_file):
pods_in_scope = _convert_to_new_format(pods_in_scope)
if ctx.verbose:
_log(f"Pods: {pods_in_scope}")
print(f"Pods: {pods_in_scope}")
# Construct a docker compose command suitable for our purpose
@@ -307,22 +303,12 @@ def _make_cluster_context(ctx, stack, include, exclude, cluster, env_file):
compose_files.append(compose_file_name)
else:
if ctx.verbose:
_log(f"Excluding: {pod_name}")
print(f"Excluding: {pod_name}")
if ctx.verbose:
_log(f"files: {compose_files}")
print(f"files: {compose_files}")
return cluster_context(cluster, compose_files, pre_start_commands, post_start_commands, cluster_config, env_file)
class cluster_context:
def __init__(self, cluster, compose_files, pre_start_commands, post_start_commands, config, env_file) -> None:
self.cluster = cluster
self.compose_files = compose_files
self.pre_start_commands = pre_start_commands
self.post_start_commands = post_start_commands
self.config = config
self.env_file = env_file
return ClusterContext(cluster, compose_files, pre_start_commands, post_start_commands, cluster_config, env_file)
def _convert_to_new_format(old_pod_array):
@@ -342,7 +328,7 @@ def _convert_to_new_format(old_pod_array):
def _run_command(ctx, cluster_name, command):
if ctx.verbose:
_log(f"Running command: {command}")
print(f"Running command: {command}")
command_dir = os.path.dirname(command)
command_file = os.path.join(".", os.path.basename(command))
command_env = os.environ.copy()
@@ -351,7 +337,7 @@ def _run_command(ctx, cluster_name, command):
command_env["CERC_SCRIPT_DEBUG"] = "true"
command_result = subprocess.run(command_file, shell=True, env=command_env, cwd=command_dir)
if command_result.returncode != 0:
_log(f"FATAL Error running command: {command}")
print(f"FATAL Error running command: {command}")
sys.exit(1)
@@ -368,7 +354,7 @@ def _orchestrate_cluster_config(ctx, cluster_config, docker, container_exec_env)
for container in cluster_config:
container_config = cluster_config[container]
if ctx.verbose:
_log(f"{container} config: {container_config}")
print(f"{container} config: {container_config}")
for directive in container_config:
pd = ConfigDirective(
container_config[directive].split(".")[0],
@@ -377,7 +363,7 @@ def _orchestrate_cluster_config(ctx, cluster_config, docker, container_exec_env)
directive
)
if ctx.verbose:
_log(f"Setting {pd.destination_container}.{pd.destination_variable}"
print(f"Setting {pd.destination_container}.{pd.destination_variable}"
f" = {pd.source_container}.{pd.source_variable}")
# TODO: add a timeout
waiting_for_data = True
@@ -394,19 +380,19 @@ def _orchestrate_cluster_config(ctx, cluster_config, docker, container_exec_env)
envs=container_exec_env)
except DockerException as error:
if ctx.debug:
_log(f"Docker exception reading config source: {error}")
print(f"Docker exception reading config source: {error}")
# If the script executed failed for some reason, we get:
# "It returned with code 1"
if "It returned with code 1" in str(error):
if ctx.verbose:
_log("Config export script returned an error, re-trying")
print("Config export script returned an error, re-trying")
# If the script failed to execute (e.g. the file is not there) then we get:
# "It returned with code 2"
if "It returned with code 2" in str(error):
_log(f"Fatal error reading config source: {error}")
print(f"Fatal error reading config source: {error}")
if source_value:
if ctx.debug:
_log(f"fetched source value: {source_value}")
print(f"fetched source value: {source_value}")
destination_output = docker.compose.execute(pd.destination_container,
["sh", "-c",
f"sh /scripts/import-{pd.destination_variable}.sh"
@@ -415,8 +401,9 @@ def _orchestrate_cluster_config(ctx, cluster_config, docker, container_exec_env)
envs=container_exec_env)
waiting_for_data = False
if ctx.debug:
_log(f"destination output: {destination_output}")
print(f"destination output: {destination_output}")
command.add_command(deployment_init)
command.add_command(deployment_create)
command.add_command(deployment_setup)
+47
View File
@@ -0,0 +1,47 @@
# Copyright © 2023 Cerc
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
from typing import List
from dataclasses import dataclass
from pathlib import Path
from python_on_whales import DockerClient
@dataclass
class ClusterContext:
cluster: str
compose_files: List[str]
pre_start_commands: List[str]
post_start_commands: List[str]
config: str
env_file: str
@dataclass
class DeployCommandContext:
stack: str
cluster_context: ClusterContext
docker: DockerClient
@dataclass
class DeploymentContext:
deployment_dir: Path
command_context: DeployCommandContext
@dataclass
class VolumeMapping:
host_path: str
container_path: str
+56
View File
@@ -0,0 +1,56 @@
# Copyright © 2022, 2023 Cerc
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
import os
from typing import List
from dataclasses import dataclass
from app.deploy_types import DeployCommandContext, VolumeMapping
from app.util import get_parsed_stack_config, get_yaml, get_compose_file_dir
def _container_image_from_service(stack:str, service: str):
# Parse the compose files looking for the image name of the specified service
image_name = None
parsed_stack = get_parsed_stack_config(stack)
pods = parsed_stack["pods"]
yaml = get_yaml()
for pod in pods:
pod_file_path = os.path.join(get_compose_file_dir(), f"docker-compose-{pod}.yml")
parsed_pod_file = yaml.load(open(pod_file_path, "r"))
if "services" in parsed_pod_file:
services = parsed_pod_file["services"]
if service in services:
service_definition = services[service]
if "image" in service_definition:
image_name = service_definition["image"]
return image_name
def _volumes_to_docker(mounts: List[VolumeMapping]):
# Example from doc: [("/", "/host"), ("/etc/hosts", "/etc/hosts", "rw")]
result = []
for mount in mounts:
docker_volume = (mount.host_path, mount.container_path)
result.append(docker_volume)
return result
def run_container_command(ctx: DeployCommandContext, service: str, command: str, mounts: List[VolumeMapping]):
docker = ctx.docker
container_image = _container_image_from_service(ctx.stack, service)
docker_volumes = _volumes_to_docker(mounts)
docker_output = docker.run(container_image, ["-c", command], entrypoint="bash", volumes=docker_volumes)
# There doesn't seem to be a way to get an exit code from docker.run()
return (docker_output, 0)
+14 -22
View File
@@ -17,8 +17,8 @@ import click
from dataclasses import dataclass
from pathlib import Path
import sys
from .deploy import up_operation, down_operation, ps_operation, port_operation, exec_operation, logs_operation, create_deploy_context
from .util import global_options
from app.deploy import up_operation, down_operation, ps_operation, port_operation, exec_operation, logs_operation, create_deploy_context
from app.util import global_options
@dataclass
@@ -54,42 +54,46 @@ def make_deploy_context(ctx):
@command.command()
@click.option("--stay-attached/--detatch-terminal", default=False, help="detatch or not to see container stdout")
@click.argument('extra_args', nargs=-1) # help: command: up <service1> <service2>
@click.pass_context
def up(ctx, extra_args):
def up(ctx, stay_attached, extra_args):
ctx.obj = make_deploy_context(ctx)
services_list = list(extra_args) or None
up_operation(ctx, services_list)
up_operation(ctx, services_list, stay_attached)
# start is the preferred alias for up
@command.command()
@click.option("--stay-attached/--detatch-terminal", default=False, help="detatch or not to see container stdout")
@click.argument('extra_args', nargs=-1) # help: command: up <service1> <service2>
@click.pass_context
def start(ctx, extra_args):
def start(ctx, stay_attached, extra_args):
ctx.obj = make_deploy_context(ctx)
services_list = list(extra_args) or None
up_operation(ctx, services_list)
up_operation(ctx, services_list, stay_attached)
@command.command()
@click.option("--delete-volumes/--preserve-volumes", default=False, help="delete data volumes")
@click.argument('extra_args', nargs=-1) # help: command: down <service1> <service2>
@click.pass_context
def down(ctx, extra_args):
def down(ctx, delete_volumes, extra_args):
# Get the stack config file name
# TODO: add cluster name and env file here
ctx.obj = make_deploy_context(ctx)
down_operation(ctx, extra_args, None)
down_operation(ctx, delete_volumes, extra_args)
# stop is the preferred alias for down
@command.command()
@click.option("--delete-volumes/--preserve-volumes", default=False, help="delete data volumes")
@click.argument('extra_args', nargs=-1) # help: command: down <service1> <service2>
@click.pass_context
def stop(ctx, extra_args):
def stop(ctx, delete_volumes, extra_args):
# TODO: add cluster name and env file here
ctx.obj = make_deploy_context(ctx)
down_operation(ctx, extra_args, None)
down_operation(ctx, delete_volumes, extra_args)
@command.command()
@@ -126,15 +130,3 @@ def logs(ctx, extra_args):
@click.pass_context
def status(ctx):
print(f"Context: {ctx.parent.obj}")
#from importlib import resources, util
# TODO: figure out how to do this dynamically
#stack = "mainnet-laconic"
#module_name = "commands"
#spec = util.spec_from_file_location(module_name, "./app/data/stacks/" + stack + "/deploy/commands.py")
#imported_stack = util.module_from_spec(spec)
#spec.loader.exec_module(imported_stack)
#command.add_command(imported_stack.init)
#command.add_command(imported_stack.create)
+92 -26
View File
@@ -14,42 +14,28 @@
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
import click
from dataclasses import dataclass
from importlib import util
import os
from pathlib import Path
from shutil import copyfile, copytree
import sys
import ruamel.yaml
from .util import get_stack_file_path, get_parsed_deployment_spec, get_parsed_stack_config, global_options
def _get_yaml():
# See: https://stackoverflow.com/a/45701840/1701505
yaml = ruamel.yaml.YAML()
yaml.preserve_quotes = True
yaml.indent(sequence=3, offset=1)
return yaml
from app.util import get_stack_file_path, get_parsed_deployment_spec, get_parsed_stack_config, global_options, get_yaml, get_compose_file_dir
from app.deploy_types import DeploymentContext, DeployCommandContext
def _make_default_deployment_dir():
return "deployment-001"
def _get_compose_file_dir():
# TODO: refactor to use common code with deploy command
# See: https://stackoverflow.com/questions/25389095/python-get-path-of-root-project-structure
data_dir = Path(__file__).absolute().parent.joinpath("data")
source_compose_dir = data_dir.joinpath("compose")
return source_compose_dir
def _get_named_volumes(stack):
# Parse the compose files looking for named volumes
named_volumes = []
parsed_stack = get_parsed_stack_config(stack)
pods = parsed_stack["pods"]
yaml = _get_yaml()
yaml = get_yaml()
for pod in pods:
pod_file_path = os.path.join(_get_compose_file_dir(), f"docker-compose-{pod}.yml")
pod_file_path = os.path.join(get_compose_file_dir(), f"docker-compose-{pod}.yml")
parsed_pod_file = yaml.load(open(pod_file_path, "r"))
if "volumes" in parsed_pod_file:
volumes = parsed_pod_file["volumes"]
@@ -96,21 +82,77 @@ def _fixup_pod_file(pod, spec, compose_dir):
pod["volumes"][volume] = new_volume_spec
def call_stack_deploy_init(deploy_command_context):
# Link with the python file in the stack
# Call a function in it
# If no function found, return None
python_file_path = get_stack_file_path(deploy_command_context.stack).parent.joinpath("deploy", "commands.py")
spec = util.spec_from_file_location("commands", python_file_path)
imported_stack = util.module_from_spec(spec)
spec.loader.exec_module(imported_stack)
return imported_stack.init(deploy_command_context)
# TODO: fold this with function above
def call_stack_deploy_setup(deploy_command_context, extra_args):
# Link with the python file in the stack
# Call a function in it
# If no function found, return None
python_file_path = get_stack_file_path(deploy_command_context.stack).parent.joinpath("deploy", "commands.py")
spec = util.spec_from_file_location("commands", python_file_path)
imported_stack = util.module_from_spec(spec)
spec.loader.exec_module(imported_stack)
return imported_stack.setup(deploy_command_context, extra_args)
# TODO: fold this with function above
def call_stack_deploy_create(deployment_context):
# Link with the python file in the stack
# Call a function in it
# If no function found, return None
python_file_path = get_stack_file_path(deployment_context.stack).parent.joinpath("deploy", "commands.py")
spec = util.spec_from_file_location("commands", python_file_path)
imported_stack = util.module_from_spec(spec)
spec.loader.exec_module(imported_stack)
return imported_stack.create(deployment_context)
# Inspect the pod yaml to find config files referenced in subdirectories
# other than the one associated with the pod
def _find_extra_config_dirs(parsed_pod_file, pod):
config_dirs = set()
services = parsed_pod_file["services"]
for service in services:
service_info = services[service]
if "volumes" in service_info:
for volume in service_info["volumes"]:
if ":" in volume:
host_path = volume.split(":")[0]
if host_path.startswith("../config"):
config_dir = host_path.split("/")[2]
if config_dir != pod:
config_dirs.add(config_dir)
return config_dirs
@click.command()
@click.option("--output", required=True, help="Write yaml spec file here")
@click.pass_context
def init(ctx, output):
yaml = _get_yaml()
yaml = get_yaml()
stack = global_options(ctx).stack
verbose = global_options(ctx).verbose
default_spec_file_content = call_stack_deploy_init(ctx.obj)
spec_file_content = {"stack": stack}
if default_spec_file_content:
spec_file_content.update(default_spec_file_content)
if verbose:
print(f"Creating spec file for stack: {stack}")
named_volumes = _get_named_volumes(stack)
if named_volumes:
volume_descriptors = {}
for named_volume in named_volumes:
volume_descriptors[named_volume] = f"../data/{named_volume}"
volume_descriptors[named_volume] = f"./data/{named_volume}"
spec_file_content["volumes"] = volume_descriptors
with open(output, "w") as output_file:
yaml.dump(spec_file_content, output_file)
@@ -142,14 +184,38 @@ def create(ctx, spec_file, deployment_dir):
destination_compose_dir = os.path.join(deployment_dir, "compose")
os.mkdir(destination_compose_dir)
data_dir = Path(__file__).absolute().parent.joinpath("data")
yaml = _get_yaml()
yaml = get_yaml()
for pod in pods:
pod_file_path = os.path.join(_get_compose_file_dir(), f"docker-compose-{pod}.yml")
parsed_pod_file = yaml.load(open(pod_file_path, "r"))
extra_config_dirs = _find_extra_config_dirs(parsed_pod_file, pod)
if global_options(ctx).debug:
print(f"extra config dirs: {extra_config_dirs}")
_fixup_pod_file(parsed_pod_file, parsed_spec, destination_compose_dir)
with open(os.path.join(destination_compose_dir, os.path.basename(pod_file_path)), "w") as output_file:
yaml.dump(parsed_pod_file, output_file)
# Copy the config files for the pod, if any
source_config_dir = data_dir.joinpath("config", pod)
if os.path.exists(source_config_dir):
copytree(source_config_dir, os.path.join(deployment_dir, "config", pod))
config_dirs = {pod}
config_dirs = config_dirs.union(extra_config_dirs)
for config_dir in config_dirs:
source_config_dir = data_dir.joinpath("config", config_dir)
if os.path.exists(source_config_dir):
destination_config_dir = os.path.join(deployment_dir, "config", config_dir)
# If the same config dir appears in multiple pods, it may already have been copied
if not os.path.exists(destination_config_dir):
copytree(source_config_dir, destination_config_dir)
# Delegate to the stack's Python code
deployment_context = DeploymentContext(Path(deployment_dir), ctx.obj)
call_stack_deploy_create(deployment_context)
@click.command()
@click.option("--node-moniker", help="Help goes here")
@click.option("--key-name", help="Help goes here")
@click.option("--initialize-network", is_flag=True, default=False, help="Help goes here")
@click.option("--join-network", is_flag=True, default=False, help="Help goes here")
@click.option("--create-network", is_flag=True, default=False, help="Help goes here")
@click.argument('extra_args', nargs=-1)
@click.pass_context
def setup(ctx, node_moniker, key_name, initialize_network, join_network, create_network, extra_args):
call_stack_deploy_setup(ctx.obj, extra_args)
+26 -26
View File
@@ -25,7 +25,7 @@ import click
import importlib.resources
from pathlib import Path
import yaml
from .util import _log, include_exclude_check
from app.util import include_exclude_check
class GitProgress(git.RemoteProgress):
@@ -88,7 +88,7 @@ def _get_repo_current_branch_or_tag(full_filesystem_repo_path):
# TODO: fix the messy arg list here
def process_repo(verbose, quiet, dry_run, pull, check_only, git_ssh, dev_root_path, branches_array, fully_qualified_repo):
if verbose:
_log(f"Processing repo: {fully_qualified_repo}")
print(f"Processing repo: {fully_qualified_repo}")
repo_host, repo_path, repo_branch = host_and_path_for_repo(fully_qualified_repo)
git_ssh_prefix = f"git@{repo_host}:"
git_http_prefix = f"https://{repo_host}/"
@@ -100,40 +100,40 @@ def process_repo(verbose, quiet, dry_run, pull, check_only, git_ssh, dev_root_pa
if not quiet:
present_text = f"already exists active {'branch' if is_branch else 'tag'}: {current_repo_branch_or_tag}" if is_present \
else 'Needs to be fetched'
_log(f"Checking: {full_filesystem_repo_path}: {present_text}")
print(f"Checking: {full_filesystem_repo_path}: {present_text}")
# Quick check that it's actually a repo
if is_present:
if not is_git_repo(full_filesystem_repo_path):
_log(f"Error: {full_filesystem_repo_path} does not contain a valid git repository")
print(f"Error: {full_filesystem_repo_path} does not contain a valid git repository")
sys.exit(1)
else:
if pull:
if verbose:
_log(f"Running git pull for {full_filesystem_repo_path}")
print(f"Running git pull for {full_filesystem_repo_path}")
if not check_only:
if is_branch:
git_repo = git.Repo(full_filesystem_repo_path)
origin = git_repo.remotes.origin
origin.pull(progress=None if quiet else GitProgress())
else:
_log(f"skipping pull because this repo checked out a tag")
print(f"skipping pull because this repo checked out a tag")
else:
_log("(git pull skipped)")
print("(git pull skipped)")
if not is_present:
# Clone
if verbose:
_log(f'Running git clone for {full_github_repo_path} into {full_filesystem_repo_path}')
print(f'Running git clone for {full_github_repo_path} into {full_filesystem_repo_path}')
if not dry_run:
git.Repo.clone_from(full_github_repo_path,
full_filesystem_repo_path,
progress=None if quiet else GitProgress())
else:
_log("(git clone skipped)")
print("(git clone skipped)")
# Checkout the requested branch, if one was specified
branch_to_checkout = None
if branches_array:
# Find the current repo in the branches list
_log("Checking")
print("Checking")
for repo_branch in branches_array:
repo_branch_tuple = repo_branch.split(" ")
if repo_branch_tuple[0] == branch_strip(fully_qualified_repo):
@@ -145,13 +145,13 @@ def process_repo(verbose, quiet, dry_run, pull, check_only, git_ssh, dev_root_pa
if branch_to_checkout:
if current_repo_branch_or_tag is None or (current_repo_branch_or_tag and (current_repo_branch_or_tag != branch_to_checkout)):
if not quiet:
_log(f"switching to branch {branch_to_checkout} in repo {repo_path}")
print(f"switching to branch {branch_to_checkout} in repo {repo_path}")
git_repo = git.Repo(full_filesystem_repo_path)
# git checkout works for both branches and tags
git_repo.git.checkout(branch_to_checkout)
else:
if verbose:
_log(f"repo {repo_path} is already on branch/tag {branch_to_checkout}")
print(f"repo {repo_path} is already on branch/tag {branch_to_checkout}")
def parse_branches(branches_string):
@@ -161,7 +161,7 @@ def parse_branches(branches_string):
for branch_directive in branches_directives:
split_directive = branch_directive.split("@")
if len(split_directive) != 2:
_log(f"Error: branch specified is not valid: {branch_directive}")
print(f"Error: branch specified is not valid: {branch_directive}")
sys.exit(1)
result_array.append(f"{split_directive[0]} {split_directive[1]}")
return result_array
@@ -191,43 +191,43 @@ def command(ctx, include, exclude, git_ssh, check_only, pull, branches, branches
# TODO: branches file needs to be re-worked in the context of stacks
if branches_file:
if branches:
_log("Error: can't specify both --branches and --branches-file")
print("Error: can't specify both --branches and --branches-file")
sys.exit(1)
else:
if verbose:
_log(f"loading branches from: {branches_file}")
print(f"loading branches from: {branches_file}")
with open(branches_file) as branches_file_open:
branches_array = branches_file_open.read().splitlines()
_log(f"branches: {branches}")
print(f"branches: {branches}")
if branches:
if branches_file:
_log("Error: can't specify both --branches and --branches-file")
print("Error: can't specify both --branches and --branches-file")
sys.exit(1)
else:
branches_array = parse_branches(branches)
if branches_array and verbose:
_log(f"Branches are: {branches_array}")
print(f"Branches are: {branches_array}")
local_stack = ctx.obj.local_stack
if local_stack:
dev_root_path = os.getcwd()[0:os.getcwd().rindex("stack-orchestrator")]
_log(f"Local stack dev_root_path (CERC_REPO_BASE_DIR) overridden to: {dev_root_path}")
print(f"Local stack dev_root_path (CERC_REPO_BASE_DIR) overridden to: {dev_root_path}")
else:
dev_root_path = os.path.expanduser(config("CERC_REPO_BASE_DIR", default="~/cerc"))
if not quiet:
_log(f"Dev Root is: {dev_root_path}")
print(f"Dev Root is: {dev_root_path}")
if not os.path.isdir(dev_root_path):
if not quiet:
_log('Dev root directory doesn\'t exist, creating')
print('Dev root directory doesn\'t exist, creating')
os.makedirs(dev_root_path)
# See: https://stackoverflow.com/a/20885799/1701505
from . import data
from app import data
with importlib.resources.open_text(data, "repository-list.txt") as repository_list_file:
all_repos = repository_list_file.read().splitlines()
@@ -244,9 +244,9 @@ def command(ctx, include, exclude, git_ssh, check_only, pull, branches, branches
repos_in_scope = all_repos
if verbose:
_log(f"Repos: {repos_in_scope}")
print(f"Repos: {repos_in_scope}")
if stack:
_log(f"Stack: {stack}")
print(f"Stack: {stack}")
repos = []
for repo in repos_in_scope:
@@ -254,11 +254,11 @@ def command(ctx, include, exclude, git_ssh, check_only, pull, branches, branches
repos.append(repo)
else:
if verbose:
_log(f"Excluding: {repo}")
print(f"Excluding: {repo}")
for repo in repos:
try:
process_repo(verbose, quiet, dry_run, pull, check_only, git_ssh, dev_root_path, branches_array, repo)
except git.exc.GitCommandError as error:
_log(f"\n******* git command returned error exit status:\n{error}")
print(f"\n******* git command returned error exit status:\n{error}")
sys.exit(1)
+22
View File
@@ -0,0 +1,22 @@
# Copyright © 2023 Cerc
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
from enum import Enum
class State(Enum):
CREATED = 1
CONFIGURED = 2
STARTED = 3
STOPPED = 4
+19 -7
View File
@@ -15,14 +15,10 @@
import os.path
import sys
import yaml
import ruamel.yaml
from pathlib import Path
def _log(*args):
print(*args, file=sys.stderr)
def include_exclude_check(s, include, exclude):
if include is None and exclude is None:
return True
@@ -46,7 +42,7 @@ def get_parsed_stack_config(stack):
stack_file_path = stack if isinstance(stack, os.PathLike) else get_stack_file_path(stack)
try:
with stack_file_path:
stack_config = yaml.safe_load(open(stack_file_path, "r"))
stack_config = get_yaml().load(open(stack_file_path, "r"))
return stack_config
except FileNotFoundError as error:
# We try here to generate a useful diagnostic error
@@ -60,11 +56,19 @@ def get_parsed_stack_config(stack):
sys.exit(1)
def get_compose_file_dir():
# TODO: refactor to use common code with deploy command
# See: https://stackoverflow.com/questions/25389095/python-get-path-of-root-project-structure
data_dir = Path(__file__).absolute().parent.joinpath("data")
source_compose_dir = data_dir.joinpath("compose")
return source_compose_dir
def get_parsed_deployment_spec(spec_file):
spec_file_path = Path(spec_file)
try:
with spec_file_path:
deploy_spec = yaml.safe_load(open(spec_file_path, "r"))
deploy_spec = get_yaml().load(open(spec_file_path, "r"))
return deploy_spec
except FileNotFoundError as error:
# We try here to generate a useful diagnostic error
@@ -73,6 +77,14 @@ def get_parsed_deployment_spec(spec_file):
sys.exit(1)
def get_yaml():
# See: https://stackoverflow.com/a/45701840/1701505
yaml = ruamel.yaml.YAML()
yaml.preserve_quotes = True
yaml.indent(sequence=3, offset=1)
return yaml
# TODO: this is fragile wrt to the subcommand depth
# See also: https://github.com/pallets/click/issues/108
def global_options(ctx):
+1 -1
View File
@@ -22,7 +22,7 @@ def command(ctx):
'''print tool version'''
# See: https://stackoverflow.com/a/20885799/1701505
from . import data
from app import data
with importlib.resources.open_text(data, "build_tag.txt") as version_file:
# TODO: code better version that skips comment lines
version_string = version_file.read().splitlines()[1]
+20
View File
@@ -23,6 +23,26 @@ mkdir -p $CERC_REPO_BASE_DIR
# with and without volume removal
$TEST_TARGET_SO --stack test setup-repositories
$TEST_TARGET_SO --stack test build-containers
# Test deploy command execution
$TEST_TARGET_SO --stack test deploy setup $CERC_REPO_BASE_DIR
# Check that we now have the expected output directory
if [ ! -d "$CERC_REPO_BASE_DIR/container-output-dir" ]; then
echo "deploy setup test: output directory not present"
echo "deploy setup test: FAILED"
exit 1
fi
if [ ! -f "$CERC_REPO_BASE_DIR/container-output-dir/output-file" ]; then
echo "deploy setup test: output file not present"
echo "deploy setup test: FAILED"
exit 1
fi
output_file_content=$(<$CERC_REPO_BASE_DIR/container-output-dir/output-file)
if [ ! "$output_file_content" == "output-data" ]; then
echo "deploy setup test: output file contents not correct"
echo "deploy setup test: FAILED"
exit 1
fi
# Check that we now have the expected output file
$TEST_TARGET_SO --stack test deploy up
# Test deploy port command
deploy_port_output=$( $TEST_TARGET_SO --stack test deploy port test 80 )
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -e
if [ -n "$CERC_SCRIPT_DEBUG" ]; then
set -x
fi
echo "Running stack-orchestrator Ethereum mainnet test"
# Bit of a hack, test the most recent package
TEST_TARGET_SO=$( ls -t1 ./package/laconic-so* | head -1 )
# Set a new unique repo dir
export CERC_REPO_BASE_DIR=$(mktemp -d stack-orchestrator-mainnet-eth-test.XXXXXXXXXX)
DEPLOYMENT_DIR=mainnet-eth-deployment-test
echo "Testing this package: $TEST_TARGET_SO"
echo "Test version command"
reported_version_string=$( $TEST_TARGET_SO version )
echo "Version reported is: ${reported_version_string}"
echo "Cloning repositories into: $CERC_REPO_BASE_DIR"
$TEST_TARGET_SO --stack mainnet-eth setup-repositories
$TEST_TARGET_SO --stack mainnet-eth build-containers
$TEST_TARGET_SO --stack mainnet-eth deploy init --output mainnet-eth-spec.yml
$TEST_TARGET_SO deploy create --spec-file mainnet-eth-spec.yml --deployment-dir $DEPLOYMENT_DIR
# Start the stack
$TEST_TARGET_SO deployment --dir $DEPLOYMENT_DIR start
# Verify that the stack is up and running
$TEST_TARGET_SO deployment --dir $DEPLOYMENT_DIR ps
#TODO: add a check that the container logs show good startup
$TEST_TARGET_SO deployment --dir $DEPLOYMENT_DIR stop --delete-volumes
echo "Removing deployment directory"
rm -rf $DEPLOYMENT_DIR
echo "Removing cloned repositories"
rm -rf $CERC_REPO_BASE_DIR
exit $test_result