From f55b38634e71fec15d351925c30650f854b1aa32 Mon Sep 17 00:00:00 2001 From: Ron Kuris Date: Thu, 25 Sep 2025 18:25:40 -0700 Subject: [PATCH 1/2] fix: Larger swap for smaller memory instances This makes the swap bigger which might allow avalanchego to work correctly when using an instance with a small memory size. --- benchmark/bootstrap/aws-launch.sh | 174 ++++++++++++++++++++---------- 1 file changed, 115 insertions(+), 59 deletions(-) diff --git a/benchmark/bootstrap/aws-launch.sh b/benchmark/bootstrap/aws-launch.sh index d7ea714c9..1b3aab706 100755 --- a/benchmark/bootstrap/aws-launch.sh +++ b/benchmark/bootstrap/aws-launch.sh @@ -12,43 +12,90 @@ REGION="us-west-2" DRY_RUN=false SPOT_INSTANCE=false -# Valid instance types and their architectures -declare -A VALID_INSTANCES=( - ["i4g.large"]="arm64" - ["i4g.xlarge"]="arm64" - ["i4i.large"]="amd64" - ["i4i.xlarge"]="amd64" - ["m6id.xlarge"]="amd64" - ["c6gd.2xlarge"]="arm64" - ["x2gd.xlarge"]="arm64" - ["m5ad.2xlarge"]="arm64" - ["r6gd.2xlarge"]="arm64" - ["r6id.2xlarge"]="amd64" - ["x2gd.2xlarge"]="arm64" - ["z1d.2xlarge"]="amd64" - ["i8ge.12xlarge"]="arm64" -) - -# Maximum spot prices for each instance type (from the pricing table) -declare -A MAX_SPOT_PRICES=( - ["i4g.large"]="0.1544" - ["i4g.xlarge"]="0.3088" - ["i4i.large"]="0.1720" - ["i4i.xlarge"]="0.3440" - ["m6id.xlarge"]="0.2373" - ["c6gd.2xlarge"]="0.3072" - ["x2gd.xlarge"]="0.3340" - ["m5ad.2xlarge"]="0.4120" - ["r6gd.2xlarge"]="0.4608" - ["r6id.2xlarge"]="0.6048" - ["x2gd.2xlarge"]="0.6680" - ["z1d.2xlarge"]="0.7440" - ["i8ge.12xlarge"]="5.6952" +# Instance information: architecture:spot_price:swap_size:disk:vcpu:memory:notes +declare -A INSTANCE_INFO=( + ["i4g.large"]="arm64:0.1544:32G:468:2:16 GiB:Graviton2-powered" + ["i4i.large"]="amd64:0.1720:32G:468:2:16 GiB:Intel Xeon Scalable" + ["m6id.xlarge"]="amd64:0.2373:32G:237:4:16 GiB:Intel Xeon Scalable" + ["c6gd.2xlarge"]="arm64:0.3072:32G:474:8:16 GiB:Graviton2 compute-optimized" + ["i4g.xlarge"]="arm64:0.3088:16G:937:4:32 GiB:Graviton2-powered" + ["x2gd.xlarge"]="arm64:0.3340:0:237:4:64 GiB:Graviton2 memory-optimized" + ["i4i.xlarge"]="amd64:0.3440:16G:937:4:32 GiB:Intel Xeon Scalable" + ["m5ad.2xlarge"]="arm64:0.4120:16G:300:8:32 GiB:AMD EPYC processors" + ["r6gd.2xlarge"]="arm64:0.4608:0:474:8:64 GiB:Graviton2 memory-optimized" + ["r6id.2xlarge"]="amd64:0.6048:0:474:8:64 GiB:Intel Xeon Scalable" + ["x2gd.2xlarge"]="arm64:0.6680:0:475:8:128 GiB:Graviton2 memory-optimized" + ["z1d.2xlarge"]="amd64:0.7440:0:300:8:64 GiB:High-frequency Intel Xeon CPUs" + ["i8ge.12xlarge"]="arm64:5.6952:0:11250:48:384 GiB:Careful, very expensive" ) # Valid nblocks values VALID_NBLOCKS=("1m" "10m" "50m") +# Helper functions to extract instance information +get_instance_arch() { + local instance_type=$1 + echo "${INSTANCE_INFO[$instance_type]}" | cut -d: -f1 +} + +get_instance_spot_price() { + local instance_type=$1 + echo "${INSTANCE_INFO[$instance_type]}" | cut -d: -f2 +} + +get_instance_swap_size() { + local instance_type=$1 + echo "${INSTANCE_INFO[$instance_type]}" | cut -d: -f3 +} + +get_instance_disk() { + local instance_type=$1 + echo "${INSTANCE_INFO[$instance_type]}" | cut -d: -f4 +} + +get_instance_vcpu() { + local instance_type=$1 + echo "${INSTANCE_INFO[$instance_type]}" | cut -d: -f5 +} + +get_instance_memory() { + local instance_type=$1 + echo "${INSTANCE_INFO[$instance_type]}" | cut -d: -f6 +} + +get_instance_notes() { + local instance_type=$1 + echo "${INSTANCE_INFO[$instance_type]}" | cut -d: -f7 +} + +# Function to generate instance table +generate_instance_table() { + echo " # name Type disk vcpu memory \$/hr notes" + + # Create array of instance types with their prices for sorting + local instances_with_prices=() + local spot_price + for instance_type in "${!INSTANCE_INFO[@]}"; do + spot_price=$(get_instance_spot_price "$instance_type") + instances_with_prices+=("$spot_price:$instance_type") + done + + # Sort by price and print table + local instance_type arch disk vcpu memory notes + for entry in $(printf '%s\n' "${instances_with_prices[@]}" | sort -t: -k1 -n); do + instance_type=$(echo "$entry" | cut -d: -f2) + arch=$(get_instance_arch "$instance_type") + spot_price=$(get_instance_spot_price "$instance_type") + disk=$(get_instance_disk "$instance_type") + vcpu=$(get_instance_vcpu "$instance_type") + memory=$(get_instance_memory "$instance_type") + notes=$(get_instance_notes "$instance_type") + + printf " %-13s %-5s %-5s %-4s %-8s \$%-6s %s\n" \ + "$instance_type" "$arch" "$disk" "$vcpu" "$memory" "$spot_price" "$notes" + done +} + # Function to show usage show_usage() { echo "Usage: $0 [OPTIONS]" @@ -66,20 +113,7 @@ show_usage() { echo " --help Show this help message" echo "" echo "Valid instance types:" - echo " # name Type disk vcpu memory $/hr notes" - echo " i4g.large arm64 468 2 16 GiB \$0.1544 Graviton2-powered" - echo " i4i.large amd64 468 2 16 GiB \$0.1720 Intel Xeon Scalable" - echo " m6id.xlarge arm64 237 4 16 GiB \$0.2373 Intel Xeon Scalable" - echo " c6gd.2xlarge arm64 474 8 16 GiB \$0.3072 Graviton2 compute-optimized" - echo " i4g.xlarge arm64 937 4 32 GiB \$0.3088 Graviton2-powered" - echo " i4i.xlarge amd64 937 4 32 GiB \$0.3440 Intel Xeon Scalable" - echo " x2gd.xlarge arm64 237 4 64 GiB \$0.3340 Graviton2 memory-optimized" - echo " m5ad.2xlarge arm64 300 8 32 GiB \$0.4120 AMD EPYC processors" - echo " r6gd.2xlarge arm64 474 8 64 GiB \$0.4608 Graviton2 memory-optimized" - echo " r6id.2xlarge amd64 474 8 64 GiB \$0.6048 Intel Xeon Scalable" - echo " x2gd.2xlarge arm64 475 8 128 GiB \$0.6680 Graviton2 memory-optimized" - echo " z1d.2xlarge amd64 300 8 64 GiB \$0.7440 High-frequency Intel Xeon CPUs" - echo " i8ge.12xlarge arm64 11250 48 384 GiB \$5.5952 Careful, very expensive" + generate_instance_table echo "" echo "Valid nblocks values: ${VALID_NBLOCKS[*]}" } @@ -89,9 +123,9 @@ while [[ $# -gt 0 ]]; do case $1 in --instance-type) INSTANCE_TYPE="$2" - if [[ ! ${VALID_INSTANCES[$INSTANCE_TYPE]+_} ]]; then + if [[ ! ${INSTANCE_INFO[$INSTANCE_TYPE]+_} ]]; then echo "Error: Invalid instance type '$INSTANCE_TYPE'" - echo "Valid types: ${!VALID_INSTANCES[*]}" + echo "Valid types: ${!INSTANCE_INFO[*]}" exit 1 fi shift 2 @@ -147,7 +181,7 @@ while [[ $# -gt 0 ]]; do done # Set architecture type based on instance type -TYPE=${VALID_INSTANCES[$INSTANCE_TYPE]} +TYPE=$(get_instance_arch "$INSTANCE_TYPE") echo "Configuration:" echo " Instance Type: $INSTANCE_TYPE ($TYPE)" @@ -157,8 +191,15 @@ echo " Coreth Branch: ${CORETH_BRANCH:-default}" echo " LibEVM Branch: ${LIBEVM_BRANCH:-default}" echo " Number of Blocks: $NBLOCKS" echo " Region: $REGION" +SWAP_SIZE=$(get_instance_swap_size "$INSTANCE_TYPE") +if [ "$SWAP_SIZE" = "0" ]; then + echo " Swap: Disabled (sufficient RAM)" +else + echo " Swap: $SWAP_SIZE" +fi if [ "$SPOT_INSTANCE" = true ]; then - echo " Spot Instance: Yes (max price: \$${MAX_SPOT_PRICES[$INSTANCE_TYPE]})" + SPOT_PRICE=$(get_instance_spot_price "$INSTANCE_TYPE") + echo " Spot Instance: Yes (max price: \$$SPOT_PRICE)" else echo " Spot Instance: No" fi @@ -203,6 +244,17 @@ if [ -n "$LIBEVM_BRANCH" ]; then LIBEVM_BRANCH_ARG="--branch $LIBEVM_BRANCH" fi +# Generate swap configuration based on instance type +SWAP_SIZE=$(get_instance_swap_size "$INSTANCE_TYPE") +if [ "$SWAP_SIZE" = "0" ]; then + SWAP_CONFIG="" +else + SWAP_CONFIG="swap: + filename: /swapfile + size: $SWAP_SIZE + maxsize: $SWAP_SIZE" +fi + # set up this script to run at startup, installing a few packages, creating user accounts, # and downloading the blocks for the C-chain USERDATA_TEMPLATE=$(cat <<'END_HEREDOC' @@ -263,10 +315,7 @@ users: ssh_authorized_keys: - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE/1C8JVL0g6qqMw1p0TwJMqJqERxYTX+7PnP+gXP4km cardno:19_155_748 bernard -swap: - filename: /swapfile - size: 16G - maxsize: 16G +__SWAP_CONFIG__ # anyone can use the -D option write_files: @@ -365,15 +414,22 @@ case $NBLOCKS in *) END_BLOCK="1000000" ;; # Default fallback esac -# Substitute branch arguments and block values in the userdata template -USERDATA=$(echo "$USERDATA_TEMPLATE" | \ +# Substitute branch arguments, swap config, and block values in the userdata template +# Use a temporary variable to handle potential newlines in SWAP_CONFIG +TEMP_USERDATA=$(echo "$USERDATA_TEMPLATE" | \ sed "s|__FIREWOOD_BRANCH_ARG__|$FIREWOOD_BRANCH_ARG|g" | \ sed "s|__AVALANCHEGO_BRANCH_ARG__|$AVALANCHEGO_BRANCH_ARG|g" | \ sed "s|__CORETH_BRANCH_ARG__|$CORETH_BRANCH_ARG|g" | \ sed "s|__LIBEVM_BRANCH_ARG__|$LIBEVM_BRANCH_ARG|g" | \ sed "s|__NBLOCKS__|$NBLOCKS|g" | \ - sed "s|__END_BLOCK__|$END_BLOCK|g" | \ - base64) + sed "s|__END_BLOCK__|$END_BLOCK|g") + +# Replace swap config using a different approach to handle multiline content +if [ -n "$SWAP_CONFIG" ]; then + USERDATA=$(echo "$TEMP_USERDATA" | sed "s|__SWAP_CONFIG__|$SWAP_CONFIG|g" | base64) +else + USERDATA=$(echo "$TEMP_USERDATA" | sed "s|__SWAP_CONFIG__||g" | base64) +fi export USERDATA fi # End of DRY_RUN=false conditional @@ -399,7 +455,7 @@ fi # Build spot instance market options if requested SPOT_OPTIONS="" if [ "$SPOT_INSTANCE" = true ]; then - MAX_PRICE=${MAX_SPOT_PRICES[$INSTANCE_TYPE]} + MAX_PRICE=$(get_instance_spot_price "$INSTANCE_TYPE") SPOT_OPTIONS="--instance-market-options '{\"MarketType\":\"spot\", \"SpotOptions\": {\"MaxPrice\":\"$MAX_PRICE\"}}'" fi From 6e0ab5ef462166779ea25a5318de1dd50941d0ef Mon Sep 17 00:00:00 2001 From: Ron Kuris Date: Fri, 26 Sep 2025 06:59:51 -0700 Subject: [PATCH 2/2] WIP: rust launcher --- fwdctl/Cargo.toml | 7 + fwdctl/src/cloud-init.yaml | 145 +++++++++++++++++++ fwdctl/src/launch-types.json | 15 ++ fwdctl/src/launch.rs | 260 +++++++++++++++++++++++++++++++++++ fwdctl/src/main.rs | 4 + 5 files changed, 431 insertions(+) create mode 100644 fwdctl/src/cloud-init.yaml create mode 100644 fwdctl/src/launch-types.json create mode 100644 fwdctl/src/launch.rs diff --git a/fwdctl/Cargo.toml b/fwdctl/Cargo.toml index 0b979a48d..24757137b 100644 --- a/fwdctl/Cargo.toml +++ b/fwdctl/Cargo.toml @@ -36,6 +36,13 @@ csv = "1.3.1" indicatif = "0.18.0" askama = "0.14.0" num-format = "0.4.4" +serde = { version = "1.0.213", features = ["derive"] } +serde_json = "1.0.135" +saphyr = "0.0.6" +# AWS SDK dependencies +aws-config = "1.0" +aws-sdk-ec2 = "1.0" +tokio = { version = "1.0", features = ["full"] } [features] ethhash = ["firewood/ethhash"] diff --git a/fwdctl/src/cloud-init.yaml b/fwdctl/src/cloud-init.yaml new file mode 100644 index 000000000..700fe3a3f --- /dev/null +++ b/fwdctl/src/cloud-init.yaml @@ -0,0 +1,145 @@ +#cloud-config +package_update: true +package_upgrade: true +packages: + - git + - build-essential + - curl + - protobuf-compiler + - make + - apt-transport-https + - net-tools + - unzip +users: + - default + - name: rkuris + lock_passwd: true + groups: users, adm, sudo + shell: /usr/bin/bash + sudo: "ALL=(ALL) NOPASSWD:ALL" + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL2RVmfpoKYi0tJd2DhQEp8tB3m2PSuaYxIfnLwqt03u cardno:23_537_110 ron + - name: austin + lock_passwd: true + groups: users, adm, sudo + shell: /usr/bin/bash + sudo: "ALL=(ALL) NOPASSWD:ALL" + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICoGgX8nCin3FPc1V3YYN1M9g039wMbzZSAXZJCqzBt3 cardno:31_786_961 austin + - name: aaron + lock_passwd: true + groups: users, adm, sudo + shell: /usr/bin/bash + sudo: "ALL=(ALL) NOPASSWD:ALL" + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMj2j6ySwsFx7Y6FW2UXlkjCZfFDQKHWh0GTBjkK9ruV cardno:19_236_959 aaron + - name: brandon + lock_passwd: true + groups: users, adm, sudo + shell: /usr/bin/bash + sudo: "ALL=(ALL) NOPASSWD:ALL" + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuwpEMnsBLdfr7V9SFRTm9XWHEFX3yQQP7nmsFHetBo cardno:26_763_547 brandon + - name: amin + lock_passwd: true + groups: users, adm, sudo + shell: /usr/bin/bash + sudo: "ALL=(ALL) NOPASSWD:ALL" + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE8iR1X8/ELrzjczZvCkrTGCEoN6/dtlP01QFGuUpYxV cardno:33_317_839 amin + - name: bernard + lock_passwd: true + groups: users, adm, sudo + shell: /usr/bin/bash + sudo: "ALL=(ALL) NOPASSWD:ALL" + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE/1C8JVL0g6qqMw1p0TwJMqJqERxYTX+7PnP+gXP4km cardno:19_155_748 bernard + +!custom swap_config: {} + +# anyone can use the -D option +write_files: +- content: | + Defaults runcwd=* + path: "/etc/sudoers.d/91-cloud-init-enable-D-option" + permissions: '0440' +- content: | + export PATH="$PATH:/usr/local/go/bin" + permissions: "0644" + path: "/etc/profile.d/go_path.sh" +- content: | + export RUSTUP_HOME=/usr/local/rust + export PATH="$PATH:/usr/local/rust/bin" + permissions: "0644" + path: "/etc/profile.d/rust_path.sh" + +runcmd: + # install rust + - echo 'PATH=/usr/local/go/bin:$HOME/.cargo/bin:$PATH' >> ~ubuntu/.profile + - > + curl https://sh.rustup.rs -sSf + | RUSTUP_HOME=/usr/local/rust CARGO_HOME=/usr/local/rust + sh -s -- -y --no-modify-path + - sudo -u ubuntu --login rustup default stable + # install firewood + - git clone --depth 1 __FIREWOOD_BRANCH_ARG__ https://github.com/ava-labs/firewood.git /tmp/firewood + - bash /tmp/firewood/benchmark/setup-scripts/build-environment.sh + - bash -c 'mkdir ~ubuntu/firewood; mv /tmp/firewood/{.[!.],}* ~ubuntu/firewood/' + # fix up the directories so that anyone is group 'users' has r/w access + - chown -R ubuntu:users /mnt/nvme/ubuntu + - chmod -R g=u /mnt/nvme/ubuntu + - find /mnt/nvme/ubuntu -type d -print0 | xargs -0 chmod g+s + # helpful symbolic links from home directories + - sudo -u ubuntu ln -s /mnt/nvme/ubuntu/data /home/ubuntu/data + - sudo -u ubuntu ln -s /mnt/nvme/ubuntu/avalanchego /home/ubuntu/avalanchego + # install go and grafana + - bash /mnt/nvme/ubuntu/firewood/benchmark/setup-scripts/install-golang.sh + - bash /mnt/nvme/ubuntu/firewood/benchmark/setup-scripts/install-grafana.sh + # install task, avalanchego, coreth + - snap install task --classic + - > + sudo -u ubuntu -D /mnt/nvme/ubuntu + git clone --depth 100 __AVALANCHEGO_BRANCH_ARG__ https://github.com/ava-labs/avalanchego.git + - > + sudo -u ubuntu -D /mnt/nvme/ubuntu + git clone --depth 100 __CORETH_BRANCH_ARG__ https://github.com/ava-labs/coreth.git + - > + sudo -u ubuntu -D /mnt/nvme/ubuntu + git clone --depth 100 __LIBEVM_BRANCH_ARG__ https://github.com/ava-labs/libevm.git + # force avalanchego to use the checked-out versions of coreth, libevm, and firewood + - > + sudo -u ubuntu -D /mnt/nvme/ubuntu/avalanchego + /usr/local/go/bin/go mod edit -replace + github.com/ava-labs/firewood-go-ethhash/ffi=../firewood/ffi + - > + sudo -u ubuntu -D /mnt/nvme/ubuntu/avalanchego + /usr/local/go/bin/go mod edit -replace + github.com/ava-labs/coreth=../coreth + - > + sudo -u ubuntu -D /mnt/nvme/ubuntu/avalanchego + /usr/local/go/bin/go mod edit -replace + github.com/ava-labs/libevm=../libevm + # build firewood in maxperf mode + - > + sudo -u ubuntu -D /mnt/nvme/ubuntu/firewood/ffi --login time cargo build + --profile maxperf + --features ethhash,logger + > /mnt/nvme/ubuntu/firewood/build.log 2>&1 + # build avalanchego + - sudo -u ubuntu --login -D /mnt/nvme/ubuntu/avalanchego go mod tidy + - > + sudo -u ubuntu --login -D /mnt/nvme/ubuntu/avalanchego time scripts/build.sh + > /mnt/nvme/ubuntu/avalanchego/build.log 2>&1 & + # install s5cmd + - curl -L -o /tmp/s5cmd.deb $(curl -s https://api.github.com/repos/peak/s5cmd/releases/latest | grep "browser_download_url" | grep "linux_$(dpkg --print-architecture).deb" | cut -d '"' -f 4) && dpkg -i /tmp/s5cmd.deb + # download and extract mainnet blocks + - echo 'downloading mainnet blocks' + - sudo -u ubuntu mkdir -p /mnt/nvme/ubuntu/exec-data/blocks + - s5cmd cp s3://avalanchego-bootstrap-testing/cchain-mainnet-blocks-__NBLOCKS__-ldb/\* /mnt/nvme/ubuntu/exec-data/blocks/ >/dev/null + - chown -R ubuntu /mnt/nvme/ubuntu/exec-data + - chmod -R g=u /mnt/nvme/ubuntu/exec-data + # execute bootstrapping + - > + sudo -u ubuntu -D /mnt/nvme/ubuntu/avalanchego --login + time task reexecute-cchain-range CURRENT_STATE_DIR=/mnt/nvme/ubuntu/exec-data/current-state BLOCK_DIR=/mnt/nvme/ubuntu/exec-data/blocks START_BLOCK=1 END_BLOCK=__END_BLOCK__ CONFIG=firewood METRICS_ENABLED=false + > /var/log/bootstrap.log 2>&1 diff --git a/fwdctl/src/launch-types.json b/fwdctl/src/launch-types.json new file mode 100644 index 000000000..2c7d67e02 --- /dev/null +++ b/fwdctl/src/launch-types.json @@ -0,0 +1,15 @@ +{ + "i4g.large": {"arch": "Arm64", "spot": 0.1544, "swap": 32, "disk": 468, "vcpu": 2, "memory": 16, "notes": "Graviton2-powered"}, + "i4i.large": {"arch": "Amd64", "spot": 0.1720, "swap": 32, "disk": 468, "vcpu": 2, "memory": 16, "notes": "Intel Xeon Scalable"}, + "m6id.xlarge": {"arch": "Amd64", "spot": 0.2373, "swap": 32, "disk": 237, "vcpu": 4, "memory": 16, "notes": "Intel Xeon Scalable"}, + "c6gd.2xlarge": {"arch": "Arm64", "spot": 0.3072, "swap": 32, "disk": 474, "vcpu": 8, "memory": 16, "notes": "Graviton2 compute-optimized"}, + "i4g.xlarge": {"arch": "Arm64", "spot": 0.3088, "swap": 16, "disk": 937, "vcpu": 4, "memory": 32, "notes": "Graviton2-powered"}, + "x2gd.xlarge": {"arch": "Arm64", "spot": 0.3340, "swap": 0, "disk": 237, "vcpu": 4, "memory": 64, "notes": "Graviton2 memory-optimized"}, + "i4i.xlarge": {"arch": "Amd64", "spot": 0.3440, "swap": 16, "disk": 937, "vcpu": 4, "memory": 32, "notes": "Intel Xeon Scalable"}, + "m5ad.2xlarge": {"arch": "Arm64", "spot": 0.4120, "swap": 16, "disk": 300, "vcpu": 8, "memory": 32, "notes": "AMD EPYC processors"}, + "r6gd.2xlarge": {"arch": "Arm64", "spot": 0.4608, "swap": 0, "disk": 474, "vcpu": 8, "memory": 64, "notes": "Graviton2 memory-optimized"}, + "r6id.2xlarge": {"arch": "Amd64", "spot": 0.6048, "swap": 0, "disk": 474, "vcpu": 8, "memory": 64, "notes": "Intel Xeon Scalable"}, + "x2gd.2xlarge": {"arch": "Arm64", "spot": 0.6680, "swap": 0, "disk": 475, "vcpu": 8, "memory": 128, "notes": "Graviton2 memory-optimized"}, + "z1d.2xlarge": {"arch": "Amd64", "spot": 0.7440, "swap": 0, "disk": 300, "vcpu": 8, "memory": 64, "notes": "High-frequency Intel Xeon CPUs"}, + "i8ge.12xlarge": {"arch": "Arm64", "spot": 5.6952, "swap": 0, "disk": 11250, "vcpu": 48, "memory": 384, "notes": "Careful, very expensive"} +} diff --git a/fwdctl/src/launch.rs b/fwdctl/src/launch.rs new file mode 100644 index 000000000..3079638d6 --- /dev/null +++ b/fwdctl/src/launch.rs @@ -0,0 +1,260 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE.md for licensing terms. + +use std::{collections::HashMap, sync::OnceLock}; + +use aws_config::BehaviorVersion; +use aws_sdk_ec2::{ + Client, + // types::{Tag, TagSpecification}, +}; +use clap::Args; +use firewood::v2::api; +use log::info; +use saphyr::LoadableYamlNode as _; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct InstanceType { + // Instance information: architecture:spot:swap:disk:vcpu:memory:notes + arch: Architecture, + spot: f64, + swap: u64, + disk: u64, + vcpu: u64, + memory: u64, + notes: String, +} + +#[derive(Debug, Deserialize)] +struct InstanceTypes(HashMap); + +static INSTANCE_DATA: &str = include_str!("launch-types.json"); + +fn instance_types() -> &'static InstanceTypes { + static ITEM: OnceLock = OnceLock::new(); + + ITEM.get_or_init(|| { + serde_json::from_str(INSTANCE_DATA).expect("Failed to parse launch-types.json") + }) +} + +/// Helper function to get branch name or "default" +fn branchname(branch: Option<&String>) -> &str { + branch.map_or("default", |s| s.as_str()) +} + +/// Create an authenticated AWS EC2 client +/// +/// This function loads AWS configuration using the standard credential chain: +/// 1. Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, etc.) +/// 2. AWS SSO credentials (if `aws sso login` has been run) +/// 3. AWS credentials file (~/.aws/credentials) +/// 4. IAM instance profile (if running on EC2) +/// 5. ECS task role (if running on ECS) +async fn create_ec2_client(region: Option<&str>) -> Result> { + let mut config_loader = aws_config::defaults(BehaviorVersion::latest()); + + if let Some(region) = region { + config_loader = config_loader.region(aws_config::Region::new(region.to_string())); + } + + let config = config_loader.load().await; + let client = Client::new(&config); + + Ok(client) +} + +#[derive(Debug, Deserialize)] +enum Architecture { + Arm64, + Amd64, +} + +#[derive(Debug, Args)] +pub struct Options { + /// EC2 instance type + #[arg( + long = "instance-type", + value_name = "TYPE", + default_value = "i4g.large", + help = "EC2 instance type" + )] + pub instance_type: String, + + /// `Firewood` git branch to checkout + #[arg( + long = "firewood-branch", + value_name = "BRANCH", + help = "Firewood git branch to checkout" + )] + pub firewood_branch: Option, + + /// `AvalancheGo` git branch to checkout + #[arg( + long = "avalanchego-branch", + value_name = "BRANCH", + help = "AvalancheGo git branch to checkout" + )] + pub avalanchego_branch: Option, + + /// `Coreth` git branch to checkout + #[arg( + long = "coreth-branch", + value_name = "BRANCH", + help = "Coreth git branch to checkout" + )] + pub coreth_branch: Option, + + /// `LibEVM` git branch to checkout + #[arg( + long = "libevm-branch", + value_name = "BRANCH", + help = "LibEVM git branch to checkout" + )] + pub libevm_branch: Option, + + /// Number of blocks to download + #[arg( + long = "nblocks", + value_name = "BLOCKS", + default_value = "1m", + help = "Number of blocks to download", + value_parser = ["1m", "10m", "50m"] + )] + pub nblocks: String, + + /// AWS region + #[arg( + long = "region", + value_name = "REGION", + default_value = "us-west-2", + help = "AWS region" + )] + pub region: String, + + /// Use spot instance pricing + #[arg( + long = "spot", + help = "Use spot instance pricing (default depends on instance type)" + )] + pub spot: bool, + + /// Show the aws command that would be run without executing it + #[arg( + long = "dry-run", + help = "Show the aws command that would be run without executing it" + )] + pub dry_run: bool, +} + +pub(super) fn run(opts: &Options) -> Result<(), api::Error> { + log::debug!("launch AWS instance {opts:?}"); + + // Use tokio to run the async function + let rt = tokio::runtime::Runtime::new().map_err(|e| { + api::Error::InternalError(format!("Failed to create tokio runtime: {e}").into()) + })?; + + rt.block_on(run_async(opts)) + .map_err(|e| api::Error::InternalError(format!("Launch failed: {e}").into())) +} + +async fn run_async(opts: &Options) -> Result<(), Box> { + info!("Launch command called with options:"); + info!(" Instance Type: {}", opts.instance_type); + info!( + " Firewood Branch: {}", + branchname(opts.firewood_branch.as_ref()) + ); + info!( + " AvalancheGo Branch: {}", + branchname(opts.avalanchego_branch.as_ref()) + ); + info!( + " Coreth Branch: {}", + branchname(opts.coreth_branch.as_ref()) + ); + info!( + " LibEVM Branch: {}", + branchname(opts.libevm_branch.as_ref()) + ); + info!(" Number of Blocks: {}", opts.nblocks); + info!(" Region: {}", opts.region); + info!(" Spot Instance: {}", opts.spot); + info!(" Dry Run: {}", opts.dry_run); + + // Validate instance type + let instance_info = instance_types(); + if let Some(instance) = instance_info.0.get(&opts.instance_type) { + info!("📋 Instance specifications:"); + info!(" Architecture: {:?}", instance.arch); + info!(" Spot Price: ${:.4}/hr", instance.spot); + info!(" Swap: {}GB", instance.swap); + info!(" Disk: {}GB", instance.disk); + info!(" vCPU: {}", instance.vcpu); + info!(" Memory: {}GB", instance.memory); + info!(" Notes: {}", instance.notes); + } else { + return Err(format!("Invalid instance type: {}", opts.instance_type).into()); + } + + // TODO: this should not be part of the source, instead it should read it from + // a file in some config directory for this tool + let mut yamls = saphyr::Yaml::load_from_str(include_str!("cloud-init.yaml"))?; + let /*mut*/ cloud_init = yamls.first_mut().expect("should at least have one yaml"); + // TODO: mutate the cloud_init file to insert the swap config + // maybe something like this + // for mut seq in cloud_init.as_mapping_mut().expect("not a mapping?") { + // if let (k, saphyr::Yaml::Tagged(tag, value)) = seq { + // // TODO: use the correct mapping + // seq = (&saphyr::Yaml::Mapping(Mapping::new()), &mut saphyr::Yaml::Mapping(Mapping::new())); + // } + // } + + let mut yaml = String::new(); + let mut emitter = saphyr::YamlEmitter::new(&mut yaml); + emitter.dump(cloud_init)?; + // this needs to get sent to the launcher + println!("{yaml}"); + + if opts.dry_run { + info!("🏃 Dry run mode - would launch instance here"); + } else { + let _client = create_ec2_client(Some(&opts.region)).await?; + // TODO: base64 encode the user data + /* client + .run_instances() + //.region(opts.region) + //.image_id(image_id) + .instance_type(InstanceType::builder().instance_type(opts.instance_type).build()) + .key_name("rkuris") + .security_groups(vec!["rkuris-starlink-only".to_string()]) + .iam_instance_profile(Some("Name=s3-readonly")) + //.user_data(user_data) + .tag_specifications(vec![ + TagSpecification::builder() + .resource_type("instance") + .tags( + Tag::builder().key("Name").value(instance_name).build(), + ) + .build(), + ]) + .block_device_mappings(vec![ + BlockDeviceMapping::builder() + .device_name("/dev/sda1") + .ebs( + EbsBlockDevice::builder() + .volume_size(50) + .volume_type("gp3") + .build(), + ) + .build(), + ]) + .send() + .await?; */ + info!("🚀 Would launch instance here (not implemented yet)"); + } + + Ok(()) +} diff --git a/fwdctl/src/main.rs b/fwdctl/src/main.rs index 3dfd7021a..9b061316c 100644 --- a/fwdctl/src/main.rs +++ b/fwdctl/src/main.rs @@ -15,6 +15,7 @@ pub mod dump; pub mod get; pub mod graph; pub mod insert; +pub mod launch; pub mod root; #[derive(Clone, Debug, Parser)] @@ -69,6 +70,8 @@ enum Commands { Graph(graph::Options), /// Runs the checker on the database Check(check::Options), + /// Launch AWS instance for benchmarking + Launch(launch::Options), } fn main() -> Result<(), api::Error> { @@ -87,6 +90,7 @@ fn main() -> Result<(), api::Error> { Commands::Dump(opts) => dump::run(opts), Commands::Graph(opts) => graph::run(opts), Commands::Check(opts) => check::run(opts), + Commands::Launch(opts) => launch::run(opts), } }