direxio-deployer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +92 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/README_zh.md +218 -0
- package/SKILL.md +722 -0
- package/agents/README.md +25 -0
- package/agents/openai.yaml +12 -0
- package/bin/direxio-deployer.mjs +375 -0
- package/package.json +28 -0
- package/references/agent-targets.md +128 -0
- package/references/architecture.md +44 -0
- package/references/bug-history.md +78 -0
- package/references/deployment-lessons.md +218 -0
- package/references/deployment-optimization-audit.md +317 -0
- package/references/deployment-workflow.md +341 -0
- package/references/iam-policy.json +52 -0
- package/references/runtime-wiring.md +209 -0
- package/references/state-machine.md +46 -0
- package/references/token-refresh.md +81 -0
- package/references/tooling.md +106 -0
- package/references/troubleshooting.md +26 -0
- package/references/user-journey.md +75 -0
- package/references/verification-recovery.md +84 -0
- package/references/voip-turn-runbook.md +154 -0
- package/references/windows-deployment-notes.md +119 -0
- package/scripts/aws-credentials.sh +195 -0
- package/scripts/cloud-init/Caddyfile +48 -0
- package/scripts/cloud-init/docker-compose.yml +125 -0
- package/scripts/cloud-init/init-tokens.sh +238 -0
- package/scripts/cloud-init/user-data.yaml +40 -0
- package/scripts/destroy.ps1 +77 -0
- package/scripts/destroy.sh +589 -0
- package/scripts/lib/aws.sh +73 -0
- package/scripts/lib/domain.sh +175 -0
- package/scripts/lib/operation_report.sh +240 -0
- package/scripts/lib/ops.sh +230 -0
- package/scripts/lib/paths.sh +35 -0
- package/scripts/lib/state.sh +137 -0
- package/scripts/mcp-tools-list.mjs +95 -0
- package/scripts/orchestrate.ps1 +112 -0
- package/scripts/orchestrate.sh +1126 -0
- package/scripts/phases/s0_prereq_aws.sh +39 -0
- package/scripts/phases/s1_preflight.sh +72 -0
- package/scripts/phases/s2_domain.sh +103 -0
- package/scripts/phases/s3_provision.sh +421 -0
- package/scripts/phases/s4_bootstrap_stack.sh +38 -0
- package/scripts/phases/s5_init_tokens.sh +118 -0
- package/scripts/phases/s6_wire_local.sh +1435 -0
- package/scripts/phases/s7_verify_e2e.sh +136 -0
- package/scripts/pricing-estimate.sh +256 -0
- package/scripts/render/render-userdata.sh +86 -0
- package/scripts/reset-app-data.sh +40 -0
- package/scripts/update.sh +30 -0
- package/tests/aws_credentials_test.sh +139 -0
- package/tests/connect_daemon_runtime_check_test.sh +120 -0
- package/tests/default_paths_test.sh +58 -0
- package/tests/destroy_local_bridge_test.sh +154 -0
- package/tests/destroy_root_identity_test.sh +91 -0
- package/tests/destroy_route53_zone_test.sh +80 -0
- package/tests/domain_authoritative_dns_test.sh +49 -0
- package/tests/mcp_doctor_runtime_check_test.sh +86 -0
- package/tests/mcp_smoke_runtime_check_test.sh +121 -0
- package/tests/mcp_tools_runtime_check_test.sh +123 -0
- package/tests/npm_skill_distribution_test.sh +95 -0
- package/tests/operation_report_test.sh +258 -0
- package/tests/orchestrate_status_recovery_test.sh +91 -0
- package/tests/phase_timeout_test.sh +88 -0
- package/tests/pricing_estimate_test.sh +159 -0
- package/tests/render_userdata_remote_nodes_test.sh +40 -0
- package/tests/root_volume_tracking_test.sh +41 -0
- package/tests/route53_overwrite_guard_test.sh +86 -0
- package/tests/route53_zone_auto_create_test.sh +66 -0
- package/tests/runtime_summary_check_test.sh +203 -0
- package/tests/s6_wire_local_test.sh +405 -0
- package/tests/skill_structure_test.sh +298 -0
- package/tests/update_reset_ops_test.sh +230 -0
- package/tests/user_confirmation_gates_test.sh +152 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# S7 VERIFY_E2E - end-to-end acceptance. DONE only when every check passes.
|
|
3
|
+
#
|
|
4
|
+
# Checks: healthz, Matrix versions, Matrix federation well-known, owner.json+CORS,
|
|
5
|
+
# token-authenticated /_p2p command, and non-empty TURN turnServer.
|
|
6
|
+
# Local bridge message send/read is validated separately; this script checks HTTP actions.
|
|
7
|
+
|
|
8
|
+
run_phase() {
|
|
9
|
+
phase_set S7_VERIFY_E2E in_progress "running end-to-end acceptance"
|
|
10
|
+
local domain token password
|
|
11
|
+
domain=$(state_get domain)
|
|
12
|
+
password=$(state_get password)
|
|
13
|
+
local fails=0
|
|
14
|
+
|
|
15
|
+
_check "healthz" "https://$domain/healthz" "" 200 || fails=$((fails+1))
|
|
16
|
+
_check "matrix versions" "https://$domain/_matrix/client/versions" "" 200 || fails=$((fails+1))
|
|
17
|
+
_check_matrix_server_wellknown "$domain" || fails=$((fails+1))
|
|
18
|
+
_check_owner_cors "$domain" || fails=$((fails+1))
|
|
19
|
+
token=$(_p2p_access_token "$domain" "$password")
|
|
20
|
+
if [ -n "$token" ]; then
|
|
21
|
+
_check_p2p_agent_auth "$domain" "$token" || fails=$((fails+1))
|
|
22
|
+
else
|
|
23
|
+
warn " ✗ _p2p/query mcp.messages.list (failed to exchange fresh access_token)"
|
|
24
|
+
fails=$((fails+1))
|
|
25
|
+
fi
|
|
26
|
+
_check_turn "$domain" "$password" || fails=$((fails+1))
|
|
27
|
+
|
|
28
|
+
if [ "$fails" -eq 0 ]; then
|
|
29
|
+
phase_set S7_VERIFY_E2E done "all green"
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
32
|
+
phase_set S7_VERIFY_E2E failed "$fails checks failed"
|
|
33
|
+
warn "$fails acceptance checks failed. See references/troubleshooting.md for targeted fixes."
|
|
34
|
+
return 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_check_p2p_agent_auth() {
|
|
38
|
+
local domain=$1 token=$2 code body
|
|
39
|
+
local room_id
|
|
40
|
+
room_id=$(state_get agent_room_id)
|
|
41
|
+
local args=()
|
|
42
|
+
while IFS= read -r arg; do args+=("$arg"); done < <(curl_resolve_args "$domain")
|
|
43
|
+
body=$(mktemp)
|
|
44
|
+
code=$(curl -sk "${args[@]}" -o "$body" -w '%{http_code}' \
|
|
45
|
+
-X POST "https://$domain/_p2p/query" \
|
|
46
|
+
-H 'Content-Type: application/json' \
|
|
47
|
+
-H "Authorization: Bearer $token" \
|
|
48
|
+
-d "{\"action\":\"mcp.messages.list\",\"params\":{\"room_id\":\"$room_id\",\"limit\":1}}" 2>/dev/null)
|
|
49
|
+
if [ "$code" = "200" ] && jq -e '(.messages | type == "array") and (.room_id | type == "string")' "$body" >/dev/null 2>&1; then
|
|
50
|
+
rm -f "$body"
|
|
51
|
+
ok " ✓ _p2p/query mcp.messages.list (agent token)"
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
warn " ✗ _p2p/query mcp.messages.list (got $code, body=$(head -c 120 "$body" 2>/dev/null))"
|
|
55
|
+
rm -f "$body"
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_p2p_access_token() {
|
|
60
|
+
local domain=$1 password=$2 at
|
|
61
|
+
local args=()
|
|
62
|
+
while IFS= read -r arg; do args+=("$arg"); done < <(curl_resolve_args "$domain")
|
|
63
|
+
at=$(curl -sk "${args[@]}" -X POST "https://$domain/_p2p/command" -H 'Content-Type: application/json' \
|
|
64
|
+
-d "{\"action\":\"portal.auth\",\"params\":{\"password\":\"$password\"}}" 2>/dev/null | jq -r '.access_token // empty')
|
|
65
|
+
printf '%s' "$at"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
curl_resolve_args() {
|
|
69
|
+
local domain=$1 pubip
|
|
70
|
+
pubip=$(res_get public_ip)
|
|
71
|
+
[ -n "$pubip" ] && printf '%s\n' --resolve "$domain:443:$pubip"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Web client reads owner.json from the local dev origin. HTTP 200 without CORS
|
|
75
|
+
# still fails in the browser, so S7 validates the response header.
|
|
76
|
+
_check_owner_cors() {
|
|
77
|
+
local domain=$1 tmp code cors
|
|
78
|
+
tmp=$(mktemp)
|
|
79
|
+
local args=()
|
|
80
|
+
while IFS= read -r arg; do args+=("$arg"); done < <(curl_resolve_args "$domain")
|
|
81
|
+
code=$(curl -sk "${args[@]}" -o /dev/null -D "$tmp" -w '%{http_code}' \
|
|
82
|
+
-H 'Origin: http://127.0.0.1:51820' \
|
|
83
|
+
"https://$domain/.well-known/portal/owner.json" 2>/dev/null)
|
|
84
|
+
cors=$(grep -i '^Access-Control-Allow-Origin:' "$tmp" | tr -d '\r' | head -n 1 || true)
|
|
85
|
+
rm -f "$tmp"
|
|
86
|
+
|
|
87
|
+
if [ "$code" = "200" ] && printf '%s' "$cors" | grep -Eiq 'Access-Control-Allow-Origin:[[:space:]]*(\*|http://127\.0\.0\.1:51820)$'; then
|
|
88
|
+
ok " ✓ portal owner.json (200 + CORS)"; return 0
|
|
89
|
+
fi
|
|
90
|
+
warn " x portal owner.json CORS invalid (code=$code, header=${cors:-<missing>})"; return 1
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Federation acceptance: when server_name is a bare domain, remote homeservers
|
|
94
|
+
# default to 8448. This deployment exposes 443, so well-known must point there.
|
|
95
|
+
_check_matrix_server_wellknown() {
|
|
96
|
+
local domain=$1 body
|
|
97
|
+
local args=()
|
|
98
|
+
while IFS= read -r arg; do args+=("$arg"); done < <(curl_resolve_args "$domain")
|
|
99
|
+
body=$(curl -sk "${args[@]}" "https://$domain/.well-known/matrix/server" 2>/dev/null)
|
|
100
|
+
if printf '%s' "$body" | jq -e --arg want "$domain:443" '.["m.server"] == $want' >/dev/null 2>&1; then
|
|
101
|
+
ok " ✓ matrix federation well-known ($domain:443)"; return 0
|
|
102
|
+
fi
|
|
103
|
+
warn " x matrix federation well-known invalid:$(printf '%s' "$body" | head -c 120)"; return 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# _check <name> <url> <bearer-token-or-empty> <expected-code>
|
|
107
|
+
_check() {
|
|
108
|
+
local name=$1 url=$2 tok=$3 want=$4 code
|
|
109
|
+
local domain args=()
|
|
110
|
+
domain=$(state_get domain)
|
|
111
|
+
while IFS= read -r arg; do args+=("$arg"); done < <(curl_resolve_args "$domain")
|
|
112
|
+
if [ -n "$tok" ]; then
|
|
113
|
+
code=$(curl -sk "${args[@]}" -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" "$url" 2>/dev/null)
|
|
114
|
+
else
|
|
115
|
+
code=$(curl -sk "${args[@]}" -o /dev/null -w '%{http_code}' "$url" 2>/dev/null)
|
|
116
|
+
fi
|
|
117
|
+
if [ "$code" = "$want" ]; then ok " ✓ $name ($code)"; return 0
|
|
118
|
+
else warn " ✗ $name (got $code, want $want)"; return 1; fi
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# TURN acceptance: exchange the backend password/init-code field for Matrix access_token, then verify
|
|
122
|
+
# /voip/turnServer returns non-empty valid TURN credentials.
|
|
123
|
+
_check_turn() {
|
|
124
|
+
local domain=$1 password=$2 at turn
|
|
125
|
+
local args=()
|
|
126
|
+
while IFS= read -r arg; do args+=("$arg"); done < <(curl_resolve_args "$domain")
|
|
127
|
+
at=$(_p2p_access_token "$domain" "$password")
|
|
128
|
+
if [ -z "$at" ]; then warn " x TURN (failed to exchange access_token; cannot verify turnServer)"; return 1; fi
|
|
129
|
+
turn=$(curl -sk "${args[@]}" "https://$domain/_matrix/client/v3/voip/turnServer" \
|
|
130
|
+
-H "Authorization: Bearer $at" 2>/dev/null)
|
|
131
|
+
if printf '%s' "$turn" | jq -e '(.uris|type=="array" and length>0) and (any(.uris[]; test("^turns?:"))) and (.username|tostring|length>0) and (.password|tostring|length>0) and (.ttl>0)' >/dev/null 2>&1; then
|
|
132
|
+
ok " ✓ TURN turnServer non-empty and valid"; return 0
|
|
133
|
+
else
|
|
134
|
+
warn " x TURN turnServer invalid/empty:$(printf '%s' "$turn" | head -c 120)"; return 1
|
|
135
|
+
fi
|
|
136
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# pricing-estimate.sh - estimate monthly AWS costs for a Direxio EC2 node.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
usage() {
|
|
6
|
+
cat >&2 <<'EOF'
|
|
7
|
+
Usage:
|
|
8
|
+
scripts/pricing-estimate.sh --state <state.json> [--write-state]
|
|
9
|
+
scripts/pricing-estimate.sh --region <region> --instance-type <type> --disk-gb <gb> --domain-mode <user|route53>
|
|
10
|
+
|
|
11
|
+
Queries AWS Price List where possible. Falls back conservatively when pricing is unavailable.
|
|
12
|
+
EOF
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
region_location() {
|
|
16
|
+
case "$1" in
|
|
17
|
+
us-east-1) echo "US East (N. Virginia)" ;;
|
|
18
|
+
us-east-2) echo "US East (Ohio)" ;;
|
|
19
|
+
us-west-1) echo "US West (N. California)" ;;
|
|
20
|
+
us-west-2) echo "US West (Oregon)" ;;
|
|
21
|
+
ap-southeast-1) echo "Asia Pacific (Singapore)" ;;
|
|
22
|
+
ap-southeast-2) echo "Asia Pacific (Sydney)" ;;
|
|
23
|
+
ap-northeast-1) echo "Asia Pacific (Tokyo)" ;;
|
|
24
|
+
ap-northeast-2) echo "Asia Pacific (Seoul)" ;;
|
|
25
|
+
ap-east-1) echo "Asia Pacific (Hong Kong)" ;;
|
|
26
|
+
eu-west-1) echo "EU (Ireland)" ;;
|
|
27
|
+
eu-central-1) echo "EU (Frankfurt)" ;;
|
|
28
|
+
*) echo "" ;;
|
|
29
|
+
esac
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
price_from_get_products() {
|
|
33
|
+
local service=$1 location=$2 shift_count=2 json
|
|
34
|
+
shift "$shift_count"
|
|
35
|
+
json=$(aws pricing get-products \
|
|
36
|
+
--region us-east-1 \
|
|
37
|
+
--service-code "$service" \
|
|
38
|
+
--filters "Type=TERM_MATCH,Field=location,Value=$location" "$@" \
|
|
39
|
+
--max-results 1 \
|
|
40
|
+
--output json 2>/dev/null) || return 1
|
|
41
|
+
printf '%s\n' "$json" | jq -r '
|
|
42
|
+
.PriceList[0]
|
|
43
|
+
| fromjson
|
|
44
|
+
| .terms.OnDemand
|
|
45
|
+
| to_entries[0].value.priceDimensions
|
|
46
|
+
| to_entries[0].value.pricePerUnit.USD
|
|
47
|
+
' 2>/dev/null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
numeric_or_empty() {
|
|
51
|
+
case "${1:-}" in
|
|
52
|
+
''|null|None) return 1 ;;
|
|
53
|
+
*[!0-9.]* ) return 1 ;;
|
|
54
|
+
*) printf '%s\n' "$1" ;;
|
|
55
|
+
esac
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
lookup_ec2_hourly() {
|
|
59
|
+
local location=$1 instance_type=$2 raw
|
|
60
|
+
raw=$(price_from_get_products AmazonEC2 "$location" \
|
|
61
|
+
"Type=TERM_MATCH,Field=instanceType,Value=$instance_type" \
|
|
62
|
+
"Type=TERM_MATCH,Field=operatingSystem,Value=Linux" \
|
|
63
|
+
"Type=TERM_MATCH,Field=tenancy,Value=Shared" \
|
|
64
|
+
"Type=TERM_MATCH,Field=preInstalledSw,Value=NA" \
|
|
65
|
+
"Type=TERM_MATCH,Field=capacitystatus,Value=Used" \
|
|
66
|
+
) || return 1
|
|
67
|
+
numeric_or_empty "$raw"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
lookup_gp3_gb_month() {
|
|
71
|
+
local location=$1 raw
|
|
72
|
+
raw=$(price_from_get_products AmazonEC2 "$location" \
|
|
73
|
+
"Type=TERM_MATCH,Field=productFamily,Value=Storage" \
|
|
74
|
+
"Type=TERM_MATCH,Field=volumeApiName,Value=gp3" \
|
|
75
|
+
) || return 1
|
|
76
|
+
numeric_or_empty "$raw"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
lookup_public_ipv4_hourly() {
|
|
80
|
+
local location=$1 raw
|
|
81
|
+
raw=$(price_from_get_products AmazonVPC "$location" \
|
|
82
|
+
"Type=TERM_MATCH,Field=productFamily,Value=Public IPv4 Address" \
|
|
83
|
+
) || return 1
|
|
84
|
+
numeric_or_empty "$raw"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
round2() {
|
|
88
|
+
awk -v n="${1:-0}" 'BEGIN { printf "%.2f", n + 0 }'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
build_estimate() {
|
|
92
|
+
local region=$1 instance_type=$2 disk_gb=$3 domain_mode=$4
|
|
93
|
+
local hours=730 location ec2_hourly gp3_rate public_ipv4_hourly route53_monthly status warnings_json
|
|
94
|
+
local ec2_source gp3_source ipv4_source
|
|
95
|
+
location=$(region_location "$region")
|
|
96
|
+
status=queried
|
|
97
|
+
warnings_json='[]'
|
|
98
|
+
|
|
99
|
+
if [ -z "$location" ]; then
|
|
100
|
+
status=fallback
|
|
101
|
+
warnings_json=$(jq -cn --arg w "Region is not mapped to an AWS Pricing location; using conservative fallback estimates" '$ARGS.named | [.w]')
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [ "$status" = "queried" ] && ec2_hourly=$(lookup_ec2_hourly "$location" "$instance_type"); then
|
|
105
|
+
ec2_source=aws_pricing
|
|
106
|
+
else
|
|
107
|
+
status=fallback
|
|
108
|
+
ec2_hourly=${DIREXIO_FALLBACK_EC2_HOURLY_USD:-0.030}
|
|
109
|
+
ec2_source=fallback
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
if [ "$status" = "queried" ] && gp3_rate=$(lookup_gp3_gb_month "$location"); then
|
|
113
|
+
gp3_source=aws_pricing
|
|
114
|
+
else
|
|
115
|
+
status=fallback
|
|
116
|
+
gp3_rate=${DIREXIO_FALLBACK_GP3_GB_MONTH_USD:-0.10}
|
|
117
|
+
gp3_source=fallback
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
if [ "$status" = "queried" ] && public_ipv4_hourly=$(lookup_public_ipv4_hourly "$location"); then
|
|
121
|
+
ipv4_source=aws_pricing
|
|
122
|
+
else
|
|
123
|
+
[ "$status" = "fallback" ] || status=fallback
|
|
124
|
+
public_ipv4_hourly=${DIREXIO_FALLBACK_PUBLIC_IPV4_HOURLY_USD:-0.005}
|
|
125
|
+
ipv4_source=fallback
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
if [ "$status" = "fallback" ]; then
|
|
129
|
+
warnings_json=$(printf '%s\n' "$warnings_json" | jq '. + ["AWS Pricing API unavailable; using conservative fallback estimates"] | unique')
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
if [ "$domain_mode" = "route53" ]; then
|
|
133
|
+
route53_monthly=${DIREXIO_ROUTE53_HOSTED_ZONE_MONTHLY_USD:-0.50}
|
|
134
|
+
else
|
|
135
|
+
route53_monthly=0
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
jq -n \
|
|
139
|
+
--arg pricing_status "$status" \
|
|
140
|
+
--arg region "$region" \
|
|
141
|
+
--arg location "$location" \
|
|
142
|
+
--arg instance_type "$instance_type" \
|
|
143
|
+
--arg domain_mode "$domain_mode" \
|
|
144
|
+
--arg ec2_source "$ec2_source" \
|
|
145
|
+
--arg gp3_source "$gp3_source" \
|
|
146
|
+
--arg ipv4_source "$ipv4_source" \
|
|
147
|
+
--argjson warnings "$warnings_json" \
|
|
148
|
+
--argjson hours "$hours" \
|
|
149
|
+
--argjson disk_gb "$disk_gb" \
|
|
150
|
+
--argjson ec2_hourly "$ec2_hourly" \
|
|
151
|
+
--argjson ec2_monthly "$(round2 "$(awk -v h="$ec2_hourly" -v m="$hours" 'BEGIN { print h*m }')")" \
|
|
152
|
+
--argjson gp3_rate "$gp3_rate" \
|
|
153
|
+
--argjson gp3_monthly "$(round2 "$(awk -v r="$gp3_rate" -v gb="$disk_gb" 'BEGIN { print r*gb }')")" \
|
|
154
|
+
--argjson ipv4_hourly "$public_ipv4_hourly" \
|
|
155
|
+
--argjson ipv4_monthly "$(round2 "$(awk -v h="$public_ipv4_hourly" -v m="$hours" 'BEGIN { print h*m }')")" \
|
|
156
|
+
--argjson route53_monthly "$route53_monthly" '
|
|
157
|
+
def total:
|
|
158
|
+
(.components.ec2_instance.monthly_usd
|
|
159
|
+
+ .components.ebs_gp3.monthly_usd
|
|
160
|
+
+ .components.public_ipv4.monthly_usd
|
|
161
|
+
+ .components.route53_hosted_zone.monthly_usd);
|
|
162
|
+
{
|
|
163
|
+
pricing_status: $pricing_status,
|
|
164
|
+
region: $region,
|
|
165
|
+
location: $location,
|
|
166
|
+
hours_per_month: $hours,
|
|
167
|
+
warnings: $warnings,
|
|
168
|
+
components: {
|
|
169
|
+
ec2_instance: {
|
|
170
|
+
instance_type: $instance_type,
|
|
171
|
+
hourly_usd: $ec2_hourly,
|
|
172
|
+
monthly_usd: $ec2_monthly,
|
|
173
|
+
source: $ec2_source
|
|
174
|
+
},
|
|
175
|
+
ebs_gp3: {
|
|
176
|
+
storage_gb: $disk_gb,
|
|
177
|
+
gb_month_usd: $gp3_rate,
|
|
178
|
+
monthly_usd: $gp3_monthly,
|
|
179
|
+
source: $gp3_source
|
|
180
|
+
},
|
|
181
|
+
public_ipv4: {
|
|
182
|
+
hourly_usd: $ipv4_hourly,
|
|
183
|
+
monthly_usd: $ipv4_monthly,
|
|
184
|
+
billed_even_when_attached: true,
|
|
185
|
+
source: $ipv4_source
|
|
186
|
+
},
|
|
187
|
+
route53_hosted_zone: {
|
|
188
|
+
monthly_usd: $route53_monthly,
|
|
189
|
+
included: ($domain_mode == "route53")
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
notes: [
|
|
193
|
+
"Estimate excludes data transfer, TURN relay traffic, domain registration, taxes, and AWS credit eligibility.",
|
|
194
|
+
"Public IPv4 is billed hourly by AWS even when attached to a running instance.",
|
|
195
|
+
"AWS credits may reduce charges only when the account, plan, region, and service usage are eligible; verify in AWS Billing Console."
|
|
196
|
+
],
|
|
197
|
+
recommendations: [
|
|
198
|
+
"Set an AWS Budget or billing alert before leaving the node running.",
|
|
199
|
+
"Review AWS Billing Console after deployment and after destroy to confirm actual charges and remaining credits."
|
|
200
|
+
]
|
|
201
|
+
} | .total_monthly_usd = ((total * 100 | round) / 100)
|
|
202
|
+
'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
state=""
|
|
206
|
+
write_state=0
|
|
207
|
+
region=""
|
|
208
|
+
instance_type=""
|
|
209
|
+
disk_gb=""
|
|
210
|
+
domain_mode=""
|
|
211
|
+
|
|
212
|
+
while [ "$#" -gt 0 ]; do
|
|
213
|
+
case "$1" in
|
|
214
|
+
--state) state=${2:-}; shift 2 ;;
|
|
215
|
+
--write-state) write_state=1; shift ;;
|
|
216
|
+
--region) region=${2:-}; shift 2 ;;
|
|
217
|
+
--instance-type) instance_type=${2:-}; shift 2 ;;
|
|
218
|
+
--disk-gb) disk_gb=${2:-}; shift 2 ;;
|
|
219
|
+
--domain-mode) domain_mode=${2:-}; shift 2 ;;
|
|
220
|
+
-h|--help) usage; exit 0 ;;
|
|
221
|
+
*) usage; exit 1 ;;
|
|
222
|
+
esac
|
|
223
|
+
done
|
|
224
|
+
|
|
225
|
+
if [ -n "$state" ]; then
|
|
226
|
+
[ -f "$state" ] || {
|
|
227
|
+
echo "state.json not found: $state" >&2
|
|
228
|
+
exit 1
|
|
229
|
+
}
|
|
230
|
+
region=${region:-$(jq -r '.region // empty' "$state")}
|
|
231
|
+
instance_type=${instance_type:-$(jq -r '.instance_type // empty' "$state")}
|
|
232
|
+
domain_mode=${domain_mode:-$(jq -r '.domain_mode // "user"' "$state")}
|
|
233
|
+
disk_gb=${disk_gb:-$(jq -r '.resources.root_volume_gb // .root_volume_gb // "8"' "$state")}
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
region=${region:-${AWS_DEFAULT_REGION:-${AWS_REGION:-}}}
|
|
237
|
+
instance_type=${instance_type:-t3.small}
|
|
238
|
+
disk_gb=${disk_gb:-8}
|
|
239
|
+
domain_mode=${domain_mode:-user}
|
|
240
|
+
|
|
241
|
+
[ -n "$region" ] || {
|
|
242
|
+
echo "region is required for pricing estimate" >&2
|
|
243
|
+
exit 1
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
estimate=$(build_estimate "$region" "$instance_type" "$disk_gb" "$domain_mode")
|
|
247
|
+
if [ "$write_state" = "1" ]; then
|
|
248
|
+
[ -n "$state" ] || {
|
|
249
|
+
echo "--write-state requires --state" >&2
|
|
250
|
+
exit 1
|
|
251
|
+
}
|
|
252
|
+
tmp="$state.tmp.$$"
|
|
253
|
+
jq --argjson estimate "$estimate" '.cost_estimate = $estimate' "$state" > "$tmp" && mv "$tmp" "$state"
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
printf '%s\n' "$estimate"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# render-userdata.sh - render final cloud-init user-data.
|
|
3
|
+
#
|
|
4
|
+
# Bundle cloud-init deployment files (docker-compose.yml / Caddyfile /
|
|
5
|
+
# init-tokens.sh) into a tar.gz, inline it as one write_files entry, and unpack
|
|
6
|
+
# it to /opt/p2p in runcmd. Comment-only lines are stripped at the end to keep
|
|
7
|
+
# AWS user-data below the 16384-byte limit. Replaces __DOMAIN__ /
|
|
8
|
+
# __ACME_EMAIL__ / __MESSAGE_SERVER_IMAGE__; the EC2 instance does not need to
|
|
9
|
+
# clone repos.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# render-userdata.sh --domain <domain> --acme <email> --message-server-image <img> > user-data.yaml
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
HERE=$(cd "$(dirname "$0")/.." && pwd)
|
|
16
|
+
CI="$HERE/cloud-init"
|
|
17
|
+
source "$HERE/lib/domain.sh"
|
|
18
|
+
|
|
19
|
+
DOMAIN=""; ACME=""; MESSAGE_SERVER_IMAGE=""
|
|
20
|
+
while [ $# -gt 0 ]; do
|
|
21
|
+
case "$1" in
|
|
22
|
+
--domain) DOMAIN=$2; shift 2;;
|
|
23
|
+
--acme) ACME=$2; shift 2;;
|
|
24
|
+
--message-server-image) MESSAGE_SERVER_IMAGE=$2; shift 2;;
|
|
25
|
+
--as-image) MESSAGE_SERVER_IMAGE=$2; shift 2;;
|
|
26
|
+
*) echo "unknown arg: $1" >&2; exit 1;;
|
|
27
|
+
esac
|
|
28
|
+
done
|
|
29
|
+
[ -n "$MESSAGE_SERVER_IMAGE" ] || { echo "--message-server-image required" >&2; exit 1; }
|
|
30
|
+
[ -n "$DOMAIN" ] || { echo "--domain required; production deployments require a real domain" >&2; exit 1; }
|
|
31
|
+
DOMAIN=$(domain_normalize "$DOMAIN")
|
|
32
|
+
[ "$DOMAIN" != "PLACEHOLDER" ] || { echo "PLACEHOLDER/sslip.io domains are not accepted in the production renderer" >&2; exit 1; }
|
|
33
|
+
domain_is_formal_name "$DOMAIN" || { echo "invalid production domain: $DOMAIN" >&2; exit 1; }
|
|
34
|
+
|
|
35
|
+
# Single-line base64 compatible with GNU/Linux and macOS/BSD base64.
|
|
36
|
+
b64() { base64 | tr -d '\n'; }
|
|
37
|
+
sed_replacement_escape() { printf '%s' "$1" | sed 's/[\\&#]/\\&/g'; }
|
|
38
|
+
|
|
39
|
+
# Build a deterministic tar.gz bundle with fixed permissions and no extra attrs.
|
|
40
|
+
WORK=$(mktemp -d)
|
|
41
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
42
|
+
cp "$CI/docker-compose.yml" "$WORK/docker-compose.yml"
|
|
43
|
+
cp "$CI/Caddyfile" "$WORK/Caddyfile"
|
|
44
|
+
tr -d '\r' < "$CI/init-tokens.sh" > "$WORK/init-tokens.sh"
|
|
45
|
+
chmod 0644 "$WORK/docker-compose.yml" "$WORK/Caddyfile"
|
|
46
|
+
chmod 0755 "$WORK/init-tokens.sh"
|
|
47
|
+
find "$WORK" -name '._*' -delete
|
|
48
|
+
# -C creates a flat archive. Explicit gzip avoids macOS tar stdout quirks.
|
|
49
|
+
# COPYFILE_DISABLE=1 avoids AppleDouble ._* extended-attribute files.
|
|
50
|
+
BUNDLE_B64=$(COPYFILE_DISABLE=1 tar -C "$WORK" -cf - docker-compose.yml Caddyfile init-tokens.sh | gzip -n | b64)
|
|
51
|
+
|
|
52
|
+
# Generate user-data: append the bundle entry to write_files and unpack first in runcmd.
|
|
53
|
+
# Avoid passing multiline strings via awk -v; macOS awk rejects newline in string.
|
|
54
|
+
EXTRA_WF=$(mktemp); trap 'rm -rf "$WORK" "$EXTRA_WF"' EXIT
|
|
55
|
+
cat > "$EXTRA_WF" <<EOF
|
|
56
|
+
- path: /opt/p2p/bundle.tar.gz
|
|
57
|
+
permissions: '0644'
|
|
58
|
+
encoding: b64
|
|
59
|
+
content: $BUNDLE_B64
|
|
60
|
+
EOF
|
|
61
|
+
|
|
62
|
+
# Insert unpack as the first runcmd step before Docker install / compose up.
|
|
63
|
+
UNPACK=' - mkdir -p /opt/p2p && tar -xzf /opt/p2p/bundle.tar.gz -C /opt/p2p && chmod 0755 /opt/p2p/init-tokens.sh'
|
|
64
|
+
|
|
65
|
+
strip_userdata_comments() {
|
|
66
|
+
awk '
|
|
67
|
+
NR == 1 && $0 == "#cloud-config" { print; next }
|
|
68
|
+
/^[[:space:]]*#/ { next }
|
|
69
|
+
{ print }
|
|
70
|
+
'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
awk -v wf="$EXTRA_WF" -v unpack="$UNPACK" '
|
|
74
|
+
# Insert bundle entry before runcmd.
|
|
75
|
+
/^runcmd:/ && !wfdone {
|
|
76
|
+
while ((getline line < wf) > 0) print line
|
|
77
|
+
close(wf)
|
|
78
|
+
print
|
|
79
|
+
print unpack
|
|
80
|
+
wfdone=1
|
|
81
|
+
next
|
|
82
|
+
}
|
|
83
|
+
{ print }
|
|
84
|
+
' "$CI/user-data.yaml" \
|
|
85
|
+
| sed "s#__DOMAIN__#$(sed_replacement_escape "$DOMAIN")#g; s#__ACME_EMAIL__#$(sed_replacement_escape "$ACME")#g; s#__MESSAGE_SERVER_IMAGE__#$(sed_replacement_escape "$MESSAGE_SERVER_IMAGE")#g" \
|
|
86
|
+
| strip_userdata_comments
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# reset-app-data.sh - clear app data on an existing node while preserving infra/TLS.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
6
|
+
# shellcheck disable=SC1090
|
|
7
|
+
source "$HERE/lib/paths.sh"
|
|
8
|
+
# shellcheck disable=SC1090
|
|
9
|
+
source "$HERE/lib/operation_report.sh"
|
|
10
|
+
# shellcheck disable=SC1090
|
|
11
|
+
source "$HERE/lib/ops.sh"
|
|
12
|
+
|
|
13
|
+
STATE_JSON=$(ops_state_path "${1:-}")
|
|
14
|
+
ops_require_state "$STATE_JSON"
|
|
15
|
+
|
|
16
|
+
if [ "${DIREXIO_RESET_APP_DATA_CONFIRM:-0}" != "1" ]; then
|
|
17
|
+
cat >&2 <<'EOF'
|
|
18
|
+
reset-app-data is destructive for application data.
|
|
19
|
+
It preserves EC2, Elastic IP/public IPv4, DNS, and Caddy TLS volumes, but clears Matrix/message-server data.
|
|
20
|
+
Set DIREXIO_RESET_APP_DATA_CONFIRM=1 to continue.
|
|
21
|
+
EOF
|
|
22
|
+
exit 2
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
remote_command=$(ops_reset_remote_command)
|
|
26
|
+
ops_ssh "$STATE_JSON" "$remote_command"
|
|
27
|
+
ops_mark_refresh_pending "$STATE_JSON" S4_BOOTSTRAP_STACK
|
|
28
|
+
if ops_stop_scoped_daemon "$STATE_JSON"; then
|
|
29
|
+
bridge_stop_message="Scoped local bridge daemon was stopped; rerun S6 to install fresh config."
|
|
30
|
+
else
|
|
31
|
+
bridge_stop_message="Scoped local bridge daemon stop was skipped or not needed."
|
|
32
|
+
fi
|
|
33
|
+
report=$(ops_write_report reset_app_data reset_remote_data_cleared_refresh_pending "$STATE_JSON")
|
|
34
|
+
|
|
35
|
+
echo "Application data reset complete on the existing node."
|
|
36
|
+
echo "Caddy TLS storage was preserved."
|
|
37
|
+
echo "Old user confirmations and runtime checks were cleared."
|
|
38
|
+
echo "$bridge_stop_message"
|
|
39
|
+
echo "Local S4-S7 gates were reset; rerun orchestrate with P2P_EXISTING_STATE_ACTION=continue."
|
|
40
|
+
echo "operation report: $report"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# update.sh - update an existing EC2 node without recreating infra or deleting data.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
6
|
+
# shellcheck disable=SC1090
|
|
7
|
+
source "$HERE/lib/paths.sh"
|
|
8
|
+
# shellcheck disable=SC1090
|
|
9
|
+
source "$HERE/lib/operation_report.sh"
|
|
10
|
+
# shellcheck disable=SC1090
|
|
11
|
+
source "$HERE/lib/ops.sh"
|
|
12
|
+
|
|
13
|
+
STATE_JSON=$(ops_state_path "${1:-}")
|
|
14
|
+
ops_require_state "$STATE_JSON"
|
|
15
|
+
|
|
16
|
+
remote_command=$(ops_update_remote_command "${MESSAGE_SERVER_IMAGE:-}")
|
|
17
|
+
ops_ssh "$STATE_JSON" "$remote_command"
|
|
18
|
+
ops_mark_refresh_pending "$STATE_JSON" S4_BOOTSTRAP_STACK
|
|
19
|
+
if ops_stop_scoped_daemon "$STATE_JSON"; then
|
|
20
|
+
bridge_stop_message="Scoped local bridge daemon was stopped; rerun S6 to install fresh config."
|
|
21
|
+
else
|
|
22
|
+
bridge_stop_message="Scoped local bridge daemon stop was skipped or not needed."
|
|
23
|
+
fi
|
|
24
|
+
report=$(ops_write_report update update_remote_restart_complete_refresh_pending "$STATE_JSON")
|
|
25
|
+
|
|
26
|
+
echo "Update remote restart complete."
|
|
27
|
+
echo "Old user confirmations and runtime checks were cleared."
|
|
28
|
+
echo "$bridge_stop_message"
|
|
29
|
+
echo "Local S4-S7 gates were reset; rerun orchestrate with P2P_EXISTING_STATE_ACTION=continue to refresh credentials, MCP, and verification."
|
|
30
|
+
echo "operation report: $report"
|