feat(guanghu-os): add authenticated native public projection
This commit is contained in:
parent
95385e812a
commit
2e076c4d5e
14 changed files with 780 additions and 4 deletions
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Accept one SSH-authenticated native projection state transition."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import secrets
|
||||
import shlex
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
SCHEMA = "guanghu.native-public-projection-state/v1"
|
||||
|
||||
|
||||
def parse_command(command: str) -> tuple[str, str, str, str, str]:
|
||||
fields = shlex.split(command)
|
||||
if len(fields) != 5 or fields[0] not in {"ready", "dormant"}:
|
||||
raise ValueError("exact ready or dormant projection command required")
|
||||
return fields[0], fields[1], fields[2], fields[3], fields[4]
|
||||
|
||||
|
||||
def atomic_write(path: pathlib.Path, payload: dict[str, object]) -> None:
|
||||
if not path.is_absolute() or not path.parent.is_dir():
|
||||
raise ValueError("state file must have an existing absolute parent")
|
||||
encoded = (json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
||||
descriptor, temporary_name = tempfile.mkstemp(
|
||||
dir=path.parent,
|
||||
prefix=f".{path.name}.",
|
||||
)
|
||||
temporary = pathlib.Path(temporary_name)
|
||||
try:
|
||||
os.fchmod(descriptor, 0o640)
|
||||
with os.fdopen(descriptor, "wb") as output:
|
||||
output.write(encoded)
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
os.replace(temporary, path)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--state-file", required=True)
|
||||
parser.add_argument("--expected-node", required=True)
|
||||
parser.add_argument("--expected-relay", required=True)
|
||||
parser.add_argument("--expected-candidate-sha256", required=True)
|
||||
parser.add_argument("--expected-source-commit", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
action, node, candidate_sha, source_commit, relay = parse_command(
|
||||
os.environ.get("SSH_ORIGINAL_COMMAND", "")
|
||||
)
|
||||
except ValueError as error:
|
||||
raise SystemExit(str(error)) from error
|
||||
expected = (
|
||||
args.expected_node,
|
||||
args.expected_candidate_sha256,
|
||||
args.expected_source_commit,
|
||||
args.expected_relay,
|
||||
)
|
||||
if (node, candidate_sha, source_commit, relay) != expected:
|
||||
raise SystemExit("projection command does not match the pinned deployment")
|
||||
if len(candidate_sha) != 64 or len(source_commit) != 40:
|
||||
raise SystemExit("projection hashes are malformed")
|
||||
|
||||
payload: dict[str, object] = {
|
||||
"schema": SCHEMA,
|
||||
"status": "READY_NATIVE_RESIDENT" if action == "ready" else "DORMANT",
|
||||
"node_id": node,
|
||||
"relay_node": relay,
|
||||
"candidate_sha256": candidate_sha,
|
||||
"source_commit": source_commit,
|
||||
"accepted_at_epoch": time.time(),
|
||||
"receipt_nonce": secrets.token_hex(16),
|
||||
"transport_authentication": "SSH_FORCED_COMMAND",
|
||||
}
|
||||
atomic_write(pathlib.Path(args.state_file), payload)
|
||||
print(json.dumps(payload, sort_keys=True, separators=(",", ":")))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Serve pinned public snapshots only while an authenticated native state is fresh."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Any
|
||||
|
||||
|
||||
STATE_SCHEMA = "guanghu.native-public-projection-state/v1"
|
||||
|
||||
|
||||
def load_snapshot(path: pathlib.Path, schema: str, source_commit: str) -> bytes:
|
||||
payload = path.read_bytes()
|
||||
value = json.loads(payload)
|
||||
if not isinstance(value, dict) or value.get("schema") != schema:
|
||||
raise ValueError(f"snapshot schema mismatch: {path}")
|
||||
if schema == "guanghu.public-navigation-anchor/v1":
|
||||
observed_commit = value.get("navigation_source", {}).get("source_commit")
|
||||
else:
|
||||
observed_commit = value.get("commit")
|
||||
if value.get("full_forgejo_equivalence") is not False:
|
||||
raise ValueError("code snapshot cannot claim full Forgejo equivalence")
|
||||
if observed_commit != source_commit:
|
||||
raise ValueError(f"snapshot source commit mismatch: {path}")
|
||||
return payload
|
||||
|
||||
|
||||
class Projection:
|
||||
def __init__(self, args: argparse.Namespace) -> None:
|
||||
self.state_file = pathlib.Path(args.state_file)
|
||||
self.expected = {
|
||||
"schema": STATE_SCHEMA,
|
||||
"node_id": args.expected_node,
|
||||
"relay_node": args.expected_relay,
|
||||
"candidate_sha256": args.expected_candidate_sha256,
|
||||
"source_commit": args.expected_source_commit,
|
||||
"transport_authentication": "SSH_FORCED_COMMAND",
|
||||
}
|
||||
self.max_age_seconds = args.max_age_seconds
|
||||
self.anchor = load_snapshot(
|
||||
pathlib.Path(args.anchor_file),
|
||||
"guanghu.public-navigation-anchor/v1",
|
||||
args.expected_source_commit,
|
||||
)
|
||||
self.code = load_snapshot(
|
||||
pathlib.Path(args.code_file),
|
||||
"guanghu.native-code-channel-read-only/v1",
|
||||
args.expected_source_commit,
|
||||
)
|
||||
|
||||
def state(self) -> tuple[bool, str, dict[str, Any] | None]:
|
||||
try:
|
||||
if self.state_file.is_symlink():
|
||||
return False, "state_file_symlink", None
|
||||
value = json.loads(self.state_file.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
return False, "state_missing", None
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False, "state_invalid", None
|
||||
if not isinstance(value, dict):
|
||||
return False, "state_invalid", None
|
||||
if any(value.get(key) != expected for key, expected in self.expected.items()):
|
||||
return False, "state_binding_mismatch", value
|
||||
if value.get("status") != "READY_NATIVE_RESIDENT":
|
||||
return False, "native_not_ready", value
|
||||
accepted_at = value.get("accepted_at_epoch")
|
||||
if not isinstance(accepted_at, (int, float)):
|
||||
return False, "state_time_invalid", value
|
||||
age = time.time() - float(accepted_at)
|
||||
if age < -5:
|
||||
return False, "state_time_in_future", value
|
||||
if age > self.max_age_seconds:
|
||||
return False, "projection_expired", value
|
||||
return True, "ready", value
|
||||
|
||||
|
||||
def handler_for(projection: Projection) -> type[BaseHTTPRequestHandler]:
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "GuanghuNativeProjection/1"
|
||||
|
||||
def log_message(self, _format: str, *_args: object) -> None:
|
||||
return
|
||||
|
||||
def send_bytes(self, status: int, payload: bytes, content_type: str) -> None:
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("X-Content-Type-Options", "nosniff")
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(payload)
|
||||
|
||||
def send_json(self, status: int, value: dict[str, object]) -> None:
|
||||
payload = (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
||||
self.send_bytes(status, payload, "application/json; charset=utf-8")
|
||||
|
||||
def do_HEAD(self) -> None: # noqa: N802
|
||||
self.do_GET()
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
fresh, reason, state = projection.state()
|
||||
if self.path == "/healthz":
|
||||
self.send_json(
|
||||
HTTPStatus.OK,
|
||||
{
|
||||
"schema": "guanghu.native-public-projection-health/v1",
|
||||
"native_projection_fresh": fresh,
|
||||
"reason": reason,
|
||||
"node_id": projection.expected["node_id"],
|
||||
},
|
||||
)
|
||||
return
|
||||
if self.path not in {
|
||||
"/api/ai/v1/anchor",
|
||||
"/api/ai/v1/code-channel",
|
||||
}:
|
||||
self.send_json(HTTPStatus.NOT_FOUND, {"error": "not_found"})
|
||||
return
|
||||
if not fresh:
|
||||
self.send_json(
|
||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
{
|
||||
"schema": "guanghu.native-public-projection-unavailable/v1",
|
||||
"native_projection_fresh": False,
|
||||
"reason": reason,
|
||||
},
|
||||
)
|
||||
return
|
||||
assert state is not None
|
||||
payload = (
|
||||
projection.anchor
|
||||
if self.path == "/api/ai/v1/anchor"
|
||||
else projection.code
|
||||
)
|
||||
self.send_bytes(HTTPStatus.OK, payload, "application/json; charset=utf-8")
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
self.send_json(HTTPStatus.METHOD_NOT_ALLOWED, {"error": "method_not_allowed"})
|
||||
|
||||
return Handler
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--listen-host", default="127.0.0.1")
|
||||
parser.add_argument("--listen-port", required=True, type=int)
|
||||
parser.add_argument("--state-file", required=True)
|
||||
parser.add_argument("--anchor-file", required=True)
|
||||
parser.add_argument("--code-file", required=True)
|
||||
parser.add_argument("--expected-node", required=True)
|
||||
parser.add_argument("--expected-relay", required=True)
|
||||
parser.add_argument("--expected-candidate-sha256", required=True)
|
||||
parser.add_argument("--expected-source-commit", required=True)
|
||||
parser.add_argument("--max-age-seconds", type=float, default=15.0)
|
||||
args = parser.parse_args()
|
||||
if args.listen_host not in {"127.0.0.1", "::1"}:
|
||||
raise SystemExit("projection gateway must remain loopback-only")
|
||||
if not 1 <= args.listen_port <= 65535:
|
||||
raise SystemExit("invalid listen port")
|
||||
if args.max_age_seconds <= 0:
|
||||
raise SystemExit("max age must be greater than zero")
|
||||
projection = Projection(args)
|
||||
server = ThreadingHTTPServer((args.listen_host, args.listen_port), handler_for(projection))
|
||||
server.serve_forever()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Project a fresh protected native-relay receipt through one pinned SSH route."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def parse_receipt(path: pathlib.Path) -> tuple[dict[str, str], float]:
|
||||
if path.is_symlink():
|
||||
raise ValueError("relay receipt cannot be a symlink")
|
||||
stat = path.stat()
|
||||
values: dict[str, str] = {}
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if not line or line[:1].isspace() or ": " not in line:
|
||||
continue
|
||||
key, value = line.split(": ", 1)
|
||||
if key in values:
|
||||
raise ValueError(f"duplicate relay receipt key: {key}")
|
||||
values[key] = value
|
||||
return values, time.time() - stat.st_mtime
|
||||
|
||||
|
||||
def projection_action(
|
||||
values: dict[str, str], age: float, max_age: float, expected_source: str
|
||||
) -> str:
|
||||
ready = (
|
||||
age >= -5
|
||||
and age <= max_age
|
||||
and values.get("schema")
|
||||
== "guanghu.physical-native-final-resident-relay/v1"
|
||||
and values.get("status") == "READY_NATIVE_RESIDENT"
|
||||
and values.get("recovery_capability_sent") == "false"
|
||||
and values.get("acknowledged_sequences") == "1,2,3,4,5,6"
|
||||
and values.get("observed_nat_sources") == expected_source
|
||||
)
|
||||
return "ready" if ready else "dormant"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--relay-receipt", required=True)
|
||||
parser.add_argument("--max-receipt-age-seconds", type=float, default=15.0)
|
||||
parser.add_argument("--expected-nat-source", required=True)
|
||||
parser.add_argument("--node", required=True)
|
||||
parser.add_argument("--relay", required=True)
|
||||
parser.add_argument("--candidate-sha256", required=True)
|
||||
parser.add_argument("--source-commit", required=True)
|
||||
parser.add_argument("--ssh-bin", default="/usr/bin/ssh")
|
||||
parser.add_argument("--ssh-config", required=True)
|
||||
parser.add_argument("--ssh-target", required=True)
|
||||
args = parser.parse_args()
|
||||
if args.max_receipt_age_seconds <= 0:
|
||||
raise SystemExit("max receipt age must be greater than zero")
|
||||
ssh_bin = pathlib.Path(args.ssh_bin)
|
||||
ssh_config = pathlib.Path(args.ssh_config)
|
||||
if not ssh_bin.is_absolute() or not os.access(ssh_bin, os.X_OK):
|
||||
raise SystemExit("SSH binary must be an executable absolute path")
|
||||
if not ssh_config.is_absolute() or not ssh_config.is_file():
|
||||
raise SystemExit("SSH config must be an existing absolute file")
|
||||
try:
|
||||
values, age = parse_receipt(pathlib.Path(args.relay_receipt))
|
||||
action = projection_action(
|
||||
values,
|
||||
age,
|
||||
args.max_receipt_age_seconds,
|
||||
args.expected_nat_source,
|
||||
)
|
||||
except (OSError, UnicodeError, ValueError):
|
||||
action = "dormant"
|
||||
command = [
|
||||
str(ssh_bin),
|
||||
"-F",
|
||||
str(ssh_config),
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=5",
|
||||
args.ssh_target,
|
||||
action,
|
||||
args.node,
|
||||
args.candidate_sha256,
|
||||
args.source_commit,
|
||||
args.relay,
|
||||
]
|
||||
completed = subprocess.run(command, check=False, timeout=10)
|
||||
if completed.returncode != 0:
|
||||
raise SystemExit("native projection transport failed closed")
|
||||
print(f"NATIVE_PUBLIC_PROJECTION_RELAY_OK action={action}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -21,6 +21,7 @@ grep -Fxq ' native_target: GOSK_CODE_CHANNEL_QUALITY_EXECUTOR' "${protocol}"
|
|||
grep -Fq -- '--fail-under-lines 100' "${runner}"
|
||||
grep -Fq -- '--fail-under-functions 100' "${runner}"
|
||||
grep -Fq -- '--test broadcast_library' "${runner}"
|
||||
grep -Fq 'test-native-public-projection.sh' "${runner}"
|
||||
grep -Fq 'GHNQG_PASS_100' "${runner}"
|
||||
grep -Fq 'GHNQG_FAIL_0' "${runner}"
|
||||
grep -Fq 'total_score: ${total_score}' "${runner}"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
test_root=$(mktemp -d)
|
||||
gateway_pid=
|
||||
cleanup() {
|
||||
if [[ -n ${gateway_pid} ]]; then
|
||||
kill "${gateway_pid}" 2>/dev/null || true
|
||||
wait "${gateway_pid}" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "${test_root}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
port=$((35000 + ${BASHPID:-$$} % 1000))
|
||||
state_file=${test_root}/state.json
|
||||
anchor_file=${test_root}/anchor.json
|
||||
code_file=${test_root}/code.json
|
||||
relay_receipt=${test_root}/relay.hldp
|
||||
fake_ssh=${test_root}/ssh
|
||||
ssh_args=${test_root}/ssh-args.txt
|
||||
ssh_config=${test_root}/ssh-config
|
||||
candidate_sha=9807fa751616d4b421c103d60eecface1c5eb8013cbd89417ef9e027da281fb3
|
||||
source_commit=fb1096c5ecdcfb36015d51273f487c2f36d9c9cd
|
||||
|
||||
cat >"${anchor_file}" <<EOF
|
||||
{"schema":"guanghu.public-navigation-anchor/v1","anchor_id":"GLW-PUBLIC-NAV-ANCHOR-001","navigation_source":{"source_commit":"${source_commit}"}}
|
||||
EOF
|
||||
cat >"${code_file}" <<EOF
|
||||
{"schema":"guanghu.native-code-channel-read-only/v1","repository_id":"REPO-012","branch":"main","commit":"${source_commit}","mode":"READ_ONLY_DISCOVERY","runtime":"GUANGHU_OS_NATIVE","full_forgejo_equivalence":false}
|
||||
EOF
|
||||
|
||||
python3 "${source_root}/scripts/native-public-projection-gateway.py" \
|
||||
--listen-host 127.0.0.1 \
|
||||
--listen-port "${port}" \
|
||||
--state-file "${state_file}" \
|
||||
--anchor-file "${anchor_file}" \
|
||||
--code-file "${code_file}" \
|
||||
--expected-node JD-FD-PRIMARY \
|
||||
--expected-relay BS-SG-003 \
|
||||
--expected-candidate-sha256 "${candidate_sha}" \
|
||||
--expected-source-commit "${source_commit}" \
|
||||
--max-age-seconds 1 >"${test_root}/gateway.log" 2>&1 &
|
||||
gateway_pid=$!
|
||||
|
||||
for _ in $(seq 1 50); do
|
||||
curl -fsS "http://127.0.0.1:${port}/healthz" >/dev/null 2>&1 && break
|
||||
sleep 0.05
|
||||
done
|
||||
|
||||
[[ $(curl -sS -o "${test_root}/stale.json" -w '%{http_code}' \
|
||||
"http://127.0.0.1:${port}/api/ai/v1/anchor") == 503 ]]
|
||||
grep -q '"native_projection_fresh":false' "${test_root}/stale.json"
|
||||
|
||||
if SSH_ORIGINAL_COMMAND="ready JD-FD-PRIMARY ${candidate_sha} ${source_commit} BS-SG-003 extra" \
|
||||
python3 "${source_root}/scripts/accept-native-public-projection.py" \
|
||||
--state-file "${state_file}" \
|
||||
--expected-node JD-FD-PRIMARY \
|
||||
--expected-relay BS-SG-003 \
|
||||
--expected-candidate-sha256 "${candidate_sha}" \
|
||||
--expected-source-commit "${source_commit}"; then
|
||||
echo "projection acceptor allowed an overlong command" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SSH_ORIGINAL_COMMAND="ready JD-FD-PRIMARY ${candidate_sha} ${source_commit} BS-SG-003" \
|
||||
python3 "${source_root}/scripts/accept-native-public-projection.py" \
|
||||
--state-file "${state_file}" \
|
||||
--expected-node JD-FD-PRIMARY \
|
||||
--expected-relay BS-SG-003 \
|
||||
--expected-candidate-sha256 "${candidate_sha}" \
|
||||
--expected-source-commit "${source_commit}"
|
||||
|
||||
curl -fsS "http://127.0.0.1:${port}/api/ai/v1/anchor" >"${test_root}/anchor-readback.json"
|
||||
curl -fsS "http://127.0.0.1:${port}/api/ai/v1/code-channel" >"${test_root}/code-readback.json"
|
||||
cmp "${anchor_file}" "${test_root}/anchor-readback.json"
|
||||
cmp "${code_file}" "${test_root}/code-readback.json"
|
||||
[[ $(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
||||
"http://127.0.0.1:${port}/api/ai/v1/anchor") == 405 ]]
|
||||
|
||||
sleep 2
|
||||
[[ $(curl -sS -o "${test_root}/expired.json" -w '%{http_code}' \
|
||||
"http://127.0.0.1:${port}/api/ai/v1/anchor") == 503 ]]
|
||||
grep -q '"reason":"projection_expired"' "${test_root}/expired.json"
|
||||
|
||||
SSH_ORIGINAL_COMMAND="dormant JD-FD-PRIMARY ${candidate_sha} ${source_commit} BS-SG-003" \
|
||||
python3 "${source_root}/scripts/accept-native-public-projection.py" \
|
||||
--state-file "${state_file}" \
|
||||
--expected-node JD-FD-PRIMARY \
|
||||
--expected-relay BS-SG-003 \
|
||||
--expected-candidate-sha256 "${candidate_sha}" \
|
||||
--expected-source-commit "${source_commit}"
|
||||
[[ $(curl -sS -o "${test_root}/dormant.json" -w '%{http_code}' \
|
||||
"http://127.0.0.1:${port}/api/ai/v1/code-channel") == 503 ]]
|
||||
grep -q '"reason":"native_not_ready"' "${test_root}/dormant.json"
|
||||
|
||||
cat >"${fake_ssh}" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >"${GHOS_TEST_SSH_ARGS}"
|
||||
EOF
|
||||
chmod 0700 "${fake_ssh}"
|
||||
: >"${ssh_config}"
|
||||
cat >"${relay_receipt}" <<'EOF'
|
||||
schema: guanghu.physical-native-final-resident-relay/v1
|
||||
status: READY_NATIVE_RESIDENT
|
||||
recovery_capability_sent: false
|
||||
acknowledged_sequences: 1,2,3,4,5,6
|
||||
observed_nat_sources: 111.228.0.139
|
||||
EOF
|
||||
GHOS_TEST_SSH_ARGS=${ssh_args} python3 \
|
||||
"${source_root}/scripts/native-public-projection-relay.py" \
|
||||
--relay-receipt "${relay_receipt}" \
|
||||
--max-receipt-age-seconds 5 \
|
||||
--expected-nat-source 111.228.0.139 \
|
||||
--node JD-FD-PRIMARY \
|
||||
--relay BS-SG-003 \
|
||||
--candidate-sha256 "${candidate_sha}" \
|
||||
--source-commit "${source_commit}" \
|
||||
--ssh-bin "${fake_ssh}" \
|
||||
--ssh-config "${ssh_config}" \
|
||||
--ssh-target native-projection-gz
|
||||
grep -q "native-projection-gz ready JD-FD-PRIMARY ${candidate_sha} ${source_commit} BS-SG-003$" \
|
||||
"${ssh_args}"
|
||||
|
||||
sed -i.bak 's/recovery_capability_sent: false/recovery_capability_sent: true/' \
|
||||
"${relay_receipt}"
|
||||
GHOS_TEST_SSH_ARGS=${ssh_args} python3 \
|
||||
"${source_root}/scripts/native-public-projection-relay.py" \
|
||||
--relay-receipt "${relay_receipt}" \
|
||||
--max-receipt-age-seconds 5 \
|
||||
--expected-nat-source 111.228.0.139 \
|
||||
--node JD-FD-PRIMARY \
|
||||
--relay BS-SG-003 \
|
||||
--candidate-sha256 "${candidate_sha}" \
|
||||
--source-commit "${source_commit}" \
|
||||
--ssh-bin "${fake_ssh}" \
|
||||
--ssh-config "${ssh_config}" \
|
||||
--ssh-target native-projection-gz
|
||||
grep -q "native-projection-gz dormant JD-FD-PRIMARY ${candidate_sha} ${source_commit} BS-SG-003$" \
|
||||
"${ssh_args}"
|
||||
|
||||
echo "NATIVE_PUBLIC_PROJECTION_TEST_PASS_100"
|
||||
Loading…
Reference in a new issue