diff --git a/docs/quickrun.md b/docs/quickrun.md index 149f4d3df..1e2613e15 100644 --- a/docs/quickrun.md +++ b/docs/quickrun.md @@ -1,15 +1,12 @@ # Quick run -Warnet runs a server which can be used to manage multiple networks. On docker -this runs locally, but on Kubernetes this runs as a `statefulSet` in the -cluster. +## Installation -If the `$XDG_STATE_HOME` environment variable is set, the server will log to -a file `$XDG_STATE_HOME/warnet/warnet.log`, otherwise it will use `$HOME/.warnet/warnet.log`. +Either install warnet via pip, or clone the source and install: -## Quick start via pip +### via pip -You can install warnet via `pip` into your virtual environment with +You can install warnet via `pip` into a virtual environment with ```bash python3 -m venv .venv @@ -17,28 +14,53 @@ source .venv/bin/activate pip install warnet ``` -Following installation `warcli` commands will operate natively on the Kubernetes cluster currently configured with `kubectl`. +### via cloned source -Starting the Warnet server is as easy as: +You can install warnet from source into a virtual environment with ```bash -# (optional) if using a local minikube cluster check that we have all required programs installed -warcli setup +git clone https://github.com/bitcoin-dev-project/warnet.git +cd warnet +python3 -m venv .venv +source .venv/bin/activate +pip install -e . +``` + +## Running + +> [!TIP] +> When developing locally add the `--dev` flag to `warcli cluster deploy` to enable dev mode with hot-reloading server. + +### Using minikube + +To run a local cluster using minikube: + +```bash +warcli cluster setup-minikube -# (optional) if using a local minikube cluster, set it up -warcli cluster minikube-setup +warcli cluster deploy +``` + +### Other cluster types +If not using minikube (e.g. using Docker Desktop or a managed cluster), `warcli` commands will operate natively on the current Kubernetes context, so you can simply run: + +```bash warcli cluster deploy ``` -This also automatically configures port forwarding to the Server in the cluster. +...to deploy warnet to your cluster. + +`warcli deploy` also automatically configures port forwarding to the Server in the cluster. + +## Stopping To tear down the cluster: ```bash warcli cluster teardown +``` -# (optional) if using a local minikube cluster, remove the image -warcli cluster minikube-clean - +## Log location +If the `$XDG_STATE_HOME` environment variable is set, the server will log to a file `$XDG_STATE_HOME/warnet/warnet.log`, otherwise it will use `$HOME/.warnet/warnet.log`. diff --git a/docs/warcli.md b/docs/warcli.md index 4afc34a06..fd1722651 100644 --- a/docs/warcli.md +++ b/docs/warcli.md @@ -26,6 +26,10 @@ options: |----------|--------|------------|-----------| | commands | String | | | +### `warcli setup` +Check Warnet requirements are installed + + ## Bitcoin ### `warcli bitcoin debug-log` @@ -69,16 +73,20 @@ options: ## Cluster -### `warcli cluster deploy` -Setup Warnet using the current kubectl-configured cluster +### `warcli cluster connect-logging` +Connect kubectl to cluster logging -### `warcli cluster minikube-clean` -Reinit minikube images +### `warcli cluster deploy` +Deploy Warnet using the current kubectl-configured cluster +options: +| name | type | required | default | +|--------|--------|------------|-----------| +| dev | Bool | | False | -### `warcli cluster minikube-setup` -Setup minikube for use with Warnet +### `warcli cluster deploy-logging` +Deploy logging configurations to the cluster using helm ### `warcli cluster port-start` @@ -89,6 +97,14 @@ Port forward (runs as a detached process) Stop the port forwarding process +### `warcli cluster setup-minikube` +Configure a local minikube cluster + +options: +| name | type | required | default | +|--------|--------|------------|-----------| +| clean | Bool | | False | + ### `warcli cluster teardown` Stop the warnet server and tear down the cluster diff --git a/justfile b/justfile index d7a49cb80..529b0a3fc 100644 --- a/justfile +++ b/justfile @@ -1,98 +1,30 @@ set shell := ["bash", "-uc"] +default_cpus := "4" +default_memory := "4000" [private] default: just --list -cluster: - kubectl apply -f resources/manifests/namespace.yaml - kubectl apply -f resources/manifests/rbac-config.yaml - kubectl apply -f resources/manifests/warnet-rpc-service.yaml - kubectl apply -f resources/manifests/warnet-rpc-statefulset.yaml +# Configure a minikube k8s cluster +minikube cpus=default_cpus memory=default_memory: + WAR_RPC="./resources/images/rpc" WAR_CPU={{ cpus }} WAR_MEM={{ memory }} ./resources/scripts/setup_minikube.sh -# Setup and start the RPC in dev mode with minikube -start: - #!/usr/bin/env bash - set -euxo pipefail +# Deploy Warnet in dev mode +deploy: + WAR_MANIFESTS="./resources/manifests" WAR_DEV=1 ./resources/scripts/deploy.sh - # Function to check if minikube is running - check_minikube() { - minikube status | grep -q "Running" && echo "Minikube is already running" || minikube start --mount --mount-string="$PWD:/mnt/src" - } - - # Function to check if warnet-rpc container is already running - check_warnet_rpc() { - if kubectl get pods --all-namespaces | grep -q "bitcoindevproject/warnet-rpc"; then - echo "warnet-rpc already running in minikube" - exit 1 - fi - } - - # Check minikube status - check_minikube - - # Build image in local registry and load into minikube - docker build -t warnet/dev -f resources/images/rpc/Dockerfile_dev resources/images/rpc --load - minikube image load warnet/dev - - # Setup k8s - kubectl apply -f resources/manifests/namespace.yaml - kubectl apply -f resources/manifests/rbac-config.yaml - kubectl apply -f resources/manifests/warnet-rpc-service.yaml - kubectl apply -f resources/manifests/warnet-rpc-statefulset-dev.yaml - kubectl config set-context --current --namespace=warnet - - # Check for warnet-rpc container - check_warnet_rpc - - until kubectl get pod rpc-0 --namespace=warnet; do - echo "Waiting for server to find pod rpc-0..." - sleep 4 - done - - echo "⏲️ This could take a minute or so." - kubectl wait --for=condition=Ready --timeout=2m pod rpc-0 - - echo Done... - -# Stop the RPC in dev mode with minikube -stop: - #!/usr/bin/env bash - set -euxo pipefail - - kubectl delete namespace warnet - kubectl delete namespace warnet-logging --ignore-not-found - kubectl config set-context --current --namespace=default - - minikube image rm warnet/dev - -# Setup and start the RPC in dev mode with Docker Desktop -startd: - docker build -t warnet/dev -f resources/images/rpc/Dockerfile_dev resources/images/rpc --load - kubectl apply -f resources/manifests/namespace.yaml - kubectl apply -f resources/manifests/rbac-config.yaml - kubectl apply -f resources/manifests/warnet-rpc-service.yaml - sed 's?/mnt/src?'`PWD`'?g' resources/manifests/warnet-rpc-statefulset-dev.yaml | kubectl apply -f - - kubectl config set-context --current --namespace=warnet - - echo waiting for rpc to come online - kubectl wait --for=condition=Ready --timeout=2m pod rpc-0 - - echo Done... - -# Stop the RPC in dev mode with Docker Desktop -stopd: - # Delete all resources - kubectl delete namespace warnet - kubectl delete namespace warnet-logging --ignore-not-found - kubectl config set-context --current --namespace=default - - echo Done... +# Stop and teardown warnet +teardown: + ./resources/scripts/stop.sh # port forward p: kubectl port-forward svc/rpc 9276:9276 +# Quick start for minikube local dev +qs: minikube deploy p + registry := 'bitcoindevproject/bitcoin' repo := 'bitcoin/bitcoin' arches := 'amd64,arm64' @@ -104,10 +36,10 @@ build branch tag registry=registry repo=repo build-args=build-args action=load: warcli image build --registry={{registry}} --repo={{repo}} --branch={{branch}} --arches="{{arches}}" --tag={{tag}} --build-args="{{build-args}}" --action={{action}} installlogging: - resources/scripts/install_logging.sh + ./resources/scripts/install_logging.sh connectlogging: - resources/scripts/connect_logging.sh + ./resources/scripts/connect_logging.sh # Format and lint all files lint: diff --git a/resources/scripts/deploy.sh b/resources/scripts/deploy.sh new file mode 100755 index 000000000..f6d74da28 --- /dev/null +++ b/resources/scripts/deploy.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euxo pipefail + + +if [ -z "${WAR_MANIFESTS+x}" ]; then + echo "WAR_MANIFESTS is unset. Please provide a path to warnet manifests." + exit 1 +fi + +# Function to check if warnet-rpc container is already running +check_warnet_rpc() { + if kubectl get pods --all-namespaces | grep -q "bitcoindevproject/warnet-rpc"; then + echo "warnet-rpc pod found" + exit 1 + fi +} + +# Deploy base configurations +kubectl apply -f "$WAR_MANIFESTS/namespace.yaml" +kubectl apply -f "$WAR_MANIFESTS/rbac-config.yaml" +kubectl apply -f "$WAR_MANIFESTS/warnet-rpc-service.yaml" + +# Deploy rpc server +if [ -n "${WAR_DEV+x}" ]; then # Dev mode selector + # Build image in local registry + docker build -t warnet/dev -f "$WAR_RPC/Dockerfile_dev" "$WAR_RPC" --load + if [ "$(kubectl config current-context)" = "docker-desktop" ]; then + sed "s?/mnt/src?$(pwd)?g" "$WAR_MANIFESTS/warnet-rpc-statefulset-dev.yaml" | kubectl apply -f - + else # assuming minikube + minikube image load warnet/dev + kubectl apply -f "$WAR_MANIFESTS/warnet-rpc-statefulset-dev.yaml" + fi +else + kubectl apply -f "$WAR_MANIFESTS/warnet-rpc-statefulset.yaml" +fi + +kubectl config set-context --current --namespace=warnet + +# Check for warnet-rpc container +check_warnet_rpc + +until kubectl get pod rpc-0 --namespace=warnet; do + echo "Waiting for server to find pod rpc-0..." + sleep 4 +done + +echo "⏲️ This could take a minute or so." +kubectl wait --for=condition=Ready --timeout=2m pod rpc-0 + +echo Done... diff --git a/resources/scripts/quick_start.sh b/resources/scripts/quick_start.sh index 824e67cfd..7d473848c 100755 --- a/resources/scripts/quick_start.sh +++ b/resources/scripts/quick_start.sh @@ -38,7 +38,7 @@ print_partial_message() { local pre_message="$1" local formatted_part="$2" local post_message="$3" - local format="$4" + local format="${4:-}" # Default to empty string if not provided local color="${5:-$RESET}" echo -e "${color}${pre_message}${format}${formatted_part}${RESET}${color}${post_message}${RESET}" @@ -103,7 +103,6 @@ if [ -n "$just_path" ]; then else print_partial_message " 💥 Could not find " "just" ". Please follow this link to install it..." "$BOLD" print_message "" " https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries" "$BOLD" - exit 127 fi python_path=$(command -v python3 || true) @@ -115,11 +114,10 @@ else exit 127 fi -venv_status=$(python3 -m venv --help || true) -if [ -n "$venv_status" ]; then - print_partial_message " ⭐️ Found " "venv" ": a python3 module" "$BOLD" +if [ -n "$VIRTUAL_ENV" ]; then + print_partial_message " ⭐️ Running in virtual environment: " "$VIRTUAL_ENV" "$BOLD" else - print_partial_message " 💥 Could not find " "venv" ". Please install it using your package manager." "$BOLD" + print_partial_message " 💥 Not running in a virtual environment. " "Please activate a venv before proceeding." "$BOLD" exit 127 fi @@ -136,44 +134,3 @@ else print_partial_message " 💥 Could not find " "BPF" ". Please figure out how to enable Berkeley Packet Filters in your kernel." "$BOLD" exit 1 fi - -print_message "" "" "" -print_message "" " Let's try to spin up a python virtual environment..." "" -print_message "" "" "" - -if [ -d ".venv" ]; then - print_message "" " It looks like a virtual environment already exists!" "" -else - print_message "" " Creating a new virtual environment..." "" - python3 -m venv .venv -fi - -source .venv/bin/activate - -print_message "" "" "" -print_partial_message " ⭐️ " "venv" ": The python virtual environment looks good" "$BOLD" -print_message "" "" "" -print_message "" " Let's install warnet into that virtual environment..." "" -print_message "" "" "" - -pip install --upgrade pip -pip install -e . - -print_message "" "" "" -print_partial_message " ⭐️ " "warnet" ": We installed Warnet in the virtual environment" "$BOLD" -print_message "" "" "" -print_message "" " Now, let's get the Warnet started..." "" -print_message "" "" "" - -just start -just p & -sleep 1 -warcli network start -sleep 1 -while warcli network connected | grep -q "False"; do - sleep 2 -done -print_message "" "🥳" "" -print_message "" "Run the following command to enter into the python virtual environment..." "" -print_message "" " source .venv/bin/activate" "$BOLD" -print_partial_message " After that, you can run " "warcli help" " to start running Warnet commands." "$BOLD" diff --git a/resources/scripts/setup_minikube.sh b/resources/scripts/setup_minikube.sh new file mode 100755 index 000000000..6b23962bc --- /dev/null +++ b/resources/scripts/setup_minikube.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail +set +x +set +v + +if [ -z "${WAR_RPC+x}" ]; then + echo "WAR_RPC is unset. Please provide a path to warnet RPC images." + exit 1 +fi + +ERROR_CODE=0 + +# Colors and styles +RESET='\033[0m' +BOLD='\033[1m' + +# Use colors if we can and have the color space +if command -v tput &> /dev/null; then + ncolors=$(tput colors) + if [ -n "$ncolors" ] && [ "$ncolors" -ge 8 ]; then + RESET=$(tput sgr0) + BOLD=$(tput bold) + fi +fi + +print_message() { + local color="$1" + local message="$2" + local format="${3:-}" + echo -e "${format}${color}${message}${RESET}" +} + +print_partial_message() { + local pre_message="$1" + local formatted_part="$2" + local post_message="$3" + local format="${4:-}" # Default to empty string if not provided + local color="${5:-$RESET}" + + echo -e "${color}${pre_message}${format}${formatted_part}${RESET}${color}${post_message}${RESET}" +} + +docker_path=$(command -v docker || true) +if [ -n "$docker_path" ]; then + print_partial_message " ⭐️ Found " "docker" ": $docker_path" "$BOLD" +else + print_partial_message " 💥 Could not find " "docker" ". Please follow this link to install Docker Engine..." "$BOLD" + print_message "" " https://docs.docker.com/engine/install/" "$BOLD" + ERROR_CODE=127 +fi + +current_user=$(whoami) +current_context=$(docker context show) +if id -nG "$current_user" | grep -qw "docker"; then + print_partial_message " ⭐️ Found " "$current_user" " in the docker group" "$BOLD" +elif [ "$current_context" == "rootless" ]; then + print_message " " "⭐️ Running Docker as rootless" "$BOLD" +elif [[ "$(uname)" == "Darwin" ]]; then + print_message " " "⭐️ Running Docker on Darwin" "$BOLD" +else + print_partial_message " 💥 Could not find " "$current_user" " in the docker group. Please add it like this..." "$BOLD" + print_message "" " sudo usermod -aG docker $current_user && newgrp docker" "$BOLD" + ERROR_CODE=1 +fi + +minikube_path=$(command -v minikube || true) +if [ -n "$minikube_path" ]; then + print_partial_message " ⭐️ Found " "minikube" ": $minikube_path " "$BOLD" +else + print_partial_message " 💥 Could not find " "minikube" ". Please follow this link to install it..." "$BOLD" + print_message "" " https://minikube.sigs.k8s.io/docs/start/" "$BOLD" + ERROR_CODE=127 +fi + +if [ $ERROR_CODE -ne 0 ]; then + print_message "" "There were errors in the setup process. Please fix them and try again." "$BOLD" + exit $ERROR_CODE +fi + +# Check minikube status +minikube delete + +# Prepare minikube start command +MINIKUBE_CMD="minikube start --driver=docker --container-runtime=containerd --mount --mount-string=\"$PWD:/mnt/src\"" + +# Check for WAR_CPU and add to command if set +if [ -n "${WAR_CPU:-}" ]; then + MINIKUBE_CMD="$MINIKUBE_CMD --cpus=$WAR_CPU" +fi + +# Check for WAR_MEM and add to command if set +if [ -n "${WAR_MEM:-}" ]; then + MINIKUBE_CMD="$MINIKUBE_CMD --memory=${WAR_MEM}m" +fi + +# Start minikube with the constructed command +eval "$MINIKUBE_CMD" + + + +echo Done... diff --git a/resources/scripts/startd.sh b/resources/scripts/startd.sh new file mode 100755 index 000000000..84343510b --- /dev/null +++ b/resources/scripts/startd.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euxo pipefail + +if [ $# -eq 0 ]; then + echo "Please provide a path as an argument." + exit 1 +fi +RPC_PATH="$1" + +docker build -t warnet/dev -f "$RPC_PATH/Dockerfile_rpc_dev src/warnet/templates/rpc" --load +kubectl apply -f "$RPC_PATH/namespace.yaml" +kubectl apply -f "$RPC_PATH/rbac-config.yaml" +kubectl apply -f "$RPC_PATH/warnet-rpc-service.yaml" +sed "s?/mnt/src?$(pwd)?g" "$RPC_PATH/warnet-rpc-statefulset-dev.yaml" | kubectl apply -f - +kubectl config set-context --current --namespace=warnet + +echo waiting for rpc to come online +kubectl wait --for=condition=Ready --timeout=2m pod rpc-0 + +echo Done... diff --git a/resources/scripts/stop.sh b/resources/scripts/stop.sh new file mode 100755 index 000000000..b56459d3d --- /dev/null +++ b/resources/scripts/stop.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euxo pipefail + +# Delete namespaces +kubectl delete namespace warnet --ignore-not-found +kubectl delete namespace warnet-logging --ignore-not-found + +# Set the context to default namespace +kubectl config set-context --current --namespace=default + +# Delete minikube, if it exists +minikube delete || true diff --git a/src/warnet/cli/cluster.py b/src/warnet/cli/cluster.py index 74a9f99a5..dc1c56778 100644 --- a/src/warnet/cli/cluster.py +++ b/src/warnet/cli/cluster.py @@ -8,19 +8,32 @@ MANIFEST_PATH = files("manifests") RPC_PATH = files("images").joinpath("rpc") +SCRIPTS_PATH = files("scripts") +START_SCRIPT = SCRIPTS_PATH / "start.sh" +DEPLOY_SCRIPT = SCRIPTS_PATH / "deploy.sh" +INSTALL_LOGGING_SCRIPT = SCRIPTS_PATH / "install_logging.sh" +CONNECT_LOGGING_SCRIPT = SCRIPTS_PATH / "connect_logging.sh" + @click.group(name="cluster", chain=True) def cluster(): """Start, configure and stop a warnet k8s cluster\n \b Supports chaining, e.g: - warcli cluster minikube-setup deploy - warcli cluster teardown minikube-clean + warcli cluster deploy + warcli cluster teardown """ pass -def run_command(command, stream_output=False): +def run_command(command, stream_output=False, env=None): + # Merge the current environment with the provided env + full_env = os.environ.copy() + if env: + # Convert all env values to strings (only a safeguard) + env = {k: str(v) for k, v in env.items()} + full_env.update(env) + if stream_output: process = subprocess.Popen( ["/bin/bash", "-c", command], @@ -29,6 +42,7 @@ def run_command(command, stream_output=False): text=True, bufsize=1, universal_newlines=True, + env=full_env, ) for line in iter(process.stdout.readline, ""): @@ -53,93 +67,51 @@ def run_command(command, stream_output=False): @cluster.command() -def minikube_setup(): - """Setup minikube for use with Warnet""" - script_content = f""" - #!/usr/bin/env bash - set -euxo pipefail - - # Function to check if minikube is running - check_minikube() {{ - minikube status | grep -q "Running" && echo "Minikube is already running" || minikube start --memory=4000mb --cpus=4 --mount --mount-string="$PWD:/mnt/src" - }} - - # Check minikube status - check_minikube - - # Build image in local registry and load into minikube - docker build -t warnet/dev -f {RPC_PATH}/Dockerfile_dev {RPC_PATH} --load - minikube image load warnet/dev - """ - - run_command(script_content, stream_output=True) +@click.option("--clean", is_flag=True, help="Remove configuration files") +def setup_minikube(clean): + """Configure a local minikube cluster""" + memory = click.prompt( + "How much RAM should we assign to the minikube cluster? (MB)", + type=int, + default=4000, + ) + cpu = click.prompt( + "How many CPUs should we assign to the minikube cluster?", type=int, default=4 + ) + env = {"WAR_MEM": str(memory), "WAR_CPU": str(cpu), "WAR_RPC": RPC_PATH} + run_command(SCRIPTS_PATH / "setup_minikube.sh", stream_output=True, env=env) +# TODO: Add a --dev flag to this @cluster.command() -def deploy(): - """Setup Warnet using the current kubectl-configured cluster""" - script_content = f""" - #!/usr/bin/env bash - set -euxo pipefail - - # Function to check if warnet-rpc container is already running - check_warnet_rpc() {{ - if kubectl get pods --all-namespaces | grep -q "bitcoindevproject/warnet-rpc"; then - echo "warnet-rpc already running in minikube" - exit 1 - fi - }} - - # Setup K8s - kubectl apply -f {MANIFEST_PATH}/namespace.yaml - kubectl apply -f {MANIFEST_PATH}/rbac-config.yaml - kubectl apply -f {MANIFEST_PATH}/warnet-rpc-service.yaml - kubectl apply -f {MANIFEST_PATH}/warnet-rpc-statefulset-dev.yaml - kubectl config set-context --current --namespace=warnet - - # Check for warnet-rpc container - check_warnet_rpc - - until kubectl get pod rpc-0 --namespace=warnet; do - echo "Waiting for server to find pod rpc-0..." - sleep 4 - done - - echo "⏲️ This could take a minute or so." - kubectl wait --for=condition=Ready --timeout=2m pod rpc-0 - - echo Done... - """ - - res = run_command(script_content, stream_output=True) +@click.option("--dev", is_flag=True, help="Remove configuration files") +def deploy(dev: bool): + """Deploy Warnet using the current kubectl-configured cluster""" + env = {"WAR_MANIFESTS": str(MANIFEST_PATH), "WAR_RPC": RPC_PATH} + if dev: + env["WAR_DEV"] = 1 + res = run_command(SCRIPTS_PATH / "deploy.sh", stream_output=True, env=env) if res: _port_start_internal() @cluster.command() -def minikube_clean(): - """Reinit minikube images""" - script_content = """ - #!/usr/bin/env bash - set -euxo pipefail - minikube image rm warnet/dev - """ - run_command(script_content, stream_output=True) +def teardown(): + """Stop the warnet server and tear down the cluster""" + run_command(SCRIPTS_PATH / "stop.sh", stream_output=True) + _port_stop_internal() @cluster.command() -def teardown(): - """Stop the warnet server and tear down the cluster""" - script_content = """ - #!/usr/bin/env bash - set -euxo pipefail +def deploy_logging(): + """Deploy logging configurations to the cluster using helm""" + run_command(SCRIPTS_PATH / "install_logging.sh", stream_output=True) - kubectl delete namespace warnet - kubectl delete namespace warnet-logging - kubectl config set-context --current --namespace=default - """ - run_command(script_content, stream_output=True) - _port_stop_internal() + +@cluster.command() +def connect_logging(): + """Connect kubectl to cluster logging""" + run_command(CONNECT_LOGGING_SCRIPT, stream_output=True) def is_windows(): diff --git a/src/warnet/cli/main.py b/src/warnet/cli/main.py index 6fd941dbb..3aa7e8dce 100644 --- a/src/warnet/cli/main.py +++ b/src/warnet/cli/main.py @@ -1,3 +1,7 @@ +import os +import subprocess +from importlib.resources import files + import click from rich import print as richprint @@ -9,6 +13,8 @@ from .network import network from .scenarios import scenarios +QUICK_START_PATH = files("scripts").joinpath("quick_start.sh") + @click.group() def cli(): @@ -60,5 +66,34 @@ def help_command(ctx, commands): cli.add_command(help_command) +@cli.command() +def setup(): + """Check Warnet requirements are installed""" + try: + process = subprocess.Popen( + ["/bin/bash", str(QUICK_START_PATH)], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + # This preserves colours from grant's lovely script! + env=dict(os.environ, TERM="xterm-256color"), + ) + + for line in iter(process.stdout.readline, ""): + print(line, end="", flush=True) + + process.stdout.close() + return_code = process.wait() + + if return_code != 0: + print(f"Quick start script failed with return code {return_code}") + return False + return True + + except Exception as e: + print(f"An error occurred while running the quick start script: {e}") + return False + + if __name__ == "__main__": cli()