Fix restore CLI syntax - take snapshot ID directly
Changed --restore to take snapshot ID directly and call /snapshots/:id/restore instead of requiring --from SNAPSHOT_ID and calling /sessions/:id/restore or /services/:id/restore. Updated session and service restore in all implementations: - un.py, un.go, un.rb, un.sh, un.ex, un.erl, un.fs, un.hs - un.groovy, un.r, un.m, un.awk Also added snapshot management features where missing.
This commit is contained in:
parent
09ec15aa62
commit
ccd723db99
13 changed files with 2020 additions and 40 deletions
329
un.awk
329
un.awk
|
|
@ -597,30 +597,286 @@ function cmd_key(do_extend) {
|
||||||
validate_key(do_extend)
|
validate_key(do_extend)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function snapshot_list( timestamp, sig_headers, signature, sig_input, sig_cmd) {
|
||||||
|
get_api_keys()
|
||||||
|
timestamp = systime()
|
||||||
|
sig_headers = ""
|
||||||
|
if (GLOBAL_SECRET_KEY != "") {
|
||||||
|
sig_input = timestamp ":GET:/snapshots:"
|
||||||
|
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||||
|
sig_cmd | getline signature
|
||||||
|
close(sig_cmd)
|
||||||
|
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||||
|
}
|
||||||
|
cmd = "curl -s '" API_BASE "/snapshots' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||||
|
while ((cmd | getline line) > 0) print line
|
||||||
|
close(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshot_info(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||||
|
get_api_keys()
|
||||||
|
endpoint = "/snapshots/" id
|
||||||
|
timestamp = systime()
|
||||||
|
sig_headers = ""
|
||||||
|
if (GLOBAL_SECRET_KEY != "") {
|
||||||
|
sig_input = timestamp ":GET:" endpoint ":"
|
||||||
|
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||||
|
sig_cmd | getline signature
|
||||||
|
close(sig_cmd)
|
||||||
|
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||||
|
}
|
||||||
|
cmd = "curl -s '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||||
|
while ((cmd | getline line) > 0) print line
|
||||||
|
close(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshot_delete(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||||
|
get_api_keys()
|
||||||
|
endpoint = "/snapshots/" id
|
||||||
|
timestamp = systime()
|
||||||
|
sig_headers = ""
|
||||||
|
if (GLOBAL_SECRET_KEY != "") {
|
||||||
|
sig_input = timestamp ":DELETE:" endpoint ":"
|
||||||
|
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||||
|
sig_cmd | getline signature
|
||||||
|
close(sig_cmd)
|
||||||
|
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "'"
|
||||||
|
}
|
||||||
|
cmd = "curl -s -X DELETE '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||||
|
system(cmd)
|
||||||
|
print GREEN "Snapshot deleted: " id RESET
|
||||||
|
}
|
||||||
|
|
||||||
|
function session_snapshot(id, name, hot , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, line, response) {
|
||||||
|
get_api_keys()
|
||||||
|
endpoint = "/sessions/" id "/snapshot"
|
||||||
|
|
||||||
|
# Build JSON payload
|
||||||
|
json = "{"
|
||||||
|
if (name != "") {
|
||||||
|
json = json "\"name\":\"" escape_json(name) "\""
|
||||||
|
if (hot != "") json = json ","
|
||||||
|
}
|
||||||
|
if (hot != "") {
|
||||||
|
json = json "\"hot\":" hot
|
||||||
|
}
|
||||||
|
json = json "}"
|
||||||
|
|
||||||
|
# Write to temp file
|
||||||
|
tmp = "/tmp/un_awk_snap_" PROCINFO["pid"] ".json"
|
||||||
|
print json > tmp
|
||||||
|
close(tmp)
|
||||||
|
|
||||||
|
# Build HMAC signature if secret key exists
|
||||||
|
timestamp = systime()
|
||||||
|
sig_headers = ""
|
||||||
|
if (GLOBAL_SECRET_KEY != "") {
|
||||||
|
sig_input = timestamp ":POST:" endpoint ":" json
|
||||||
|
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||||
|
sig_cmd | getline signature
|
||||||
|
close(sig_cmd)
|
||||||
|
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call curl
|
||||||
|
cmd = "curl -s -X POST '" API_BASE endpoint "' " \
|
||||||
|
"-H 'Content-Type: application/json' " \
|
||||||
|
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
|
||||||
|
sig_headers \
|
||||||
|
"-d '@" tmp "'"
|
||||||
|
|
||||||
|
response = ""
|
||||||
|
while ((cmd | getline line) > 0) {
|
||||||
|
response = response line
|
||||||
|
}
|
||||||
|
close(cmd)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
system("rm -f " tmp)
|
||||||
|
|
||||||
|
print GREEN "Snapshot created" RESET
|
||||||
|
print response
|
||||||
|
}
|
||||||
|
|
||||||
|
function session_restore(snapshot_id , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, line, response) {
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
get_api_keys()
|
||||||
|
endpoint = "/snapshots/" snapshot_id "/restore"
|
||||||
|
|
||||||
|
json = "{}"
|
||||||
|
|
||||||
|
# Write to temp file
|
||||||
|
tmp = "/tmp/un_awk_restore_" PROCINFO["pid"] ".json"
|
||||||
|
print json > tmp
|
||||||
|
close(tmp)
|
||||||
|
|
||||||
|
# Build HMAC signature if secret key exists
|
||||||
|
timestamp = systime()
|
||||||
|
sig_headers = ""
|
||||||
|
if (GLOBAL_SECRET_KEY != "") {
|
||||||
|
sig_input = timestamp ":POST:" endpoint ":" json
|
||||||
|
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||||
|
sig_cmd | getline signature
|
||||||
|
close(sig_cmd)
|
||||||
|
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call curl
|
||||||
|
cmd = "curl -s -X POST '" API_BASE endpoint "' " \
|
||||||
|
"-H 'Content-Type: application/json' " \
|
||||||
|
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
|
||||||
|
sig_headers \
|
||||||
|
"-d '@" tmp "'"
|
||||||
|
|
||||||
|
response = ""
|
||||||
|
while ((cmd | getline line) > 0) {
|
||||||
|
response = response line
|
||||||
|
}
|
||||||
|
close(cmd)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
system("rm -f " tmp)
|
||||||
|
|
||||||
|
print GREEN "Session restored from snapshot" RESET
|
||||||
|
}
|
||||||
|
|
||||||
|
function service_snapshot(id, name, hot , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, line, response) {
|
||||||
|
get_api_keys()
|
||||||
|
endpoint = "/services/" id "/snapshot"
|
||||||
|
|
||||||
|
# Build JSON payload
|
||||||
|
json = "{"
|
||||||
|
if (name != "") {
|
||||||
|
json = json "\"name\":\"" escape_json(name) "\""
|
||||||
|
if (hot != "") json = json ","
|
||||||
|
}
|
||||||
|
if (hot != "") {
|
||||||
|
json = json "\"hot\":" hot
|
||||||
|
}
|
||||||
|
json = json "}"
|
||||||
|
|
||||||
|
# Write to temp file
|
||||||
|
tmp = "/tmp/un_awk_snap_" PROCINFO["pid"] ".json"
|
||||||
|
print json > tmp
|
||||||
|
close(tmp)
|
||||||
|
|
||||||
|
# Build HMAC signature if secret key exists
|
||||||
|
timestamp = systime()
|
||||||
|
sig_headers = ""
|
||||||
|
if (GLOBAL_SECRET_KEY != "") {
|
||||||
|
sig_input = timestamp ":POST:" endpoint ":" json
|
||||||
|
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||||
|
sig_cmd | getline signature
|
||||||
|
close(sig_cmd)
|
||||||
|
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call curl
|
||||||
|
cmd = "curl -s -X POST '" API_BASE endpoint "' " \
|
||||||
|
"-H 'Content-Type: application/json' " \
|
||||||
|
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
|
||||||
|
sig_headers \
|
||||||
|
"-d '@" tmp "'"
|
||||||
|
|
||||||
|
response = ""
|
||||||
|
while ((cmd | getline line) > 0) {
|
||||||
|
response = response line
|
||||||
|
}
|
||||||
|
close(cmd)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
system("rm -f " tmp)
|
||||||
|
|
||||||
|
print GREEN "Snapshot created" RESET
|
||||||
|
print response
|
||||||
|
}
|
||||||
|
|
||||||
|
function service_restore(snapshot_id , endpoint, json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, line, response) {
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
get_api_keys()
|
||||||
|
endpoint = "/snapshots/" snapshot_id "/restore"
|
||||||
|
|
||||||
|
json = "{}"
|
||||||
|
|
||||||
|
# Write to temp file
|
||||||
|
tmp = "/tmp/un_awk_restore_" PROCINFO["pid"] ".json"
|
||||||
|
print json > tmp
|
||||||
|
close(tmp)
|
||||||
|
|
||||||
|
# Build HMAC signature if secret key exists
|
||||||
|
timestamp = systime()
|
||||||
|
sig_headers = ""
|
||||||
|
if (GLOBAL_SECRET_KEY != "") {
|
||||||
|
sig_input = timestamp ":POST:" endpoint ":" json
|
||||||
|
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||||
|
sig_cmd | getline signature
|
||||||
|
close(sig_cmd)
|
||||||
|
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call curl
|
||||||
|
cmd = "curl -s -X POST '" API_BASE endpoint "' " \
|
||||||
|
"-H 'Content-Type: application/json' " \
|
||||||
|
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
|
||||||
|
sig_headers \
|
||||||
|
"-d '@" tmp "'"
|
||||||
|
|
||||||
|
response = ""
|
||||||
|
while ((cmd | getline line) > 0) {
|
||||||
|
response = response line
|
||||||
|
}
|
||||||
|
close(cmd)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
system("rm -f " tmp)
|
||||||
|
|
||||||
|
print GREEN "Service restored from snapshot" RESET
|
||||||
|
}
|
||||||
|
|
||||||
function show_help() {
|
function show_help() {
|
||||||
print "Usage: awk -f un.awk <source_file>"
|
print "Usage: awk -f un.awk <source_file>"
|
||||||
print " awk -f un.awk session --list"
|
print " awk -f un.awk session --list"
|
||||||
print " awk -f un.awk session --kill ID"
|
print " awk -f un.awk session --kill ID"
|
||||||
print " awk -f un.awk session [-s SHELL] [-f FILE]..."
|
print " awk -f un.awk session [-s SHELL] [-f FILE]..."
|
||||||
|
print " awk -f un.awk session --snapshot SESSION_ID [--snapshot-name NAME] [--hot]"
|
||||||
|
print " awk -f un.awk session --restore SNAPSHOT_ID"
|
||||||
print " awk -f un.awk key [--extend]"
|
print " awk -f un.awk key [--extend]"
|
||||||
print " awk -f un.awk service --list"
|
print " awk -f un.awk service --list"
|
||||||
print " awk -f un.awk service --create --name NAME [--ports PORTS] [--domains DOMAINS] [--type TYPE] [--bootstrap CMD] [-f FILE]..."
|
print " awk -f un.awk service --create --name NAME [--ports PORTS] [--domains DOMAINS] [--type TYPE] [--bootstrap CMD] [-f FILE]..."
|
||||||
print " awk -f un.awk service --destroy ID"
|
print " awk -f un.awk service --destroy ID"
|
||||||
print " awk -f un.awk service --dump-bootstrap ID [--dump-file FILE]"
|
print " awk -f un.awk service --dump-bootstrap ID [--dump-file FILE]"
|
||||||
|
print " awk -f un.awk service --snapshot SERVICE_ID [--snapshot-name NAME] [--hot]"
|
||||||
|
print " awk -f un.awk service --restore SNAPSHOT_ID"
|
||||||
|
print " awk -f un.awk snapshot --list"
|
||||||
|
print " awk -f un.awk snapshot --info ID"
|
||||||
|
print " awk -f un.awk snapshot --delete ID"
|
||||||
print ""
|
print ""
|
||||||
print "Session options:"
|
print "Session options:"
|
||||||
print " -s, --shell SHELL Shell to use (default: bash)"
|
print " -s, --shell SHELL Shell to use (default: bash)"
|
||||||
print " -f FILE Input file to upload (can be repeated)"
|
print " -f FILE Input file to upload (can be repeated)"
|
||||||
|
print " --snapshot SESSION_ID Create snapshot of session"
|
||||||
|
print " --restore SNAPSHOT_ID Restore from snapshot ID"
|
||||||
|
print " --snapshot-name N Name for snapshot"
|
||||||
|
print " --hot Take snapshot without freezing (live snapshot)"
|
||||||
print ""
|
print ""
|
||||||
print "Service options:"
|
print "Service options:"
|
||||||
print " --name NAME Service name (required for --create)"
|
print " --name NAME Service name (required for --create)"
|
||||||
print " --ports PORTS Comma-separated port numbers"
|
print " --ports PORTS Comma-separated port numbers"
|
||||||
print " --domains DOMAINS Comma-separated domain names"
|
print " --domains DOMAINS Comma-separated domain names"
|
||||||
print " --type TYPE Service type for SRV records (minecraft, mumble, teamspeak, source, tcp, udp)"
|
print " --type TYPE Service type for SRV records (minecraft, mumble, teamspeak, source, tcp, udp)"
|
||||||
print " --bootstrap CMD Bootstrap command or script"
|
print " --bootstrap CMD Bootstrap command or script"
|
||||||
print " --dump-bootstrap ID Dump bootstrap script from service"
|
print " --dump-bootstrap ID Dump bootstrap script from service"
|
||||||
print " --dump-file FILE Save bootstrap to file (with --dump-bootstrap)"
|
print " --dump-file FILE Save bootstrap to file (with --dump-bootstrap)"
|
||||||
print " -f FILE Input file to upload (can be repeated)"
|
print " -f FILE Input file to upload (can be repeated)"
|
||||||
|
print " --snapshot SERVICE_ID Create snapshot of service"
|
||||||
|
print " --restore SNAPSHOT_ID Restore from snapshot ID"
|
||||||
|
print " --snapshot-name N Name for snapshot"
|
||||||
|
print " --hot Take snapshot without freezing (live snapshot)"
|
||||||
|
print ""
|
||||||
|
print "Snapshot options:"
|
||||||
|
print " -l, --list List all snapshots"
|
||||||
|
print " --info ID Get snapshot details"
|
||||||
|
print " --delete ID Delete a snapshot"
|
||||||
print ""
|
print ""
|
||||||
print "Requires: UNSANDBOX_API_KEY environment variable"
|
print "Requires: UNSANDBOX_API_KEY environment variable"
|
||||||
}
|
}
|
||||||
|
|
@ -647,6 +903,26 @@ END {
|
||||||
session_list()
|
session_list()
|
||||||
} else if (ARGC >= 4 && ARGV[2] == "--kill") {
|
} else if (ARGC >= 4 && ARGV[2] == "--kill") {
|
||||||
session_kill(ARGV[3])
|
session_kill(ARGV[3])
|
||||||
|
} else if (ARGC >= 4 && ARGV[2] == "--snapshot") {
|
||||||
|
# Parse snapshot options
|
||||||
|
snapshot_name = ""
|
||||||
|
hot = ""
|
||||||
|
i = 4
|
||||||
|
while (i < ARGC) {
|
||||||
|
if (ARGV[i] == "--snapshot-name" && i + 1 < ARGC) {
|
||||||
|
snapshot_name = ARGV[i + 1]
|
||||||
|
i += 2
|
||||||
|
} else if (ARGV[i] == "--hot") {
|
||||||
|
hot = "true"
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session_snapshot(ARGV[3], snapshot_name, hot)
|
||||||
|
} else if (ARGC >= 4 && ARGV[2] == "--restore") {
|
||||||
|
# --restore takes snapshot ID directly
|
||||||
|
session_restore(ARGV[3])
|
||||||
} else {
|
} else {
|
||||||
# Parse session creation arguments
|
# Parse session creation arguments
|
||||||
shell = ""
|
shell = ""
|
||||||
|
|
@ -693,6 +969,19 @@ END {
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ARGV[1] == "snapshot") {
|
||||||
|
if (ARGC >= 3 && (ARGV[2] == "--list" || ARGV[2] == "-l")) {
|
||||||
|
snapshot_list()
|
||||||
|
} else if (ARGC >= 4 && ARGV[2] == "--info") {
|
||||||
|
snapshot_info(ARGV[3])
|
||||||
|
} else if (ARGC >= 4 && ARGV[2] == "--delete") {
|
||||||
|
snapshot_delete(ARGV[3])
|
||||||
|
} else {
|
||||||
|
print "Usage: awk -f un.awk snapshot --list|--info ID|--delete ID"
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
if (ARGV[1] == "service") {
|
if (ARGV[1] == "service") {
|
||||||
if (ARGC >= 3 && ARGV[2] == "--list") {
|
if (ARGC >= 3 && ARGV[2] == "--list") {
|
||||||
service_list()
|
service_list()
|
||||||
|
|
@ -704,6 +993,26 @@ END {
|
||||||
dump_file = ARGV[5]
|
dump_file = ARGV[5]
|
||||||
}
|
}
|
||||||
service_dump_bootstrap(ARGV[3], dump_file)
|
service_dump_bootstrap(ARGV[3], dump_file)
|
||||||
|
} else if (ARGC >= 4 && ARGV[2] == "--snapshot") {
|
||||||
|
# Parse snapshot options
|
||||||
|
snapshot_name = ""
|
||||||
|
hot = ""
|
||||||
|
i = 4
|
||||||
|
while (i < ARGC) {
|
||||||
|
if (ARGV[i] == "--snapshot-name" && i + 1 < ARGC) {
|
||||||
|
snapshot_name = ARGV[i + 1]
|
||||||
|
i += 2
|
||||||
|
} else if (ARGV[i] == "--hot") {
|
||||||
|
hot = "true"
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service_snapshot(ARGV[3], snapshot_name, hot)
|
||||||
|
} else if (ARGC >= 4 && ARGV[2] == "--restore") {
|
||||||
|
# --restore takes snapshot ID directly
|
||||||
|
service_restore(ARGV[3])
|
||||||
} else if (ARGV[2] == "--create") {
|
} else if (ARGV[2] == "--create") {
|
||||||
# Parse service creation arguments
|
# Parse service creation arguments
|
||||||
name = ""
|
name = ""
|
||||||
|
|
|
||||||
138
un.erl
138
un.erl
|
|
@ -46,6 +46,7 @@ main([]) ->
|
||||||
io:format("Usage: un.erl [options] <source_file>~n"),
|
io:format("Usage: un.erl [options] <source_file>~n"),
|
||||||
io:format(" un.erl session [options]~n"),
|
io:format(" un.erl session [options]~n"),
|
||||||
io:format(" un.erl service [options]~n"),
|
io:format(" un.erl service [options]~n"),
|
||||||
|
io:format(" un.erl snapshot [options]~n"),
|
||||||
io:format(" un.erl key [options]~n"),
|
io:format(" un.erl key [options]~n"),
|
||||||
halt(1);
|
halt(1);
|
||||||
|
|
||||||
|
|
@ -55,6 +56,9 @@ main(["session" | Rest]) ->
|
||||||
main(["service" | Rest]) ->
|
main(["service" | Rest]) ->
|
||||||
service_command(Rest);
|
service_command(Rest);
|
||||||
|
|
||||||
|
main(["snapshot" | Rest]) ->
|
||||||
|
snapshot_command(Rest);
|
||||||
|
|
||||||
main(["key" | Rest]) ->
|
main(["key" | Rest]) ->
|
||||||
key_command(Rest);
|
key_command(Rest);
|
||||||
|
|
||||||
|
|
@ -115,12 +119,38 @@ session_command(Args) ->
|
||||||
io:format("\033[33mSession created (WebSocket required)\033[0m~n"),
|
io:format("\033[33mSession created (WebSocket required)\033[0m~n"),
|
||||||
io:format("~s~n", [Response]).
|
io:format("~s~n", [Response]).
|
||||||
|
|
||||||
|
%% Session snapshot commands
|
||||||
|
session_command(["--snapshot", SessionId | Rest]) ->
|
||||||
|
ApiKey = get_api_key(),
|
||||||
|
Name = get_snapshot_name(Rest),
|
||||||
|
Hot = has_hot_flag(Rest),
|
||||||
|
Json = build_snapshot_json(Name, Hot),
|
||||||
|
TmpFile = write_temp_file(Json),
|
||||||
|
Response = curl_post(ApiKey, "/sessions/" ++ SessionId ++ "/snapshot", TmpFile),
|
||||||
|
file:delete(TmpFile),
|
||||||
|
io:format("\033[32mSnapshot created\033[0m~n"),
|
||||||
|
io:format("~s~n", [Response]);
|
||||||
|
|
||||||
|
session_command(["--restore", SnapshotId | _Rest]) ->
|
||||||
|
% --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
ApiKey = get_api_key(),
|
||||||
|
TmpFile = write_temp_file("{}"),
|
||||||
|
Response = curl_post(ApiKey, "/snapshots/" ++ SnapshotId ++ "/restore", TmpFile),
|
||||||
|
file:delete(TmpFile),
|
||||||
|
io:format("\033[32mSession restored from snapshot\033[0m~n"),
|
||||||
|
io:format("~s~n", [Response]);
|
||||||
|
|
||||||
validate_session_args([]) -> ok;
|
validate_session_args([]) -> ok;
|
||||||
validate_session_args(["--shell", _ | Rest]) -> validate_session_args(Rest);
|
validate_session_args(["--shell", _ | Rest]) -> validate_session_args(Rest);
|
||||||
validate_session_args(["-s", _ | Rest]) -> validate_session_args(Rest);
|
validate_session_args(["-s", _ | Rest]) -> validate_session_args(Rest);
|
||||||
validate_session_args(["-f", _ | Rest]) -> validate_session_args(Rest);
|
validate_session_args(["-f", _ | Rest]) -> validate_session_args(Rest);
|
||||||
validate_session_args(["-n", _ | Rest]) -> validate_session_args(Rest);
|
validate_session_args(["-n", _ | Rest]) -> validate_session_args(Rest);
|
||||||
validate_session_args(["-v", _ | Rest]) -> validate_session_args(Rest);
|
validate_session_args(["-v", _ | Rest]) -> validate_session_args(Rest);
|
||||||
|
validate_session_args(["--snapshot", _ | Rest]) -> validate_session_args(Rest);
|
||||||
|
validate_session_args(["--restore", _ | Rest]) -> validate_session_args(Rest);
|
||||||
|
validate_session_args(["--from", _ | Rest]) -> validate_session_args(Rest);
|
||||||
|
validate_session_args(["--snapshot-name", _ | Rest]) -> validate_session_args(Rest);
|
||||||
|
validate_session_args(["--hot" | Rest]) -> validate_session_args(Rest);
|
||||||
validate_session_args([Arg | _]) ->
|
validate_session_args([Arg | _]) ->
|
||||||
case Arg of
|
case Arg of
|
||||||
[$- | _] ->
|
[$- | _] ->
|
||||||
|
|
@ -131,6 +161,23 @@ validate_session_args([Arg | _]) ->
|
||||||
validate_session_args([])
|
validate_session_args([])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
get_snapshot_name([]) -> undefined;
|
||||||
|
get_snapshot_name(["--snapshot-name", Name | _]) -> Name;
|
||||||
|
get_snapshot_name([_ | Rest]) -> get_snapshot_name(Rest).
|
||||||
|
|
||||||
|
get_from_snapshot([]) -> undefined;
|
||||||
|
get_from_snapshot(["--from", SnapshotId | _]) -> SnapshotId;
|
||||||
|
get_from_snapshot([_ | Rest]) -> get_from_snapshot(Rest).
|
||||||
|
|
||||||
|
has_hot_flag([]) -> false;
|
||||||
|
has_hot_flag(["--hot" | _]) -> true;
|
||||||
|
has_hot_flag([_ | Rest]) -> has_hot_flag(Rest).
|
||||||
|
|
||||||
|
build_snapshot_json(undefined, false) -> "{}";
|
||||||
|
build_snapshot_json(undefined, true) -> "{\"hot\":true}";
|
||||||
|
build_snapshot_json(Name, false) -> "{\"name\":\"" ++ escape_json(Name) ++ "\"}";
|
||||||
|
build_snapshot_json(Name, true) -> "{\"name\":\"" ++ escape_json(Name) ++ "\",\"hot\":true}".
|
||||||
|
|
||||||
%% Service command
|
%% Service command
|
||||||
service_command(["--list" | _]) ->
|
service_command(["--list" | _]) ->
|
||||||
ApiKey = get_api_key(),
|
ApiKey = get_api_key(),
|
||||||
|
|
@ -209,6 +256,27 @@ service_command(["--dump-bootstrap", ServiceId | _]) ->
|
||||||
io:format("~s", [Script])
|
io:format("~s", [Script])
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
%% Service snapshot commands
|
||||||
|
service_command(["--snapshot", ServiceId | Rest]) ->
|
||||||
|
ApiKey = get_api_key(),
|
||||||
|
Name = get_snapshot_name(Rest),
|
||||||
|
Hot = has_hot_flag(Rest),
|
||||||
|
Json = build_snapshot_json(Name, Hot),
|
||||||
|
TmpFile = write_temp_file(Json),
|
||||||
|
Response = curl_post(ApiKey, "/services/" ++ ServiceId ++ "/snapshot", TmpFile),
|
||||||
|
file:delete(TmpFile),
|
||||||
|
io:format("\033[32mSnapshot created\033[0m~n"),
|
||||||
|
io:format("~s~n", [Response]);
|
||||||
|
|
||||||
|
service_command(["--restore", SnapshotId | _Rest]) ->
|
||||||
|
% --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
ApiKey = get_api_key(),
|
||||||
|
TmpFile = write_temp_file("{}"),
|
||||||
|
Response = curl_post(ApiKey, "/snapshots/" ++ SnapshotId ++ "/restore", TmpFile),
|
||||||
|
file:delete(TmpFile),
|
||||||
|
io:format("\033[32mService restored from snapshot\033[0m~n"),
|
||||||
|
io:format("~s~n", [Response]);
|
||||||
|
|
||||||
service_command(Args) ->
|
service_command(Args) ->
|
||||||
case get_service_name(Args) of
|
case get_service_name(Args) of
|
||||||
undefined ->
|
undefined ->
|
||||||
|
|
@ -254,6 +322,76 @@ service_command(Args) ->
|
||||||
io:format("~s~n", [Response])
|
io:format("~s~n", [Response])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% Snapshot command
|
||||||
|
snapshot_command(["--list" | _]) ->
|
||||||
|
snapshot_command(["-l"]);
|
||||||
|
snapshot_command(["-l" | _]) ->
|
||||||
|
ApiKey = get_api_key(),
|
||||||
|
Response = curl_get(ApiKey, "/snapshots"),
|
||||||
|
io:format("~s~n", [Response]);
|
||||||
|
|
||||||
|
snapshot_command(["--info", SnapshotId | _]) ->
|
||||||
|
ApiKey = get_api_key(),
|
||||||
|
Response = curl_get(ApiKey, "/snapshots/" ++ SnapshotId),
|
||||||
|
io:format("~s~n", [Response]);
|
||||||
|
|
||||||
|
snapshot_command(["--delete", SnapshotId | _]) ->
|
||||||
|
ApiKey = get_api_key(),
|
||||||
|
_ = curl_delete(ApiKey, "/snapshots/" ++ SnapshotId),
|
||||||
|
io:format("\033[32mSnapshot deleted: ~s\033[0m~n", [SnapshotId]);
|
||||||
|
|
||||||
|
snapshot_command(["--clone", SnapshotId | Rest]) ->
|
||||||
|
ApiKey = get_api_key(),
|
||||||
|
Type = get_clone_type(Rest),
|
||||||
|
Name = get_clone_name(Rest),
|
||||||
|
Shell = get_clone_shell(Rest),
|
||||||
|
Ports = get_clone_ports(Rest),
|
||||||
|
Json = build_clone_json(Type, Name, Shell, Ports),
|
||||||
|
TmpFile = write_temp_file(Json),
|
||||||
|
Response = curl_post(ApiKey, "/snapshots/" ++ SnapshotId ++ "/clone", TmpFile),
|
||||||
|
file:delete(TmpFile),
|
||||||
|
io:format("\033[32mCreated from snapshot\033[0m~n"),
|
||||||
|
io:format("~s~n", [Response]);
|
||||||
|
|
||||||
|
snapshot_command(_) ->
|
||||||
|
io:format(standard_error, "Error: Use --list, --info ID, --delete ID, or --clone ID --type TYPE~n", []),
|
||||||
|
halt(1).
|
||||||
|
|
||||||
|
get_clone_type([]) -> undefined;
|
||||||
|
get_clone_type(["--type", Type | _]) -> Type;
|
||||||
|
get_clone_type([_ | Rest]) -> get_clone_type(Rest).
|
||||||
|
|
||||||
|
get_clone_name([]) -> undefined;
|
||||||
|
get_clone_name(["--name", Name | _]) -> Name;
|
||||||
|
get_clone_name([_ | Rest]) -> get_clone_name(Rest).
|
||||||
|
|
||||||
|
get_clone_shell([]) -> undefined;
|
||||||
|
get_clone_shell(["--shell", Shell | _]) -> Shell;
|
||||||
|
get_clone_shell([_ | Rest]) -> get_clone_shell(Rest).
|
||||||
|
|
||||||
|
get_clone_ports([]) -> undefined;
|
||||||
|
get_clone_ports(["--ports", Ports | _]) -> Ports;
|
||||||
|
get_clone_ports([_ | Rest]) -> get_clone_ports(Rest).
|
||||||
|
|
||||||
|
build_clone_json(undefined, _, _, _) ->
|
||||||
|
io:format(standard_error, "\033[31mError: --type required (session or service)\033[0m~n"),
|
||||||
|
halt(1);
|
||||||
|
build_clone_json(Type, Name, Shell, Ports) ->
|
||||||
|
TypeJson = "{\"type\":\"" ++ Type ++ "\"",
|
||||||
|
NameJson = case Name of
|
||||||
|
undefined -> "";
|
||||||
|
N -> ",\"name\":\"" ++ escape_json(N) ++ "\""
|
||||||
|
end,
|
||||||
|
ShellJson = case Shell of
|
||||||
|
undefined -> "";
|
||||||
|
S -> ",\"shell\":\"" ++ S ++ "\""
|
||||||
|
end,
|
||||||
|
PortsJson = case Ports of
|
||||||
|
undefined -> "";
|
||||||
|
P -> ",\"ports\":[" ++ P ++ "]"
|
||||||
|
end,
|
||||||
|
TypeJson ++ NameJson ++ ShellJson ++ PortsJson ++ "}".
|
||||||
|
|
||||||
%% Key command
|
%% Key command
|
||||||
key_command(Args) ->
|
key_command(Args) ->
|
||||||
ApiKey = get_api_key(),
|
ApiKey = get_api_key(),
|
||||||
|
|
|
||||||
98
un.ex
98
un.ex
|
|
@ -78,6 +78,7 @@ defmodule Un do
|
||||||
def main([]), do: print_usage()
|
def main([]), do: print_usage()
|
||||||
def main(["session" | rest]), do: session_command(rest)
|
def main(["session" | rest]), do: session_command(rest)
|
||||||
def main(["service" | rest]), do: service_command(rest)
|
def main(["service" | rest]), do: service_command(rest)
|
||||||
|
def main(["snapshot" | rest]), do: snapshot_command(rest)
|
||||||
def main(["key" | rest]), do: key_command(rest)
|
def main(["key" | rest]), do: key_command(rest)
|
||||||
def main(args), do: execute_command(args)
|
def main(args), do: execute_command(args)
|
||||||
|
|
||||||
|
|
@ -85,6 +86,7 @@ defmodule Un do
|
||||||
IO.puts("Usage: un.ex [options] <source_file>")
|
IO.puts("Usage: un.ex [options] <source_file>")
|
||||||
IO.puts(" un.ex session [options]")
|
IO.puts(" un.ex session [options]")
|
||||||
IO.puts(" un.ex service [options]")
|
IO.puts(" un.ex service [options]")
|
||||||
|
IO.puts(" un.ex snapshot [options]")
|
||||||
IO.puts(" un.ex key [--extend]")
|
IO.puts(" un.ex key [--extend]")
|
||||||
System.halt(1)
|
System.halt(1)
|
||||||
end
|
end
|
||||||
|
|
@ -132,6 +134,26 @@ defmodule Un do
|
||||||
IO.puts("#{@green}Session terminated: #{session_id}#{@reset}")
|
IO.puts("#{@green}Session terminated: #{session_id}#{@reset}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp session_command(["--snapshot", session_id | rest]) do
|
||||||
|
api_key = get_api_key()
|
||||||
|
name = get_opt(rest, "--snapshot-name", nil, nil)
|
||||||
|
hot = "--hot" in rest
|
||||||
|
name_json = if name, do: ",\"name\":\"#{escape_json(name)}\"", else: ""
|
||||||
|
hot_json = if hot, do: ",\"hot\":true", else: ""
|
||||||
|
json = "{#{String.slice(name_json <> hot_json, 1..-1)}}"
|
||||||
|
response = curl_post(api_key, "/sessions/#{session_id}/snapshot", json)
|
||||||
|
IO.puts("#{@green}Snapshot created#{@reset}")
|
||||||
|
IO.puts(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp session_command(["--restore", snapshot_id | _rest]) do
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
api_key = get_api_key()
|
||||||
|
response = curl_post(api_key, "/snapshots/#{snapshot_id}/restore", "{}")
|
||||||
|
IO.puts("#{@green}Session restored from snapshot#{@reset}")
|
||||||
|
IO.puts(response)
|
||||||
|
end
|
||||||
|
|
||||||
defp session_command(args) do
|
defp session_command(args) do
|
||||||
validate_session_args(args)
|
validate_session_args(args)
|
||||||
api_key = get_api_key()
|
api_key = get_api_key()
|
||||||
|
|
@ -156,6 +178,11 @@ defmodule Un do
|
||||||
defp validate_session_args(["-f", _ | rest]), do: validate_session_args(rest)
|
defp validate_session_args(["-f", _ | rest]), do: validate_session_args(rest)
|
||||||
defp validate_session_args(["-n", _ | rest]), do: validate_session_args(rest)
|
defp validate_session_args(["-n", _ | rest]), do: validate_session_args(rest)
|
||||||
defp validate_session_args(["-v", _ | rest]), do: validate_session_args(rest)
|
defp validate_session_args(["-v", _ | rest]), do: validate_session_args(rest)
|
||||||
|
defp validate_session_args(["--snapshot", _ | rest]), do: validate_session_args(rest)
|
||||||
|
defp validate_session_args(["--restore", _ | rest]), do: validate_session_args(rest)
|
||||||
|
defp validate_session_args(["--from", _ | rest]), do: validate_session_args(rest)
|
||||||
|
defp validate_session_args(["--snapshot-name", _ | rest]), do: validate_session_args(rest)
|
||||||
|
defp validate_session_args(["--hot" | rest]), do: validate_session_args(rest)
|
||||||
defp validate_session_args([arg | _]) do
|
defp validate_session_args([arg | _]) do
|
||||||
if String.starts_with?(arg, "-") do
|
if String.starts_with?(arg, "-") do
|
||||||
IO.puts(:stderr, "Unknown option: #{arg}")
|
IO.puts(:stderr, "Unknown option: #{arg}")
|
||||||
|
|
@ -203,6 +230,26 @@ defmodule Un do
|
||||||
IO.puts("#{@green}Service destroyed: #{service_id}#{@reset}")
|
IO.puts("#{@green}Service destroyed: #{service_id}#{@reset}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp service_command(["--snapshot", service_id | rest]) do
|
||||||
|
api_key = get_api_key()
|
||||||
|
name = get_opt(rest, "--snapshot-name", nil, nil)
|
||||||
|
hot = "--hot" in rest
|
||||||
|
name_json = if name, do: ",\"name\":\"#{escape_json(name)}\"", else: ""
|
||||||
|
hot_json = if hot, do: ",\"hot\":true", else: ""
|
||||||
|
json = "{#{String.slice(name_json <> hot_json, 1..-1)}}"
|
||||||
|
response = curl_post(api_key, "/services/#{service_id}/snapshot", json)
|
||||||
|
IO.puts("#{@green}Snapshot created#{@reset}")
|
||||||
|
IO.puts(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp service_command(["--restore", snapshot_id | _rest]) do
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
api_key = get_api_key()
|
||||||
|
response = curl_post(api_key, "/snapshots/#{snapshot_id}/restore", "{}")
|
||||||
|
IO.puts("#{@green}Service restored from snapshot#{@reset}")
|
||||||
|
IO.puts(response)
|
||||||
|
end
|
||||||
|
|
||||||
defp service_command(["--execute", service_id, "--command", command | _]) do
|
defp service_command(["--execute", service_id, "--command", command | _]) do
|
||||||
api_key = get_api_key()
|
api_key = get_api_key()
|
||||||
json = "{\"command\":\"#{escape_json(command)}\"}"
|
json = "{\"command\":\"#{escape_json(command)}\"}"
|
||||||
|
|
@ -286,6 +333,57 @@ defmodule Un do
|
||||||
IO.puts(response)
|
IO.puts(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Snapshot command
|
||||||
|
defp snapshot_command(["--list" | _]) do
|
||||||
|
snapshot_command(["-l"])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp snapshot_command(["-l" | _]) do
|
||||||
|
api_key = get_api_key()
|
||||||
|
response = curl_get(api_key, "/snapshots")
|
||||||
|
IO.puts(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp snapshot_command(["--info", snapshot_id | _]) do
|
||||||
|
api_key = get_api_key()
|
||||||
|
response = curl_get(api_key, "/snapshots/#{snapshot_id}")
|
||||||
|
IO.puts(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp snapshot_command(["--delete", snapshot_id | _]) do
|
||||||
|
api_key = get_api_key()
|
||||||
|
curl_delete(api_key, "/snapshots/#{snapshot_id}")
|
||||||
|
IO.puts("#{@green}Snapshot deleted: #{snapshot_id}#{@reset}")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp snapshot_command(["--clone", snapshot_id | rest]) do
|
||||||
|
api_key = get_api_key()
|
||||||
|
clone_type = get_opt(rest, "--type", nil, nil)
|
||||||
|
name = get_opt(rest, "--name", nil, nil)
|
||||||
|
shell = get_opt(rest, "--shell", nil, nil)
|
||||||
|
ports = get_opt(rest, "--ports", nil, nil)
|
||||||
|
|
||||||
|
if !clone_type do
|
||||||
|
IO.puts(:stderr, "#{@red}Error: --type required (session or service)#{@reset}")
|
||||||
|
System.halt(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
type_json = "\"type\":\"#{clone_type}\""
|
||||||
|
name_json = if name, do: ",\"name\":\"#{escape_json(name)}\"", else: ""
|
||||||
|
shell_json = if shell, do: ",\"shell\":\"#{shell}\"", else: ""
|
||||||
|
ports_json = if ports, do: ",\"ports\":[#{ports}]", else: ""
|
||||||
|
json = "{#{type_json}#{name_json}#{shell_json}#{ports_json}}"
|
||||||
|
|
||||||
|
response = curl_post(api_key, "/snapshots/#{snapshot_id}/clone", json)
|
||||||
|
IO.puts("#{@green}Created from snapshot#{@reset}")
|
||||||
|
IO.puts(response)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp snapshot_command(_) do
|
||||||
|
IO.puts(:stderr, "Error: Use --list, --info ID, --delete ID, or --clone ID --type TYPE")
|
||||||
|
System.halt(1)
|
||||||
|
end
|
||||||
|
|
||||||
# Key command
|
# Key command
|
||||||
defp key_command(args) do
|
defp key_command(args) do
|
||||||
api_key = get_api_key()
|
api_key = get_api_key()
|
||||||
|
|
|
||||||
166
un.fs
166
un.fs
|
|
@ -84,6 +84,11 @@ type Args = {
|
||||||
mutable SessionList: bool
|
mutable SessionList: bool
|
||||||
mutable SessionShell: string option
|
mutable SessionShell: string option
|
||||||
mutable SessionKill: string option
|
mutable SessionKill: string option
|
||||||
|
mutable SessionSnapshot: string option
|
||||||
|
mutable SessionRestore: string option
|
||||||
|
mutable SessionFrom: string option
|
||||||
|
mutable SessionSnapshotName: string option
|
||||||
|
mutable SessionHot: bool
|
||||||
mutable ServiceList: bool
|
mutable ServiceList: bool
|
||||||
mutable ServiceName: string option
|
mutable ServiceName: string option
|
||||||
mutable ServicePorts: string option
|
mutable ServicePorts: string option
|
||||||
|
|
@ -100,6 +105,19 @@ type Args = {
|
||||||
mutable ServiceCommand: string option
|
mutable ServiceCommand: string option
|
||||||
mutable ServiceDumpBootstrap: string option
|
mutable ServiceDumpBootstrap: string option
|
||||||
mutable ServiceDumpFile: string option
|
mutable ServiceDumpFile: string option
|
||||||
|
mutable ServiceSnapshot: string option
|
||||||
|
mutable ServiceRestore: string option
|
||||||
|
mutable ServiceFrom: string option
|
||||||
|
mutable ServiceSnapshotName: string option
|
||||||
|
mutable ServiceHot: bool
|
||||||
|
mutable SnapshotList: bool
|
||||||
|
mutable SnapshotInfo: string option
|
||||||
|
mutable SnapshotDelete: string option
|
||||||
|
mutable SnapshotClone: string option
|
||||||
|
mutable SnapshotType: string option
|
||||||
|
mutable SnapshotName: string option
|
||||||
|
mutable SnapshotShell: string option
|
||||||
|
mutable SnapshotPorts: string option
|
||||||
mutable KeyExtend: bool
|
mutable KeyExtend: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,7 +363,21 @@ let cmdExecute (args: Args) =
|
||||||
let cmdSession (args: Args) =
|
let cmdSession (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
||||||
|
|
||||||
if args.SessionList then
|
if args.SessionSnapshot.IsSome then
|
||||||
|
let mutable payload = []
|
||||||
|
if args.SessionSnapshotName.IsSome then
|
||||||
|
payload <- payload @ [("name", box args.SessionSnapshotName.Value)]
|
||||||
|
if args.SessionHot then
|
||||||
|
payload <- payload @ [("hot", box true)]
|
||||||
|
let result = apiRequest (sprintf "/sessions/%s/snapshot" args.SessionSnapshot.Value) "POST" (Some payload) publicKey secretKey
|
||||||
|
printfn "%sSnapshot created%s" green reset
|
||||||
|
printfn "%s" (toJson (box result))
|
||||||
|
elif args.SessionRestore.IsSome then
|
||||||
|
// --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
let result = apiRequest (sprintf "/snapshots/%s/restore" args.SessionRestore.Value) "POST" None publicKey secretKey
|
||||||
|
printfn "%sSession restored from snapshot%s" green reset
|
||||||
|
printfn "%s" (toJson (box result))
|
||||||
|
elif args.SessionList then
|
||||||
let result = apiRequest "/sessions" "GET" None publicKey secretKey
|
let result = apiRequest "/sessions" "GET" None publicKey secretKey
|
||||||
printfn "%-40s %-10s %-10s %s" "ID" "Shell" "Status" "Created"
|
printfn "%-40s %-10s %-10s %s" "ID" "Shell" "Status" "Created"
|
||||||
printfn "No sessions (list parsing not implemented)"
|
printfn "No sessions (list parsing not implemented)"
|
||||||
|
|
@ -465,10 +497,55 @@ let cmdKey (args: Args) =
|
||||||
printfn "Reason: %s" errorMsg
|
printfn "Reason: %s" errorMsg
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
let cmdSnapshot (args: Args) =
|
||||||
|
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
||||||
|
|
||||||
|
if args.SnapshotList then
|
||||||
|
let result = apiRequest "/snapshots" "GET" None publicKey secretKey
|
||||||
|
printfn "%s" (toJson (box result))
|
||||||
|
elif args.SnapshotInfo.IsSome then
|
||||||
|
let result = apiRequest (sprintf "/snapshots/%s" args.SnapshotInfo.Value) "GET" None publicKey secretKey
|
||||||
|
printfn "%s" (toJson (box result))
|
||||||
|
elif args.SnapshotDelete.IsSome then
|
||||||
|
let result = apiRequest (sprintf "/snapshots/%s" args.SnapshotDelete.Value) "DELETE" None publicKey secretKey
|
||||||
|
printfn "%sSnapshot deleted: %s%s" green args.SnapshotDelete.Value reset
|
||||||
|
elif args.SnapshotClone.IsSome then
|
||||||
|
if args.SnapshotType.IsNone then
|
||||||
|
eprintfn "%sError: --type required (session or service)%s" red reset
|
||||||
|
exit 1
|
||||||
|
let mutable payload = [("type", box args.SnapshotType.Value)]
|
||||||
|
if args.SnapshotName.IsSome then
|
||||||
|
payload <- payload @ [("name", box args.SnapshotName.Value)]
|
||||||
|
if args.SnapshotShell.IsSome then
|
||||||
|
payload <- payload @ [("shell", box args.SnapshotShell.Value)]
|
||||||
|
if args.SnapshotPorts.IsSome then
|
||||||
|
let ports = args.SnapshotPorts.Value.Split(',') |> Array.map (fun p -> box (int (p.Trim())))
|
||||||
|
payload <- payload @ [("ports", box ports)]
|
||||||
|
let result = apiRequest (sprintf "/snapshots/%s/clone" args.SnapshotClone.Value) "POST" (Some payload) publicKey secretKey
|
||||||
|
printfn "%sCreated from snapshot%s" green reset
|
||||||
|
printfn "%s" (toJson (box result))
|
||||||
|
else
|
||||||
|
eprintfn "%sError: Use --list, --info ID, --delete ID, or --clone ID --type TYPE%s" red reset
|
||||||
|
exit 1
|
||||||
|
|
||||||
let cmdService (args: Args) =
|
let cmdService (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
||||||
|
|
||||||
if args.ServiceList then
|
if args.ServiceSnapshot.IsSome then
|
||||||
|
let mutable payload = []
|
||||||
|
if args.ServiceSnapshotName.IsSome then
|
||||||
|
payload <- payload @ [("name", box args.ServiceSnapshotName.Value)]
|
||||||
|
if args.ServiceHot then
|
||||||
|
payload <- payload @ [("hot", box true)]
|
||||||
|
let result = apiRequest (sprintf "/services/%s/snapshot" args.ServiceSnapshot.Value) "POST" (Some payload) publicKey secretKey
|
||||||
|
printfn "%sSnapshot created%s" green reset
|
||||||
|
printfn "%s" (toJson (box result))
|
||||||
|
elif args.ServiceRestore.IsSome then
|
||||||
|
// --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
let result = apiRequest (sprintf "/snapshots/%s/restore" args.ServiceRestore.Value) "POST" None publicKey secretKey
|
||||||
|
printfn "%sService restored from snapshot%s" green reset
|
||||||
|
printfn "%s" (toJson (box result))
|
||||||
|
elif args.ServiceList then
|
||||||
let result = apiRequest "/services" "GET" None publicKey secretKey
|
let result = apiRequest "/services" "GET" None publicKey secretKey
|
||||||
printfn "%-20s %-15s %-10s %-15s %s" "ID" "Name" "Status" "Ports" "Domains"
|
printfn "%-20s %-15s %-10s %-15s %s" "ID" "Name" "Status" "Ports" "Domains"
|
||||||
printfn "No services (list parsing not implemented)"
|
printfn "No services (list parsing not implemented)"
|
||||||
|
|
@ -580,6 +657,11 @@ let parseArgs (argv: string[]) =
|
||||||
SessionList = false
|
SessionList = false
|
||||||
SessionShell = None
|
SessionShell = None
|
||||||
SessionKill = None
|
SessionKill = None
|
||||||
|
SessionSnapshot = None
|
||||||
|
SessionRestore = None
|
||||||
|
SessionFrom = None
|
||||||
|
SessionSnapshotName = None
|
||||||
|
SessionHot = false
|
||||||
ServiceList = false
|
ServiceList = false
|
||||||
ServiceName = None
|
ServiceName = None
|
||||||
ServicePorts = None
|
ServicePorts = None
|
||||||
|
|
@ -596,6 +678,19 @@ let parseArgs (argv: string[]) =
|
||||||
ServiceCommand = None
|
ServiceCommand = None
|
||||||
ServiceDumpBootstrap = None
|
ServiceDumpBootstrap = None
|
||||||
ServiceDumpFile = None
|
ServiceDumpFile = None
|
||||||
|
ServiceSnapshot = None
|
||||||
|
ServiceRestore = None
|
||||||
|
ServiceFrom = None
|
||||||
|
ServiceSnapshotName = None
|
||||||
|
ServiceHot = false
|
||||||
|
SnapshotList = false
|
||||||
|
SnapshotInfo = None
|
||||||
|
SnapshotDelete = None
|
||||||
|
SnapshotClone = None
|
||||||
|
SnapshotType = None
|
||||||
|
SnapshotName = None
|
||||||
|
SnapshotShell = None
|
||||||
|
SnapshotPorts = None
|
||||||
KeyExtend = false
|
KeyExtend = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -604,6 +699,7 @@ let parseArgs (argv: string[]) =
|
||||||
match argv.[i] with
|
match argv.[i] with
|
||||||
| "session" -> args.Command <- Some "session"
|
| "session" -> args.Command <- Some "session"
|
||||||
| "service" -> args.Command <- Some "service"
|
| "service" -> args.Command <- Some "service"
|
||||||
|
| "snapshot" -> args.Command <- Some "snapshot"
|
||||||
| "key" -> args.Command <- Some "key"
|
| "key" -> args.Command <- Some "key"
|
||||||
| "-k" | "--api-key" -> i <- i + 1; args.ApiKey <- Some argv.[i]
|
| "-k" | "--api-key" -> i <- i + 1; args.ApiKey <- Some argv.[i]
|
||||||
| "-n" | "--network" -> i <- i + 1; args.Network <- Some argv.[i]
|
| "-n" | "--network" -> i <- i + 1; args.Network <- Some argv.[i]
|
||||||
|
|
@ -617,14 +713,69 @@ let parseArgs (argv: string[]) =
|
||||||
| Some "session" -> args.SessionList <- true
|
| Some "session" -> args.SessionList <- true
|
||||||
| Some "service" -> args.ServiceList <- true
|
| Some "service" -> args.ServiceList <- true
|
||||||
| _ -> ()
|
| _ -> ()
|
||||||
| "-s" | "--shell" -> i <- i + 1; args.SessionShell <- Some argv.[i]
|
| "-s" | "--shell" ->
|
||||||
|
i <- i + 1
|
||||||
|
match args.Command with
|
||||||
|
| Some "snapshot" -> args.SnapshotShell <- Some argv.[i]
|
||||||
|
| _ -> args.SessionShell <- Some argv.[i]
|
||||||
| "--kill" -> i <- i + 1; args.SessionKill <- Some argv.[i]
|
| "--kill" -> i <- i + 1; args.SessionKill <- Some argv.[i]
|
||||||
| "--name" -> i <- i + 1; args.ServiceName <- Some argv.[i]
|
| "--snapshot" ->
|
||||||
| "--ports" -> i <- i + 1; args.ServicePorts <- Some argv.[i]
|
i <- i + 1
|
||||||
| "--type" -> i <- i + 1; args.ServiceType <- Some argv.[i]
|
match args.Command with
|
||||||
|
| Some "session" -> args.SessionSnapshot <- Some argv.[i]
|
||||||
|
| Some "service" -> args.ServiceSnapshot <- Some argv.[i]
|
||||||
|
| _ -> ()
|
||||||
|
| "--restore" ->
|
||||||
|
i <- i + 1
|
||||||
|
match args.Command with
|
||||||
|
| Some "session" -> args.SessionRestore <- Some argv.[i]
|
||||||
|
| Some "service" -> args.ServiceRestore <- Some argv.[i]
|
||||||
|
| _ -> ()
|
||||||
|
| "--from" ->
|
||||||
|
i <- i + 1
|
||||||
|
match args.Command with
|
||||||
|
| Some "session" -> args.SessionFrom <- Some argv.[i]
|
||||||
|
| Some "service" -> args.ServiceFrom <- Some argv.[i]
|
||||||
|
| _ -> ()
|
||||||
|
| "--snapshot-name" ->
|
||||||
|
i <- i + 1
|
||||||
|
match args.Command with
|
||||||
|
| Some "session" -> args.SessionSnapshotName <- Some argv.[i]
|
||||||
|
| Some "service" -> args.ServiceSnapshotName <- Some argv.[i]
|
||||||
|
| _ -> ()
|
||||||
|
| "--hot" ->
|
||||||
|
match args.Command with
|
||||||
|
| Some "session" -> args.SessionHot <- true
|
||||||
|
| Some "service" -> args.ServiceHot <- true
|
||||||
|
| _ -> ()
|
||||||
|
| "--info" ->
|
||||||
|
i <- i + 1
|
||||||
|
match args.Command with
|
||||||
|
| Some "snapshot" -> args.SnapshotInfo <- Some argv.[i]
|
||||||
|
| _ -> args.ServiceInfo <- Some argv.[i]
|
||||||
|
| "--delete" ->
|
||||||
|
i <- i + 1
|
||||||
|
match args.Command with
|
||||||
|
| Some "snapshot" -> args.SnapshotDelete <- Some argv.[i]
|
||||||
|
| _ -> ()
|
||||||
|
| "--clone" -> i <- i + 1; args.SnapshotClone <- Some argv.[i]
|
||||||
|
| "--type" ->
|
||||||
|
i <- i + 1
|
||||||
|
match args.Command with
|
||||||
|
| Some "snapshot" -> args.SnapshotType <- Some argv.[i]
|
||||||
|
| _ -> args.ServiceType <- Some argv.[i]
|
||||||
|
| "--name" ->
|
||||||
|
i <- i + 1
|
||||||
|
match args.Command with
|
||||||
|
| Some "snapshot" -> args.SnapshotName <- Some argv.[i]
|
||||||
|
| _ -> args.ServiceName <- Some argv.[i]
|
||||||
|
| "--ports" ->
|
||||||
|
i <- i + 1
|
||||||
|
match args.Command with
|
||||||
|
| Some "snapshot" -> args.SnapshotPorts <- Some argv.[i]
|
||||||
|
| _ -> args.ServicePorts <- Some argv.[i]
|
||||||
| "--bootstrap" -> i <- i + 1; args.ServiceBootstrap <- Some argv.[i]
|
| "--bootstrap" -> i <- i + 1; args.ServiceBootstrap <- Some argv.[i]
|
||||||
| "--bootstrap-file" -> i <- i + 1; args.ServiceBootstrapFile <- Some argv.[i]
|
| "--bootstrap-file" -> i <- i + 1; args.ServiceBootstrapFile <- Some argv.[i]
|
||||||
| "--info" -> i <- i + 1; args.ServiceInfo <- Some argv.[i]
|
|
||||||
| "--logs" -> i <- i + 1; args.ServiceLogs <- Some argv.[i]
|
| "--logs" -> i <- i + 1; args.ServiceLogs <- Some argv.[i]
|
||||||
| "--tail" -> i <- i + 1; args.ServiceTail <- Some argv.[i]
|
| "--tail" -> i <- i + 1; args.ServiceTail <- Some argv.[i]
|
||||||
| "--freeze" -> i <- i + 1; args.ServiceSleep <- Some argv.[i]
|
| "--freeze" -> i <- i + 1; args.ServiceSleep <- Some argv.[i]
|
||||||
|
|
@ -694,6 +845,7 @@ let main argv =
|
||||||
match args.Command with
|
match args.Command with
|
||||||
| Some "session" -> cmdSession args; 0
|
| Some "session" -> cmdSession args; 0
|
||||||
| Some "service" -> cmdService args; 0
|
| Some "service" -> cmdService args; 0
|
||||||
|
| Some "snapshot" -> cmdSnapshot args; 0
|
||||||
| Some "key" -> cmdKey args; 0
|
| Some "key" -> cmdKey args; 0
|
||||||
| _ ->
|
| _ ->
|
||||||
match args.SourceFile with
|
match args.SourceFile with
|
||||||
|
|
|
||||||
147
un.go
147
un.go
|
|
@ -330,7 +330,31 @@ func cmdExecute(sourceFile string, envs envVars, files inputFiles, artifacts boo
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int, tmux, screen bool, files inputFiles, publicKey, secretKey string) {
|
func cmdSession(sessionList, sessionKill, sessionShell, sessionSnapshot, sessionRestore, sessionSnapshotName string, sessionHot bool, network string, vcpu int, tmux, screen bool, files inputFiles, publicKey, secretKey string) {
|
||||||
|
if sessionSnapshot != "" {
|
||||||
|
payload := map[string]interface{}{}
|
||||||
|
if sessionSnapshotName != "" {
|
||||||
|
payload["name"] = sessionSnapshotName
|
||||||
|
}
|
||||||
|
if sessionHot {
|
||||||
|
payload["hot"] = true
|
||||||
|
}
|
||||||
|
result := apiRequest("/sessions/"+sessionSnapshot+"/snapshot", "POST", payload, publicKey, secretKey)
|
||||||
|
fmt.Printf("%sSnapshot created%s\n", Green, Reset)
|
||||||
|
jsonData, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
fmt.Println(string(jsonData))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionRestore != "" {
|
||||||
|
// --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
result := apiRequest("/snapshots/"+sessionRestore+"/restore", "POST", nil, publicKey, secretKey)
|
||||||
|
fmt.Printf("%sSession restored from snapshot%s\n", Green, Reset)
|
||||||
|
jsonData, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
fmt.Println(string(jsonData))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if sessionList != "" {
|
if sessionList != "" {
|
||||||
result := apiRequest("/sessions", "GET", nil, publicKey, secretKey)
|
result := apiRequest("/sessions", "GET", nil, publicKey, secretKey)
|
||||||
sessions := result["sessions"].([]interface{})
|
sessions := result["sessions"].([]interface{})
|
||||||
|
|
@ -395,7 +419,31 @@ func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int
|
||||||
fmt.Printf("%sSession created: %s%s\n", Green, result["id"], Reset)
|
fmt.Printf("%sSession created: %s%s\n", Green, result["id"], Reset)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceBootstrap, serviceBootstrapFile, serviceList, serviceInfo, serviceLogs, serviceTail, serviceSleep, serviceWake, serviceDestroy, serviceExecute, serviceCommand, serviceDumpBootstrap, serviceDumpFile, network string, vcpu int, files inputFiles, publicKey, secretKey string) {
|
func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceBootstrap, serviceBootstrapFile, serviceList, serviceInfo, serviceLogs, serviceTail, serviceSleep, serviceWake, serviceDestroy, serviceExecute, serviceCommand, serviceDumpBootstrap, serviceDumpFile, serviceSnapshot, serviceRestore, serviceSnapshotName string, serviceHot bool, network string, vcpu int, files inputFiles, publicKey, secretKey string) {
|
||||||
|
if serviceSnapshot != "" {
|
||||||
|
payload := map[string]interface{}{}
|
||||||
|
if serviceSnapshotName != "" {
|
||||||
|
payload["name"] = serviceSnapshotName
|
||||||
|
}
|
||||||
|
if serviceHot {
|
||||||
|
payload["hot"] = true
|
||||||
|
}
|
||||||
|
result := apiRequest("/services/"+serviceSnapshot+"/snapshot", "POST", payload, publicKey, secretKey)
|
||||||
|
fmt.Printf("%sSnapshot created%s\n", Green, Reset)
|
||||||
|
jsonData, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
fmt.Println(string(jsonData))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if serviceRestore != "" {
|
||||||
|
// --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
result := apiRequest("/snapshots/"+serviceRestore+"/restore", "POST", nil, publicKey, secretKey)
|
||||||
|
fmt.Printf("%sService restored from snapshot%s\n", Green, Reset)
|
||||||
|
jsonData, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
fmt.Println(string(jsonData))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if serviceList != "" {
|
if serviceList != "" {
|
||||||
result := apiRequest("/services", "GET", nil, publicKey, secretKey)
|
result := apiRequest("/services", "GET", nil, publicKey, secretKey)
|
||||||
services := result["services"].([]interface{})
|
services := result["services"].([]interface{})
|
||||||
|
|
@ -566,6 +614,60 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cmdSnapshot(snapshotList, snapshotInfo, snapshotDelete, snapshotClone, snapshotType, snapshotName, snapshotShell, snapshotPorts, publicKey, secretKey string) {
|
||||||
|
if snapshotList != "" || snapshotList == "" && snapshotInfo == "" && snapshotDelete == "" && snapshotClone == "" {
|
||||||
|
result := apiRequest("/snapshots", "GET", nil, publicKey, secretKey)
|
||||||
|
jsonData, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
fmt.Println(string(jsonData))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if snapshotInfo != "" {
|
||||||
|
result := apiRequest("/snapshots/"+snapshotInfo, "GET", nil, publicKey, secretKey)
|
||||||
|
jsonData, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
fmt.Println(string(jsonData))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if snapshotDelete != "" {
|
||||||
|
apiRequest("/snapshots/"+snapshotDelete, "DELETE", nil, publicKey, secretKey)
|
||||||
|
fmt.Printf("%sSnapshot deleted: %s%s\n", Green, snapshotDelete, Reset)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if snapshotClone != "" {
|
||||||
|
if snapshotType == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "%sError: --type required (session or service)%s\n", Red, Reset)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"type": snapshotType,
|
||||||
|
}
|
||||||
|
if snapshotName != "" {
|
||||||
|
payload["name"] = snapshotName
|
||||||
|
}
|
||||||
|
if snapshotShell != "" {
|
||||||
|
payload["shell"] = snapshotShell
|
||||||
|
}
|
||||||
|
if snapshotPorts != "" {
|
||||||
|
var ports []int
|
||||||
|
for _, p := range strings.Split(snapshotPorts, ",") {
|
||||||
|
port, _ := strconv.Atoi(strings.TrimSpace(p))
|
||||||
|
ports = append(ports, port)
|
||||||
|
}
|
||||||
|
payload["ports"] = ports
|
||||||
|
}
|
||||||
|
result := apiRequest("/snapshots/"+snapshotClone+"/clone", "POST", payload, publicKey, secretKey)
|
||||||
|
fmt.Printf("%sCreated from snapshot%s\n", Green, Reset)
|
||||||
|
jsonData, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
fmt.Println(string(jsonData))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "%sError: Use --list, --info ID, --delete ID, or --clone ID --type TYPE%s\n", Red, Reset)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
func openBrowser(url string) error {
|
func openBrowser(url string) error {
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
|
|
@ -647,14 +749,14 @@ func validateKey(publicKey, secretKey string, extend bool) {
|
||||||
|
|
||||||
valid, _ := result["valid"].(bool)
|
valid, _ := result["valid"].(bool)
|
||||||
expired, _ := result["expired"].(bool)
|
expired, _ := result["expired"].(bool)
|
||||||
publicKey, _ := result["public_key"].(string)
|
pubKey, _ := result["public_key"].(string)
|
||||||
tier, _ := result["tier"].(string)
|
tier, _ := result["tier"].(string)
|
||||||
status, _ := result["status"].(string)
|
status, _ := result["status"].(string)
|
||||||
|
|
||||||
if expired {
|
if expired {
|
||||||
// Expired key
|
// Expired key
|
||||||
fmt.Printf("%sExpired%s\n", Red, Reset)
|
fmt.Printf("%sExpired%s\n", Red, Reset)
|
||||||
fmt.Printf("Public Key: %s\n", publicKey)
|
fmt.Printf("Public Key: %s\n", pubKey)
|
||||||
fmt.Printf("Tier: %s\n", tier)
|
fmt.Printf("Tier: %s\n", tier)
|
||||||
if expiresAt, ok := result["expires_at"].(string); ok {
|
if expiresAt, ok := result["expires_at"].(string); ok {
|
||||||
fmt.Printf("Expired: %s\n", expiresAt)
|
fmt.Printf("Expired: %s\n", expiresAt)
|
||||||
|
|
@ -662,7 +764,7 @@ func validateKey(publicKey, secretKey string, extend bool) {
|
||||||
fmt.Printf("%sTo renew: Visit https://unsandbox.com/keys/extend%s\n", Yellow, Reset)
|
fmt.Printf("%sTo renew: Visit https://unsandbox.com/keys/extend%s\n", Yellow, Reset)
|
||||||
|
|
||||||
if extend {
|
if extend {
|
||||||
extendURL := PortalBase + "/keys/extend?pk=" + publicKey
|
extendURL := PortalBase + "/keys/extend?pk=" + pubKey
|
||||||
fmt.Printf("\n%sOpening browser to extend key...%s\n", Green, Reset)
|
fmt.Printf("\n%sOpening browser to extend key...%s\n", Green, Reset)
|
||||||
if err := openBrowser(extendURL); err != nil {
|
if err := openBrowser(extendURL); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%sError opening browser: %v%s\n", Red, err, Reset)
|
fmt.Fprintf(os.Stderr, "%sError opening browser: %v%s\n", Red, err, Reset)
|
||||||
|
|
@ -675,7 +777,7 @@ func validateKey(publicKey, secretKey string, extend bool) {
|
||||||
if valid {
|
if valid {
|
||||||
// Valid key
|
// Valid key
|
||||||
fmt.Printf("%sValid%s\n", Green, Reset)
|
fmt.Printf("%sValid%s\n", Green, Reset)
|
||||||
fmt.Printf("Public Key: %s\n", publicKey)
|
fmt.Printf("Public Key: %s\n", pubKey)
|
||||||
fmt.Printf("Tier: %s\n", tier)
|
fmt.Printf("Tier: %s\n", tier)
|
||||||
fmt.Printf("Status: %s\n", status)
|
fmt.Printf("Status: %s\n", status)
|
||||||
|
|
||||||
|
|
@ -703,7 +805,7 @@ func validateKey(publicKey, secretKey string, extend bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if extend {
|
if extend {
|
||||||
extendURL := PortalBase + "/keys/extend?pk=" + publicKey
|
extendURL := PortalBase + "/keys/extend?pk=" + pubKey
|
||||||
fmt.Printf("\n%sOpening browser to extend key...%s\n", Green, Reset)
|
fmt.Printf("\n%sOpening browser to extend key...%s\n", Green, Reset)
|
||||||
if err := openBrowser(extendURL); err != nil {
|
if err := openBrowser(extendURL); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%sError opening browser: %v%s\n", Red, err, Reset)
|
fmt.Fprintf(os.Stderr, "%sError opening browser: %v%s\n", Red, err, Reset)
|
||||||
|
|
@ -741,6 +843,10 @@ func main() {
|
||||||
sessionShell := sessionCmd.String("shell", "", "Shell/REPL to use")
|
sessionShell := sessionCmd.String("shell", "", "Shell/REPL to use")
|
||||||
sessionTmux := sessionCmd.Bool("tmux", false, "Enable tmux persistence")
|
sessionTmux := sessionCmd.Bool("tmux", false, "Enable tmux persistence")
|
||||||
sessionScreen := sessionCmd.Bool("screen", false, "Enable screen persistence")
|
sessionScreen := sessionCmd.Bool("screen", false, "Enable screen persistence")
|
||||||
|
sessionSnapshot := sessionCmd.String("snapshot", "", "Create snapshot of session")
|
||||||
|
sessionRestore := sessionCmd.String("restore", "", "Restore from snapshot ID")
|
||||||
|
sessionSnapshotName := sessionCmd.String("snapshot-name", "", "Name for snapshot")
|
||||||
|
sessionHot := sessionCmd.Bool("hot", false, "Take snapshot without freezing")
|
||||||
var sessionFiles inputFiles
|
var sessionFiles inputFiles
|
||||||
sessionCmd.Var(&sessionFiles, "f", "Input file")
|
sessionCmd.Var(&sessionFiles, "f", "Input file")
|
||||||
sessionNetwork := sessionCmd.String("n", "", "Network mode")
|
sessionNetwork := sessionCmd.String("n", "", "Network mode")
|
||||||
|
|
@ -768,10 +874,26 @@ func main() {
|
||||||
serviceCommand := serviceCmd.String("command", "", "Command to execute (with -execute)")
|
serviceCommand := serviceCmd.String("command", "", "Command to execute (with -execute)")
|
||||||
serviceDumpBootstrap := serviceCmd.String("dump-bootstrap", "", "Dump bootstrap script")
|
serviceDumpBootstrap := serviceCmd.String("dump-bootstrap", "", "Dump bootstrap script")
|
||||||
serviceDumpFile := serviceCmd.String("dump-file", "", "File to save bootstrap (with -dump-bootstrap)")
|
serviceDumpFile := serviceCmd.String("dump-file", "", "File to save bootstrap (with -dump-bootstrap)")
|
||||||
|
serviceSnapshot := serviceCmd.String("snapshot", "", "Create snapshot of service")
|
||||||
|
serviceRestore := serviceCmd.String("restore", "", "Restore from snapshot ID")
|
||||||
|
serviceSnapshotName := serviceCmd.String("snapshot-name", "", "Name for snapshot")
|
||||||
|
serviceHot := serviceCmd.Bool("hot", false, "Take snapshot without freezing")
|
||||||
serviceNetwork := serviceCmd.String("n", "", "Network mode")
|
serviceNetwork := serviceCmd.String("n", "", "Network mode")
|
||||||
serviceVcpu := serviceCmd.Int("v", 0, "vCPU count")
|
serviceVcpu := serviceCmd.Int("v", 0, "vCPU count")
|
||||||
serviceKey := serviceCmd.String("k", "", "API key")
|
serviceKey := serviceCmd.String("k", "", "API key")
|
||||||
|
|
||||||
|
// Snapshot flags
|
||||||
|
snapshotCmd := flag.NewFlagSet("snapshot", flag.ExitOnError)
|
||||||
|
snapshotList := snapshotCmd.String("list", "", "List snapshots")
|
||||||
|
snapshotInfo := snapshotCmd.String("info", "", "Get snapshot info")
|
||||||
|
snapshotDelete := snapshotCmd.String("delete", "", "Delete snapshot")
|
||||||
|
snapshotClone := snapshotCmd.String("clone", "", "Clone snapshot")
|
||||||
|
snapshotType := snapshotCmd.String("type", "", "Clone type (session/service)")
|
||||||
|
snapshotName := snapshotCmd.String("name", "", "Name for cloned resource")
|
||||||
|
snapshotShell := snapshotCmd.String("shell", "", "Shell for cloned session")
|
||||||
|
snapshotPorts := snapshotCmd.String("ports", "", "Ports for cloned service")
|
||||||
|
snapshotKey := snapshotCmd.String("k", "", "API key")
|
||||||
|
|
||||||
// Key flags
|
// Key flags
|
||||||
keyCmd := flag.NewFlagSet("key", flag.ExitOnError)
|
keyCmd := flag.NewFlagSet("key", flag.ExitOnError)
|
||||||
keyExtend := keyCmd.Bool("extend", false, "Open browser to extend key")
|
keyExtend := keyCmd.Bool("extend", false, "Open browser to extend key")
|
||||||
|
|
@ -793,7 +915,7 @@ func main() {
|
||||||
if vc == 0 {
|
if vc == 0 {
|
||||||
vc = *vcpu
|
vc = *vcpu
|
||||||
}
|
}
|
||||||
cmdSession(*sessionList, *sessionKill, *sessionShell, net, vc, *sessionTmux, *sessionScreen, sessionFiles, publicKey, secretKey)
|
cmdSession(*sessionList, *sessionKill, *sessionShell, *sessionSnapshot, *sessionRestore, *sessionSnapshotName, *sessionHot, net, vc, *sessionTmux, *sessionScreen, sessionFiles, publicKey, secretKey)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "service":
|
case "service":
|
||||||
|
|
@ -807,7 +929,13 @@ func main() {
|
||||||
if vc == 0 {
|
if vc == 0 {
|
||||||
vc = *vcpu
|
vc = *vcpu
|
||||||
}
|
}
|
||||||
cmdService(*serviceName, *servicePorts, *serviceDomains, *serviceType, *serviceBootstrap, *serviceBootstrapFile, *serviceList, *serviceInfo, *serviceLogs, *serviceTail, *serviceSleep, *serviceWake, *serviceDestroy, *serviceExecute, *serviceCommand, *serviceDumpBootstrap, *serviceDumpFile, net, vc, serviceFiles, publicKey, secretKey)
|
cmdService(*serviceName, *servicePorts, *serviceDomains, *serviceType, *serviceBootstrap, *serviceBootstrapFile, *serviceList, *serviceInfo, *serviceLogs, *serviceTail, *serviceSleep, *serviceWake, *serviceDestroy, *serviceExecute, *serviceCommand, *serviceDumpBootstrap, *serviceDumpFile, *serviceSnapshot, *serviceRestore, *serviceSnapshotName, *serviceHot, net, vc, serviceFiles, publicKey, secretKey)
|
||||||
|
return
|
||||||
|
|
||||||
|
case "snapshot":
|
||||||
|
snapshotCmd.Parse(os.Args[2:])
|
||||||
|
publicKey, secretKey := getAPIKeys(*snapshotKey)
|
||||||
|
cmdSnapshot(*snapshotList, *snapshotInfo, *snapshotDelete, *snapshotClone, *snapshotType, *snapshotName, *snapshotShell, *snapshotPorts, publicKey, secretKey)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "key":
|
case "key":
|
||||||
|
|
@ -823,6 +951,7 @@ func main() {
|
||||||
fmt.Fprintf(os.Stderr, "Usage: %s [options] <source_file>\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, "Usage: %s [options] <source_file>\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, " %s session [options]\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s session [options]\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, " %s service [options]\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s service [options]\n", os.Args[0])
|
||||||
|
fmt.Fprintf(os.Stderr, " %s snapshot [options]\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, " %s key [options]\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s key [options]\n", os.Args[0])
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
168
un.groovy
168
un.groovy
|
|
@ -74,6 +74,11 @@ class Args {
|
||||||
Boolean sessionList = false
|
Boolean sessionList = false
|
||||||
String sessionShell = null
|
String sessionShell = null
|
||||||
String sessionKill = null
|
String sessionKill = null
|
||||||
|
String sessionSnapshot = null
|
||||||
|
String sessionRestore = null
|
||||||
|
String sessionFrom = null
|
||||||
|
String sessionSnapshotName = null
|
||||||
|
Boolean sessionHot = false
|
||||||
Boolean serviceList = false
|
Boolean serviceList = false
|
||||||
String serviceName = null
|
String serviceName = null
|
||||||
String servicePorts = null
|
String servicePorts = null
|
||||||
|
|
@ -90,6 +95,19 @@ class Args {
|
||||||
String serviceCommand = null
|
String serviceCommand = null
|
||||||
String serviceDumpBootstrap = null
|
String serviceDumpBootstrap = null
|
||||||
String serviceDumpFile = null
|
String serviceDumpFile = null
|
||||||
|
String serviceSnapshot = null
|
||||||
|
String serviceRestore = null
|
||||||
|
String serviceFrom = null
|
||||||
|
String serviceSnapshotName = null
|
||||||
|
Boolean serviceHot = false
|
||||||
|
Boolean snapshotList = false
|
||||||
|
String snapshotInfo = null
|
||||||
|
String snapshotDelete = null
|
||||||
|
String snapshotClone = null
|
||||||
|
String snapshotType = null
|
||||||
|
String snapshotName = null
|
||||||
|
String snapshotShell = null
|
||||||
|
String snapshotPorts = null
|
||||||
Boolean keyExtend = false
|
Boolean keyExtend = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,6 +306,30 @@ def cmdExecute(args) {
|
||||||
def cmdSession(args) {
|
def cmdSession(args) {
|
||||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||||
|
|
||||||
|
if (args.sessionSnapshot) {
|
||||||
|
def json = "{"
|
||||||
|
if (args.sessionSnapshotName) {
|
||||||
|
json += "\"name\":\"${args.sessionSnapshotName.replace('\\', '\\\\').replace('"', '\\"')}\""
|
||||||
|
}
|
||||||
|
if (args.sessionHot) {
|
||||||
|
if (args.sessionSnapshotName) json += ","
|
||||||
|
json += "\"hot\":true"
|
||||||
|
}
|
||||||
|
json += "}"
|
||||||
|
def output = apiRequest("/sessions/${args.sessionSnapshot}/snapshot", 'POST', json, publicKey, secretKey)
|
||||||
|
println("${GREEN}Snapshot created${RESET}")
|
||||||
|
println(output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.sessionRestore) {
|
||||||
|
// --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
def output = apiRequest("/snapshots/${args.sessionRestore}/restore", 'POST', '{}', publicKey, secretKey)
|
||||||
|
println("${GREEN}Session restored from snapshot${RESET}")
|
||||||
|
println(output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (args.sessionList) {
|
if (args.sessionList) {
|
||||||
def output = apiRequest('/sessions', 'GET', null, publicKey, secretKey)
|
def output = apiRequest('/sessions', 'GET', null, publicKey, secretKey)
|
||||||
println("%-40s %-10s %-10s %s".format("ID", "Shell", "Status", "Created"))
|
println("%-40s %-10s %-10s %s".format("ID", "Shell", "Status", "Created"))
|
||||||
|
|
@ -351,6 +393,53 @@ def openBrowser(url) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def cmdSnapshot(args) {
|
||||||
|
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||||
|
|
||||||
|
if (args.snapshotList) {
|
||||||
|
def output = apiRequest('/snapshots', 'GET', null, publicKey, secretKey)
|
||||||
|
println(output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.snapshotInfo) {
|
||||||
|
def output = apiRequest("/snapshots/${args.snapshotInfo}", 'GET', null, publicKey, secretKey)
|
||||||
|
println(output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.snapshotDelete) {
|
||||||
|
apiRequest("/snapshots/${args.snapshotDelete}", 'DELETE', null, publicKey, secretKey)
|
||||||
|
println("${GREEN}Snapshot deleted: ${args.snapshotDelete}${RESET}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.snapshotClone) {
|
||||||
|
if (!args.snapshotType) {
|
||||||
|
System.err.println("${RED}Error: --type required (session or service)${RESET}")
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
def json = "{\"type\":\"${args.snapshotType}\""
|
||||||
|
if (args.snapshotName) {
|
||||||
|
json += ",\"name\":\"${args.snapshotName.replace('\\', '\\\\').replace('"', '\\"')}\""
|
||||||
|
}
|
||||||
|
if (args.snapshotShell) {
|
||||||
|
json += ",\"shell\":\"${args.snapshotShell}\""
|
||||||
|
}
|
||||||
|
if (args.snapshotPorts) {
|
||||||
|
json += ",\"ports\":[${args.snapshotPorts}]"
|
||||||
|
}
|
||||||
|
json += "}"
|
||||||
|
def output = apiRequest("/snapshots/${args.snapshotClone}/clone", 'POST', json, publicKey, secretKey)
|
||||||
|
println("${GREEN}Created from snapshot${RESET}")
|
||||||
|
println(output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
System.err.println("Error: Use --list, --info ID, --delete ID, or --clone ID --type TYPE")
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
def cmdKey(args) {
|
def cmdKey(args) {
|
||||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||||
|
|
||||||
|
|
@ -435,6 +524,30 @@ def cmdKey(args) {
|
||||||
def cmdService(args) {
|
def cmdService(args) {
|
||||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||||
|
|
||||||
|
if (args.serviceSnapshot) {
|
||||||
|
def json = "{"
|
||||||
|
if (args.serviceSnapshotName) {
|
||||||
|
json += "\"name\":\"${args.serviceSnapshotName.replace('\\', '\\\\').replace('"', '\\"')}\""
|
||||||
|
}
|
||||||
|
if (args.serviceHot) {
|
||||||
|
if (args.serviceSnapshotName) json += ","
|
||||||
|
json += "\"hot\":true"
|
||||||
|
}
|
||||||
|
json += "}"
|
||||||
|
def output = apiRequest("/services/${args.serviceSnapshot}/snapshot", 'POST', json, publicKey, secretKey)
|
||||||
|
println("${GREEN}Snapshot created${RESET}")
|
||||||
|
println(output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.serviceRestore) {
|
||||||
|
// --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
def output = apiRequest("/snapshots/${args.serviceRestore}/restore", 'POST', '{}', publicKey, secretKey)
|
||||||
|
println("${GREEN}Service restored from snapshot${RESET}")
|
||||||
|
println(output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (args.serviceList) {
|
if (args.serviceList) {
|
||||||
def output = apiRequest('/services', 'GET', null, publicKey, secretKey)
|
def output = apiRequest('/services', 'GET', null, publicKey, secretKey)
|
||||||
println("%-20s %-15s %-10s %-15s %s".format("ID", "Name", "Status", "Ports", "Domains"))
|
println("%-20s %-15s %-10s %-15s %s".format("ID", "Name", "Status", "Ports", "Domains"))
|
||||||
|
|
@ -619,6 +732,9 @@ def parseArgs(argv) {
|
||||||
case 'service':
|
case 'service':
|
||||||
args.command = 'service'
|
args.command = 'service'
|
||||||
break
|
break
|
||||||
|
case 'snapshot':
|
||||||
|
args.command = 'snapshot'
|
||||||
|
break
|
||||||
case 'key':
|
case 'key':
|
||||||
args.command = 'key'
|
args.command = 'key'
|
||||||
break
|
break
|
||||||
|
|
@ -662,14 +778,51 @@ def parseArgs(argv) {
|
||||||
case '--kill':
|
case '--kill':
|
||||||
args.sessionKill = argv[++i]
|
args.sessionKill = argv[++i]
|
||||||
break
|
break
|
||||||
case '--name':
|
case '--snapshot':
|
||||||
args.serviceName = argv[++i]
|
if (args.command == 'session') args.sessionSnapshot = argv[++i]
|
||||||
|
else if (args.command == 'service') args.serviceSnapshot = argv[++i]
|
||||||
break
|
break
|
||||||
case '--ports':
|
case '--restore':
|
||||||
args.servicePorts = argv[++i]
|
if (args.command == 'session') args.sessionRestore = argv[++i]
|
||||||
|
else if (args.command == 'service') args.serviceRestore = argv[++i]
|
||||||
|
break
|
||||||
|
case '--from':
|
||||||
|
if (args.command == 'session') args.sessionFrom = argv[++i]
|
||||||
|
else if (args.command == 'service') args.serviceFrom = argv[++i]
|
||||||
|
break
|
||||||
|
case '--snapshot-name':
|
||||||
|
if (args.command == 'session') args.sessionSnapshotName = argv[++i]
|
||||||
|
else if (args.command == 'service') args.serviceSnapshotName = argv[++i]
|
||||||
|
break
|
||||||
|
case '--hot':
|
||||||
|
if (args.command == 'session') args.sessionHot = true
|
||||||
|
else if (args.command == 'service') args.serviceHot = true
|
||||||
|
break
|
||||||
|
case '--info':
|
||||||
|
if (args.command == 'snapshot') args.snapshotInfo = argv[++i]
|
||||||
|
else args.serviceInfo = argv[++i]
|
||||||
|
break
|
||||||
|
case '--delete':
|
||||||
|
if (args.command == 'snapshot') args.snapshotDelete = argv[++i]
|
||||||
|
break
|
||||||
|
case '--clone':
|
||||||
|
args.snapshotClone = argv[++i]
|
||||||
break
|
break
|
||||||
case '--type':
|
case '--type':
|
||||||
args.serviceType = argv[++i]
|
if (args.command == 'snapshot') args.snapshotType = argv[++i]
|
||||||
|
else args.serviceType = argv[++i]
|
||||||
|
break
|
||||||
|
case '--name':
|
||||||
|
if (args.command == 'snapshot') args.snapshotName = argv[++i]
|
||||||
|
else args.serviceName = argv[++i]
|
||||||
|
break
|
||||||
|
case '--shell':
|
||||||
|
if (args.command == 'snapshot') args.snapshotShell = argv[++i]
|
||||||
|
else args.sessionShell = argv[++i]
|
||||||
|
break
|
||||||
|
case '--ports':
|
||||||
|
if (args.command == 'snapshot') args.snapshotPorts = argv[++i]
|
||||||
|
else args.servicePorts = argv[++i]
|
||||||
break
|
break
|
||||||
case '--bootstrap':
|
case '--bootstrap':
|
||||||
args.serviceBootstrap = argv[++i]
|
args.serviceBootstrap = argv[++i]
|
||||||
|
|
@ -677,9 +830,6 @@ def parseArgs(argv) {
|
||||||
case '--bootstrap-file':
|
case '--bootstrap-file':
|
||||||
args.serviceBootstrapFile = argv[++i]
|
args.serviceBootstrapFile = argv[++i]
|
||||||
break
|
break
|
||||||
case '--info':
|
|
||||||
args.serviceInfo = argv[++i]
|
|
||||||
break
|
|
||||||
case '--logs':
|
case '--logs':
|
||||||
args.serviceLogs = argv[++i]
|
args.serviceLogs = argv[++i]
|
||||||
break
|
break
|
||||||
|
|
@ -774,6 +924,8 @@ try {
|
||||||
cmdSession(args)
|
cmdSession(args)
|
||||||
} else if (args.command == 'service') {
|
} else if (args.command == 'service') {
|
||||||
cmdService(args)
|
cmdService(args)
|
||||||
|
} else if (args.command == 'snapshot') {
|
||||||
|
cmdSnapshot(args)
|
||||||
} else if (args.command == 'key') {
|
} else if (args.command == 'key') {
|
||||||
cmdKey(args)
|
cmdKey(args)
|
||||||
} else if (args.sourceFile) {
|
} else if (args.sourceFile) {
|
||||||
|
|
|
||||||
132
un.hs
132
un.hs
|
|
@ -116,7 +116,7 @@ escapeJSON = concatMap escape
|
||||||
escape c = [c]
|
escape c = [c]
|
||||||
|
|
||||||
-- Parse command line arguments
|
-- Parse command line arguments
|
||||||
data Command = Execute ExecuteOpts | Session SessionOpts | Service ServiceOpts | Key KeyOpts | Help
|
data Command = Execute ExecuteOpts | Session SessionOpts | Service ServiceOpts | Key KeyOpts | Snapshot SnapshotOpts | Help
|
||||||
|
|
||||||
data ExecuteOpts = ExecuteOpts
|
data ExecuteOpts = ExecuteOpts
|
||||||
{ exFile :: String
|
{ exFile :: String
|
||||||
|
|
@ -134,9 +134,13 @@ data SessionOpts = SessionOpts
|
||||||
, sessNetwork :: Maybe String
|
, sessNetwork :: Maybe String
|
||||||
, sessVcpu :: Maybe Int
|
, sessVcpu :: Maybe Int
|
||||||
, sessFiles :: [String]
|
, sessFiles :: [String]
|
||||||
|
, sessSnapshotName :: Maybe String
|
||||||
|
, sessSnapshotFrom :: Maybe String
|
||||||
|
, sessHot :: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
data SessionAction = SessionList | SessionKill String | SessionCreate
|
data SessionAction = SessionList | SessionKill String | SessionCreate
|
||||||
|
| SessionSnapshot String | SessionRestore String
|
||||||
|
|
||||||
data ServiceOpts = ServiceOpts
|
data ServiceOpts = ServiceOpts
|
||||||
{ svcAction :: ServiceAction
|
{ svcAction :: ServiceAction
|
||||||
|
|
@ -148,12 +152,25 @@ data ServiceOpts = ServiceOpts
|
||||||
, svcNetwork :: Maybe String
|
, svcNetwork :: Maybe String
|
||||||
, svcVcpu :: Maybe Int
|
, svcVcpu :: Maybe Int
|
||||||
, svcFiles :: [String]
|
, svcFiles :: [String]
|
||||||
|
, svcSnapshotName :: Maybe String
|
||||||
|
, svcSnapshotFrom :: Maybe String
|
||||||
|
, svcHot :: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
data ServiceAction = ServiceList | ServiceInfo String | ServiceLogs String
|
data ServiceAction = ServiceList | ServiceInfo String | ServiceLogs String
|
||||||
| ServiceSleep String | ServiceWake String | ServiceDestroy String
|
| ServiceSleep String | ServiceWake String | ServiceDestroy String
|
||||||
| ServiceExecute String String | ServiceDumpBootstrap String (Maybe String)
|
| ServiceExecute String String | ServiceDumpBootstrap String (Maybe String)
|
||||||
| ServiceCreate
|
| ServiceCreate | ServiceSnapshot String | ServiceRestore String
|
||||||
|
|
||||||
|
data SnapshotOpts = SnapshotOpts
|
||||||
|
{ snapAction :: SnapshotAction
|
||||||
|
, snapCloneType :: Maybe String
|
||||||
|
, snapCloneName :: Maybe String
|
||||||
|
, snapClonePorts :: Maybe String
|
||||||
|
}
|
||||||
|
|
||||||
|
data SnapshotAction = SnapshotList | SnapshotInfo String | SnapshotDelete String
|
||||||
|
| SnapshotClone String
|
||||||
|
|
||||||
data KeyOpts = KeyOpts
|
data KeyOpts = KeyOpts
|
||||||
{ keyExtend :: Bool
|
{ keyExtend :: Bool
|
||||||
|
|
@ -164,6 +181,7 @@ parseArgs :: [String] -> IO Command
|
||||||
parseArgs ("session":rest) = Session <$> parseSession rest
|
parseArgs ("session":rest) = Session <$> parseSession rest
|
||||||
parseArgs ("service":rest) = Service <$> parseService rest
|
parseArgs ("service":rest) = Service <$> parseService rest
|
||||||
parseArgs ("key":rest) = Key <$> parseKey rest
|
parseArgs ("key":rest) = Key <$> parseKey rest
|
||||||
|
parseArgs ("snapshot":rest) = Snapshot <$> parseSnapshot rest
|
||||||
parseArgs args = parseExecute args
|
parseArgs args = parseExecute args
|
||||||
|
|
||||||
parseKey :: [String] -> IO KeyOpts
|
parseKey :: [String] -> IO KeyOpts
|
||||||
|
|
@ -174,15 +192,35 @@ parseKey args = return $ parseKeyArgs args defaultKeyOpts
|
||||||
parseKeyArgs ("--extend":rest) opts = parseKeyArgs rest opts { keyExtend = True }
|
parseKeyArgs ("--extend":rest) opts = parseKeyArgs rest opts { keyExtend = True }
|
||||||
parseKeyArgs (_:rest) opts = parseKeyArgs rest opts
|
parseKeyArgs (_:rest) opts = parseKeyArgs rest opts
|
||||||
|
|
||||||
|
parseSnapshot :: [String] -> IO SnapshotOpts
|
||||||
|
parseSnapshot args = return $ parseSnapshotArgs args defaultSnapshotOpts
|
||||||
|
where
|
||||||
|
defaultSnapshotOpts = SnapshotOpts SnapshotList Nothing Nothing Nothing
|
||||||
|
parseSnapshotArgs [] opts = opts
|
||||||
|
parseSnapshotArgs ("--list":rest) opts = parseSnapshotArgs rest opts { snapAction = SnapshotList }
|
||||||
|
parseSnapshotArgs ("-l":rest) opts = parseSnapshotArgs rest opts { snapAction = SnapshotList }
|
||||||
|
parseSnapshotArgs ("--info":id:rest) opts = parseSnapshotArgs rest opts { snapAction = SnapshotInfo id }
|
||||||
|
parseSnapshotArgs ("--delete":id:rest) opts = parseSnapshotArgs rest opts { snapAction = SnapshotDelete id }
|
||||||
|
parseSnapshotArgs ("--clone":id:rest) opts = parseSnapshotArgs rest opts { snapAction = SnapshotClone id }
|
||||||
|
parseSnapshotArgs ("--type":t:rest) opts = parseSnapshotArgs rest opts { snapCloneType = Just t }
|
||||||
|
parseSnapshotArgs ("--name":n:rest) opts = parseSnapshotArgs rest opts { snapCloneName = Just n }
|
||||||
|
parseSnapshotArgs ("--ports":p:rest) opts = parseSnapshotArgs rest opts { snapClonePorts = Just p }
|
||||||
|
parseSnapshotArgs (_:rest) opts = parseSnapshotArgs rest opts
|
||||||
|
|
||||||
parseSession :: [String] -> IO SessionOpts
|
parseSession :: [String] -> IO SessionOpts
|
||||||
parseSession args = return $ parseSessionArgs args defaultSessionOpts
|
parseSession args = return $ parseSessionArgs args defaultSessionOpts
|
||||||
where
|
where
|
||||||
defaultSessionOpts = SessionOpts SessionCreate Nothing Nothing Nothing []
|
defaultSessionOpts = SessionOpts SessionCreate Nothing Nothing Nothing [] Nothing Nothing False
|
||||||
parseSessionArgs [] opts = opts
|
parseSessionArgs [] opts = opts
|
||||||
parseSessionArgs ("--list":rest) opts = parseSessionArgs rest opts { sessAction = SessionList }
|
parseSessionArgs ("--list":rest) opts = parseSessionArgs rest opts { sessAction = SessionList }
|
||||||
parseSessionArgs ("--kill":id:rest) opts = parseSessionArgs rest opts { sessAction = SessionKill id }
|
parseSessionArgs ("--kill":id:rest) opts = parseSessionArgs rest opts { sessAction = SessionKill id }
|
||||||
|
parseSessionArgs ("--snapshot":id:rest) opts = parseSessionArgs rest opts { sessAction = SessionSnapshot id }
|
||||||
|
parseSessionArgs ("--restore":id:rest) opts = parseSessionArgs rest opts { sessAction = SessionRestore id }
|
||||||
parseSessionArgs ("--shell":sh:rest) opts = parseSessionArgs rest opts { sessShell = Just sh }
|
parseSessionArgs ("--shell":sh:rest) opts = parseSessionArgs rest opts { sessShell = Just sh }
|
||||||
parseSessionArgs ("-s":sh:rest) opts = parseSessionArgs rest opts { sessShell = Just sh }
|
parseSessionArgs ("-s":sh:rest) opts = parseSessionArgs rest opts { sessShell = Just sh }
|
||||||
|
parseSessionArgs ("--snapshot-name":n:rest) opts = parseSessionArgs rest opts { sessSnapshotName = Just n }
|
||||||
|
parseSessionArgs ("--from":f:rest) opts = parseSessionArgs rest opts { sessSnapshotFrom = Just f }
|
||||||
|
parseSessionArgs ("--hot":rest) opts = parseSessionArgs rest opts { sessHot = True }
|
||||||
parseSessionArgs ("-n":net:rest) opts = parseSessionArgs rest opts { sessNetwork = Just net }
|
parseSessionArgs ("-n":net:rest) opts = parseSessionArgs rest opts { sessNetwork = Just net }
|
||||||
parseSessionArgs ("-v":v:rest) opts = parseSessionArgs rest opts { sessVcpu = Just (read v) }
|
parseSessionArgs ("-v":v:rest) opts = parseSessionArgs rest opts { sessVcpu = Just (read v) }
|
||||||
parseSessionArgs ("-f":f:rest) opts = parseSessionArgs rest opts { sessFiles = sessFiles opts ++ [f] }
|
parseSessionArgs ("-f":f:rest) opts = parseSessionArgs rest opts { sessFiles = sessFiles opts ++ [f] }
|
||||||
|
|
@ -191,7 +229,7 @@ parseSession args = return $ parseSessionArgs args defaultSessionOpts
|
||||||
parseService :: [String] -> IO ServiceOpts
|
parseService :: [String] -> IO ServiceOpts
|
||||||
parseService args = return $ parseServiceArgs args defaultServiceOpts
|
parseService args = return $ parseServiceArgs args defaultServiceOpts
|
||||||
where
|
where
|
||||||
defaultServiceOpts = ServiceOpts ServiceCreate Nothing Nothing Nothing Nothing Nothing Nothing Nothing []
|
defaultServiceOpts = ServiceOpts ServiceCreate Nothing Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing False
|
||||||
parseServiceArgs [] opts = opts
|
parseServiceArgs [] opts = opts
|
||||||
parseServiceArgs ("--list":rest) opts = parseServiceArgs rest opts { svcAction = ServiceList }
|
parseServiceArgs ("--list":rest) opts = parseServiceArgs rest opts { svcAction = ServiceList }
|
||||||
parseServiceArgs ("--info":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceInfo id }
|
parseServiceArgs ("--info":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceInfo id }
|
||||||
|
|
@ -199,6 +237,8 @@ parseService args = return $ parseServiceArgs args defaultServiceOpts
|
||||||
parseServiceArgs ("--freeze":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceSleep id }
|
parseServiceArgs ("--freeze":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceSleep id }
|
||||||
parseServiceArgs ("--unfreeze":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceWake id }
|
parseServiceArgs ("--unfreeze":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceWake id }
|
||||||
parseServiceArgs ("--destroy":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDestroy id }
|
parseServiceArgs ("--destroy":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDestroy id }
|
||||||
|
parseServiceArgs ("--snapshot":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceSnapshot id }
|
||||||
|
parseServiceArgs ("--restore":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceRestore id }
|
||||||
parseServiceArgs ("--execute":id:"--command":cmd:rest) opts = parseServiceArgs rest opts { svcAction = ServiceExecute id cmd }
|
parseServiceArgs ("--execute":id:"--command":cmd:rest) opts = parseServiceArgs rest opts { svcAction = ServiceExecute id cmd }
|
||||||
parseServiceArgs ("--dump-bootstrap":id:file:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDumpBootstrap id (Just file) }
|
parseServiceArgs ("--dump-bootstrap":id:file:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDumpBootstrap id (Just file) }
|
||||||
parseServiceArgs ("--dump-bootstrap":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDumpBootstrap id Nothing }
|
parseServiceArgs ("--dump-bootstrap":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceDumpBootstrap id Nothing }
|
||||||
|
|
@ -207,6 +247,9 @@ parseService args = return $ parseServiceArgs args defaultServiceOpts
|
||||||
parseServiceArgs ("--type":t:rest) opts = parseServiceArgs rest opts { svcType = Just t }
|
parseServiceArgs ("--type":t:rest) opts = parseServiceArgs rest opts { svcType = Just t }
|
||||||
parseServiceArgs ("--bootstrap":b:rest) opts = parseServiceArgs rest opts { svcBootstrap = Just b }
|
parseServiceArgs ("--bootstrap":b:rest) opts = parseServiceArgs rest opts { svcBootstrap = Just b }
|
||||||
parseServiceArgs ("--bootstrap-file":f:rest) opts = parseServiceArgs rest opts { svcBootstrapFile = Just f }
|
parseServiceArgs ("--bootstrap-file":f:rest) opts = parseServiceArgs rest opts { svcBootstrapFile = Just f }
|
||||||
|
parseServiceArgs ("--snapshot-name":n:rest) opts = parseServiceArgs rest opts { svcSnapshotName = Just n }
|
||||||
|
parseServiceArgs ("--from":f:rest) opts = parseServiceArgs rest opts { svcSnapshotFrom = Just f }
|
||||||
|
parseServiceArgs ("--hot":rest) opts = parseServiceArgs rest opts { svcHot = True }
|
||||||
parseServiceArgs ("-n":net:rest) opts = parseServiceArgs rest opts { svcNetwork = Just net }
|
parseServiceArgs ("-n":net:rest) opts = parseServiceArgs rest opts { svcNetwork = Just net }
|
||||||
parseServiceArgs ("-v":v:rest) opts = parseServiceArgs rest opts { svcVcpu = Just (read v) }
|
parseServiceArgs ("-v":v:rest) opts = parseServiceArgs rest opts { svcVcpu = Just (read v) }
|
||||||
parseServiceArgs ("-f":f:rest) opts = parseServiceArgs rest opts { svcFiles = svcFiles opts ++ [f] }
|
parseServiceArgs ("-f":f:rest) opts = parseServiceArgs rest opts { svcFiles = svcFiles opts ++ [f] }
|
||||||
|
|
@ -246,6 +289,7 @@ main = do
|
||||||
Session opts -> sessionCommand opts
|
Session opts -> sessionCommand opts
|
||||||
Service opts -> serviceCommand opts
|
Service opts -> serviceCommand opts
|
||||||
Key opts -> keyCommand opts
|
Key opts -> keyCommand opts
|
||||||
|
Snapshot opts -> snapshotCommand opts
|
||||||
Help -> printHelp
|
Help -> printHelp
|
||||||
|
|
||||||
printHelp :: IO ()
|
printHelp :: IO ()
|
||||||
|
|
@ -254,6 +298,7 @@ printHelp = do
|
||||||
putStrLn " un.hs [options] <source_file> Execute code"
|
putStrLn " un.hs [options] <source_file> Execute code"
|
||||||
putStrLn " un.hs session [options] Manage sessions"
|
putStrLn " un.hs session [options] Manage sessions"
|
||||||
putStrLn " un.hs service [options] Manage services"
|
putStrLn " un.hs service [options] Manage services"
|
||||||
|
putStrLn " un.hs snapshot [options] Manage snapshots"
|
||||||
putStrLn " un.hs key [options] Validate/extend API key"
|
putStrLn " un.hs key [options] Validate/extend API key"
|
||||||
putStrLn ""
|
putStrLn ""
|
||||||
putStrLn "Execute options:"
|
putStrLn "Execute options:"
|
||||||
|
|
@ -264,6 +309,29 @@ printHelp = do
|
||||||
putStrLn " -n MODE Network mode (zerotrust|semitrusted)"
|
putStrLn " -n MODE Network mode (zerotrust|semitrusted)"
|
||||||
putStrLn " -v N vCPU count (1-8)"
|
putStrLn " -v N vCPU count (1-8)"
|
||||||
putStrLn ""
|
putStrLn ""
|
||||||
|
putStrLn "Session snapshot options:"
|
||||||
|
putStrLn " --snapshot ID Create snapshot of session"
|
||||||
|
putStrLn " --restore ID Restore session from snapshot"
|
||||||
|
putStrLn " --from SNAPSHOT_ID Snapshot ID to restore from"
|
||||||
|
putStrLn " --snapshot-name NAME Optional snapshot name"
|
||||||
|
putStrLn " --hot Take live snapshot without freezing"
|
||||||
|
putStrLn ""
|
||||||
|
putStrLn "Service snapshot options:"
|
||||||
|
putStrLn " --snapshot ID Create snapshot of service"
|
||||||
|
putStrLn " --restore ID Restore service from snapshot"
|
||||||
|
putStrLn " --from SNAPSHOT_ID Snapshot ID to restore from"
|
||||||
|
putStrLn " --snapshot-name NAME Optional snapshot name"
|
||||||
|
putStrLn " --hot Take live snapshot without freezing"
|
||||||
|
putStrLn ""
|
||||||
|
putStrLn "Snapshot management options:"
|
||||||
|
putStrLn " -l, --list List all snapshots"
|
||||||
|
putStrLn " --info ID Get snapshot details"
|
||||||
|
putStrLn " --delete ID Delete a snapshot"
|
||||||
|
putStrLn " --clone ID Clone snapshot to new session/service"
|
||||||
|
putStrLn " --type TYPE Type for clone (session|service)"
|
||||||
|
putStrLn " --name NAME Name for cloned instance"
|
||||||
|
putStrLn " --ports PORTS Ports for cloned service"
|
||||||
|
putStrLn ""
|
||||||
putStrLn "Key options:"
|
putStrLn "Key options:"
|
||||||
putStrLn " --extend Open browser to extend/renew key"
|
putStrLn " --extend Open browser to extend/renew key"
|
||||||
exitFailure
|
exitFailure
|
||||||
|
|
@ -322,6 +390,20 @@ sessionCommand opts = do
|
||||||
SessionKill sid -> do
|
SessionKill sid -> do
|
||||||
(_, stdout, _) <- curlDelete apiKey ("https://api.unsandbox.com/sessions/" ++ sid)
|
(_, stdout, _) <- curlDelete apiKey ("https://api.unsandbox.com/sessions/" ++ sid)
|
||||||
putStrLn $ green ++ "Session terminated: " ++ sid ++ reset
|
putStrLn $ green ++ "Session terminated: " ++ sid ++ reset
|
||||||
|
SessionSnapshot sid -> do
|
||||||
|
let nameJSON = maybe "" (\n -> "\"name\":\"" ++ escapeJSON n ++ "\",") (sessSnapshotName opts)
|
||||||
|
let hotJSON = if sessHot opts then "\"hot\":true" else "\"hot\":false"
|
||||||
|
let json = "{" ++ nameJSON ++ hotJSON ++ "}"
|
||||||
|
hPutStrLn stderr $ "Creating snapshot of session " ++ sid ++ "..."
|
||||||
|
(_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/sessions/" ++ sid ++ "/snapshot") json
|
||||||
|
putStrLn $ green ++ "Snapshot created" ++ reset
|
||||||
|
putStrLn stdout
|
||||||
|
SessionRestore snapshotId -> do
|
||||||
|
-- --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
hPutStrLn stderr $ "Restoring from snapshot " ++ snapshotId ++ "..."
|
||||||
|
(_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/snapshots/" ++ snapshotId ++ "/restore") "{}"
|
||||||
|
putStrLn $ green ++ "Session restored from snapshot" ++ reset
|
||||||
|
putStrLn stdout
|
||||||
SessionCreate -> do
|
SessionCreate -> do
|
||||||
let shell = maybe "bash" id (sessShell opts)
|
let shell = maybe "bash" id (sessShell opts)
|
||||||
let networkJSON = maybe "" (\n -> ",\"network\":\"" ++ n ++ "\"") (sessNetwork opts)
|
let networkJSON = maybe "" (\n -> ",\"network\":\"" ++ n ++ "\"") (sessNetwork opts)
|
||||||
|
|
@ -387,6 +469,20 @@ serviceCommand opts = do
|
||||||
_ -> do
|
_ -> do
|
||||||
hPutStrLn stderr $ red ++ "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" ++ reset
|
hPutStrLn stderr $ red ++ "Error: Failed to fetch bootstrap (service not running or no bootstrap file)" ++ reset
|
||||||
exitFailure
|
exitFailure
|
||||||
|
ServiceSnapshot sid -> do
|
||||||
|
let nameJSON = maybe "" (\n -> "\"name\":\"" ++ escapeJSON n ++ "\",") (svcSnapshotName opts)
|
||||||
|
let hotJSON = if svcHot opts then "\"hot\":true" else "\"hot\":false"
|
||||||
|
let json = "{" ++ nameJSON ++ hotJSON ++ "}"
|
||||||
|
hPutStrLn stderr $ "Creating snapshot of service " ++ sid ++ "..."
|
||||||
|
(_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/services/" ++ sid ++ "/snapshot") json
|
||||||
|
putStrLn $ green ++ "Snapshot created" ++ reset
|
||||||
|
putStrLn stdout
|
||||||
|
ServiceRestore snapshotId -> do
|
||||||
|
-- --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
hPutStrLn stderr $ "Restoring from snapshot " ++ snapshotId ++ "..."
|
||||||
|
(_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/snapshots/" ++ snapshotId ++ "/restore") "{}"
|
||||||
|
putStrLn $ green ++ "Service restored from snapshot" ++ reset
|
||||||
|
putStrLn stdout
|
||||||
ServiceCreate -> do
|
ServiceCreate -> do
|
||||||
case svcName opts of
|
case svcName opts of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
|
|
@ -555,6 +651,34 @@ extractJsonString json field =
|
||||||
tails [] = [[]]
|
tails [] = [[]]
|
||||||
tails s@(_:xs) = s : tails xs
|
tails s@(_:xs) = s : tails xs
|
||||||
|
|
||||||
|
-- Snapshot command
|
||||||
|
snapshotCommand :: SnapshotOpts -> IO ()
|
||||||
|
snapshotCommand opts = do
|
||||||
|
apiKey <- getApiKey
|
||||||
|
case snapAction opts of
|
||||||
|
SnapshotList -> do
|
||||||
|
(_, stdout, _) <- curlGet apiKey "https://api.unsandbox.com/snapshots"
|
||||||
|
putStrLn stdout
|
||||||
|
SnapshotInfo sid -> do
|
||||||
|
(_, stdout, _) <- curlGet apiKey ("https://api.unsandbox.com/snapshots/" ++ sid)
|
||||||
|
putStrLn stdout
|
||||||
|
SnapshotDelete sid -> do
|
||||||
|
(_, stdout, _) <- curlDelete apiKey ("https://api.unsandbox.com/snapshots/" ++ sid)
|
||||||
|
putStrLn $ green ++ "Snapshot deleted: " ++ sid ++ reset
|
||||||
|
SnapshotClone sid -> do
|
||||||
|
case snapCloneType opts of
|
||||||
|
Nothing -> do
|
||||||
|
hPutStrLn stderr "Error: --type (session|service) required for clone"
|
||||||
|
exitFailure
|
||||||
|
Just cloneType -> do
|
||||||
|
let nameJSON = maybe "" (\n -> "\"name\":\"" ++ escapeJSON n ++ "\",") (snapCloneName opts)
|
||||||
|
let portsJSON = maybe "" (\p -> "\"ports\":[" ++ p ++ "],") (snapClonePorts opts)
|
||||||
|
let json = "{\"type\":\"" ++ cloneType ++ "\"," ++ nameJSON ++ portsJSON ++ "}"
|
||||||
|
hPutStrLn stderr $ "Cloning snapshot " ++ sid ++ " to create new " ++ cloneType ++ "..."
|
||||||
|
(_, stdout, _) <- curlPost apiKey ("https://api.unsandbox.com/snapshots/" ++ sid ++ "/clone") json
|
||||||
|
putStrLn $ green ++ "Snapshot cloned" ++ reset
|
||||||
|
putStrLn stdout
|
||||||
|
|
||||||
-- Key command
|
-- Key command
|
||||||
keyCommand :: KeyOpts -> IO ()
|
keyCommand :: KeyOpts -> IO ()
|
||||||
keyCommand opts = do
|
keyCommand opts = do
|
||||||
|
|
|
||||||
153
un.m
153
un.m
|
|
@ -454,6 +454,11 @@ void cmdSession(NSArray* args) {
|
||||||
NSString* network = nil;
|
NSString* network = nil;
|
||||||
int vcpu = 0;
|
int vcpu = 0;
|
||||||
NSMutableArray* inputFiles = [NSMutableArray array];
|
NSMutableArray* inputFiles = [NSMutableArray array];
|
||||||
|
NSString* snapshotId = nil;
|
||||||
|
NSString* restoreId = nil;
|
||||||
|
NSString* fromSnapshot = nil;
|
||||||
|
NSString* snapshotName = nil;
|
||||||
|
BOOL hotSnapshot = NO;
|
||||||
|
|
||||||
// Parse arguments
|
// Parse arguments
|
||||||
for (NSUInteger i = 0; i < [args count]; i++) {
|
for (NSUInteger i = 0; i < [args count]; i++) {
|
||||||
|
|
@ -462,6 +467,16 @@ void cmdSession(NSArray* args) {
|
||||||
listMode = YES;
|
listMode = YES;
|
||||||
} else if ([arg isEqualToString:@"--kill"] && i + 1 < [args count]) {
|
} else if ([arg isEqualToString:@"--kill"] && i + 1 < [args count]) {
|
||||||
killId = args[++i];
|
killId = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--snapshot"] && i + 1 < [args count]) {
|
||||||
|
snapshotId = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--restore"] && i + 1 < [args count]) {
|
||||||
|
restoreId = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--from"] && i + 1 < [args count]) {
|
||||||
|
fromSnapshot = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--snapshot-name"] && i + 1 < [args count]) {
|
||||||
|
snapshotName = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--hot"]) {
|
||||||
|
hotSnapshot = YES;
|
||||||
} else if ([arg isEqualToString:@"--shell"] && i + 1 < [args count]) {
|
} else if ([arg isEqualToString:@"--shell"] && i + 1 < [args count]) {
|
||||||
shell = args[++i];
|
shell = args[++i];
|
||||||
} else if ([arg isEqualToString:@"-f"] && i + 1 < [args count]) {
|
} else if ([arg isEqualToString:@"-f"] && i + 1 < [args count]) {
|
||||||
|
|
@ -498,6 +513,26 @@ void cmdSession(NSArray* args) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (snapshotId) {
|
||||||
|
fprintf(stderr, "Creating snapshot of session %s...\n", [snapshotId UTF8String]);
|
||||||
|
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
|
||||||
|
if (snapshotName) payload[@"name"] = snapshotName;
|
||||||
|
if (hotSnapshot) payload[@"hot"] = @YES;
|
||||||
|
NSString* endpoint = [NSString stringWithFormat:@"/sessions/%@/snapshot", snapshotId];
|
||||||
|
NSDictionary* result = apiRequest(endpoint, @"POST", payload, publicKey, secretKey);
|
||||||
|
printf("%sSnapshot created: %s%s\n", [GREEN UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restoreId) {
|
||||||
|
// --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
fprintf(stderr, "Restoring from snapshot %s...\n", [restoreId UTF8String]);
|
||||||
|
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/restore", restoreId];
|
||||||
|
apiRequest(endpoint, @"POST", @{}, publicKey, secretKey);
|
||||||
|
printf("%sSession restored from snapshot%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create new session
|
// Create new session
|
||||||
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
|
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
|
||||||
@"shell": shell ?: @"bash"
|
@"shell": shell ?: @"bash"
|
||||||
|
|
@ -551,6 +586,11 @@ void cmdService(NSArray* args) {
|
||||||
NSString* network = nil;
|
NSString* network = nil;
|
||||||
int vcpu = 0;
|
int vcpu = 0;
|
||||||
NSMutableArray* inputFiles = [NSMutableArray array];
|
NSMutableArray* inputFiles = [NSMutableArray array];
|
||||||
|
NSString* snapshotId = nil;
|
||||||
|
NSString* restoreId = nil;
|
||||||
|
NSString* fromSnapshot = nil;
|
||||||
|
NSString* snapshotName = nil;
|
||||||
|
BOOL hotSnapshot = NO;
|
||||||
|
|
||||||
// Parse arguments
|
// Parse arguments
|
||||||
for (NSUInteger i = 0; i < [args count]; i++) {
|
for (NSUInteger i = 0; i < [args count]; i++) {
|
||||||
|
|
@ -571,6 +611,16 @@ void cmdService(NSArray* args) {
|
||||||
dumpBootstrapId = args[++i];
|
dumpBootstrapId = args[++i];
|
||||||
} else if ([arg isEqualToString:@"--dump-file"] && i + 1 < [args count]) {
|
} else if ([arg isEqualToString:@"--dump-file"] && i + 1 < [args count]) {
|
||||||
dumpFile = args[++i];
|
dumpFile = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--snapshot"] && i + 1 < [args count]) {
|
||||||
|
snapshotId = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--restore"] && i + 1 < [args count]) {
|
||||||
|
restoreId = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--from"] && i + 1 < [args count]) {
|
||||||
|
fromSnapshot = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--snapshot-name"] && i + 1 < [args count]) {
|
||||||
|
snapshotName = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--hot"]) {
|
||||||
|
hotSnapshot = YES;
|
||||||
} else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) {
|
} else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) {
|
||||||
name = args[++i];
|
name = args[++i];
|
||||||
} else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) {
|
} else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) {
|
||||||
|
|
@ -683,6 +733,26 @@ void cmdService(NSArray* args) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (snapshotId) {
|
||||||
|
fprintf(stderr, "Creating snapshot of service %s...\n", [snapshotId UTF8String]);
|
||||||
|
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
|
||||||
|
if (snapshotName) payload[@"name"] = snapshotName;
|
||||||
|
if (hotSnapshot) payload[@"hot"] = @YES;
|
||||||
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/snapshot", snapshotId];
|
||||||
|
NSDictionary* result = apiRequest(endpoint, @"POST", payload, publicKey, secretKey);
|
||||||
|
printf("%sSnapshot created: %s%s\n", [GREEN UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restoreId) {
|
||||||
|
// --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
fprintf(stderr, "Restoring from snapshot %s...\n", [restoreId UTF8String]);
|
||||||
|
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/restore", restoreId];
|
||||||
|
apiRequest(endpoint, @"POST", @{}, publicKey, secretKey);
|
||||||
|
printf("%sService restored from snapshot%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create new service
|
// Create new service
|
||||||
if (name) {
|
if (name) {
|
||||||
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{@"name": name}];
|
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{@"name": name}];
|
||||||
|
|
@ -753,12 +823,93 @@ void cmdService(NSArray* args) {
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void cmdSnapshot(NSArray* args) {
|
||||||
|
NSString* publicKey, *secretKey;
|
||||||
|
getApiKeys(&publicKey, &secretKey);
|
||||||
|
BOOL listMode = NO;
|
||||||
|
NSString* infoId = nil;
|
||||||
|
NSString* deleteId = nil;
|
||||||
|
NSString* cloneId = nil;
|
||||||
|
NSString* cloneType = nil;
|
||||||
|
NSString* cloneName = nil;
|
||||||
|
|
||||||
|
for (NSUInteger i = 0; i < [args count]; i++) {
|
||||||
|
NSString* arg = args[i];
|
||||||
|
if ([arg isEqualToString:@"--list"] || [arg isEqualToString:@"-l"]) {
|
||||||
|
listMode = YES;
|
||||||
|
} else if ([arg isEqualToString:@"--info"] && i + 1 < [args count]) {
|
||||||
|
infoId = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--delete"] && i + 1 < [args count]) {
|
||||||
|
deleteId = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--clone"] && i + 1 < [args count]) {
|
||||||
|
cloneId = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--type"] && i + 1 < [args count]) {
|
||||||
|
cloneType = args[++i];
|
||||||
|
} else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) {
|
||||||
|
cloneName = args[++i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listMode) {
|
||||||
|
NSDictionary* result = apiRequest(@"/snapshots", @"GET", nil, publicKey, secretKey);
|
||||||
|
NSArray* snapshots = result[@"snapshots"];
|
||||||
|
if ([snapshots count] == 0) {
|
||||||
|
printf("No snapshots found\n");
|
||||||
|
} else {
|
||||||
|
printf("%-40s %-20s %-12s %-30s\n", "SNAPSHOT ID", "NAME", "SOURCE TYPE", "SOURCE ID");
|
||||||
|
for (NSDictionary* s in snapshots) {
|
||||||
|
printf("%-40s %-20s %-12s %-30s\n",
|
||||||
|
[s[@"id"] UTF8String],
|
||||||
|
[s[@"name"] UTF8String] ?: "-",
|
||||||
|
[s[@"source_type"] UTF8String],
|
||||||
|
[s[@"source_id"] UTF8String]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infoId) {
|
||||||
|
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@", infoId];
|
||||||
|
NSDictionary* result = apiRequest(endpoint, @"GET", nil, publicKey, secretKey);
|
||||||
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
||||||
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||||
|
printf("%s\n", [jsonString UTF8String]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteId) {
|
||||||
|
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@", deleteId];
|
||||||
|
apiRequest(endpoint, @"DELETE", nil, publicKey, secretKey);
|
||||||
|
printf("%sSnapshot deleted successfully%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cloneId) {
|
||||||
|
if (!cloneType) {
|
||||||
|
fprintf(stderr, "%sError: --type required with --clone (session or service)%s\n", [RED UTF8String], [RESET UTF8String]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{@"type": cloneType}];
|
||||||
|
if (cloneName) payload[@"name"] = cloneName;
|
||||||
|
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/clone", cloneId];
|
||||||
|
NSDictionary* result = apiRequest(endpoint, @"POST", payload, publicKey, secretKey);
|
||||||
|
printf("%s%s created from snapshot: %s%s\n", [GREEN UTF8String],
|
||||||
|
[cloneType UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "%sError: No snapshot action specified. Use --list, --info, --delete, or --clone%s\n",
|
||||||
|
[RED UTF8String], [RESET UTF8String]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, const char* argv[]) {
|
int main(int argc, const char* argv[]) {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
if (argc < 2) {
|
if (argc < 2) {
|
||||||
fprintf(stderr, "Usage: un.m [options] <source_file>\n");
|
fprintf(stderr, "Usage: un.m [options] <source_file>\n");
|
||||||
fprintf(stderr, " un.m session [options]\n");
|
fprintf(stderr, " un.m session [options]\n");
|
||||||
fprintf(stderr, " un.m service [options]\n");
|
fprintf(stderr, " un.m service [options]\n");
|
||||||
|
fprintf(stderr, " un.m snapshot [options]\n");
|
||||||
fprintf(stderr, " un.m key [options]\n");
|
fprintf(stderr, " un.m key [options]\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
@ -774,6 +925,8 @@ int main(int argc, const char* argv[]) {
|
||||||
cmdSession([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
cmdSession([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
||||||
} else if ([firstArg isEqualToString:@"service"]) {
|
} else if ([firstArg isEqualToString:@"service"]) {
|
||||||
cmdService([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
cmdService([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
||||||
|
} else if ([firstArg isEqualToString:@"snapshot"]) {
|
||||||
|
cmdSnapshot([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
||||||
} else if ([firstArg isEqualToString:@"key"]) {
|
} else if ([firstArg isEqualToString:@"key"]) {
|
||||||
cmdKey([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
cmdKey([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
128
un.py
128
un.py
|
|
@ -296,6 +296,28 @@ def cmd_session(args):
|
||||||
print(f"{GREEN}Session terminated: {args.kill}{RESET}")
|
print(f"{GREEN}Session terminated: {args.kill}{RESET}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if args.snapshot:
|
||||||
|
payload = {}
|
||||||
|
if args.snapshot_name:
|
||||||
|
payload["name"] = args.snapshot_name
|
||||||
|
if args.hot:
|
||||||
|
payload["hot"] = True
|
||||||
|
|
||||||
|
print(f"{YELLOW}Creating snapshot of session {args.snapshot}...{RESET}", file=sys.stderr)
|
||||||
|
result = api_request(f"/sessions/{args.snapshot}/snapshot", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
|
||||||
|
print(f"{GREEN}Snapshot created successfully{RESET}")
|
||||||
|
print(f"Snapshot ID: {result.get('id', 'N/A')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.restore:
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
print(f"{YELLOW}Restoring from snapshot {args.restore}...{RESET}", file=sys.stderr)
|
||||||
|
result = api_request(f"/snapshots/{args.restore}/restore", method="POST", public_key=public_key, secret_key=secret_key)
|
||||||
|
print(f"{GREEN}Session restored from snapshot{RESET}")
|
||||||
|
if result.get('session_id'):
|
||||||
|
print(f"New session ID: {result.get('session_id')}")
|
||||||
|
return
|
||||||
|
|
||||||
if args.attach:
|
if args.attach:
|
||||||
print(f"{YELLOW}Attaching to session {args.attach}...{RESET}")
|
print(f"{YELLOW}Attaching to session {args.attach}...{RESET}")
|
||||||
print(f"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}")
|
print(f"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}")
|
||||||
|
|
@ -420,6 +442,68 @@ def cmd_key(args):
|
||||||
validate_key(public_key, secret_key, extend=args.extend)
|
validate_key(public_key, secret_key, extend=args.extend)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_snapshot(args):
|
||||||
|
"""Manage snapshots"""
|
||||||
|
public_key, secret_key = get_api_keys(args.api_key)
|
||||||
|
|
||||||
|
if args.list:
|
||||||
|
result = api_request("/snapshots", public_key=public_key, secret_key=secret_key)
|
||||||
|
snapshots = result.get("snapshots", [])
|
||||||
|
if not snapshots:
|
||||||
|
print("No snapshots found")
|
||||||
|
else:
|
||||||
|
print(f"{'ID':<40} {'Name':<20} {'Type':<12} {'Source ID':<30} {'Size':<10}")
|
||||||
|
for s in snapshots:
|
||||||
|
print(f"{s.get('id', 'N/A'):<40} {s.get('name', '-'):<20} {s.get('source_type', 'N/A'):<12} {s.get('source_id', 'N/A'):<30} {s.get('size', 'N/A'):<10}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.info:
|
||||||
|
result = api_request(f"/snapshots/{args.info}", public_key=public_key, secret_key=secret_key)
|
||||||
|
print(f"{BLUE}Snapshot Details{RESET}\n")
|
||||||
|
print(f"Snapshot ID: {result.get('id', 'N/A')}")
|
||||||
|
print(f"Name: {result.get('name', '-')}")
|
||||||
|
print(f"Source Type: {result.get('source_type', 'N/A')}")
|
||||||
|
print(f"Source ID: {result.get('source_id', 'N/A')}")
|
||||||
|
print(f"Size: {result.get('size', 'N/A')}")
|
||||||
|
print(f"Created: {result.get('created_at', 'N/A')}")
|
||||||
|
print(f"Hot Snapshot: {result.get('hot', 'N/A')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.delete:
|
||||||
|
result = api_request(f"/snapshots/{args.delete}", method="DELETE", public_key=public_key, secret_key=secret_key)
|
||||||
|
print(f"{GREEN}Snapshot deleted successfully{RESET}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.clone:
|
||||||
|
if not args.type:
|
||||||
|
print(f"{RED}Error: --type required for --clone (session or service){RESET}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.type not in ["session", "service"]:
|
||||||
|
print(f"{RED}Error: --type must be 'session' or 'service'{RESET}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
payload = {"type": args.type}
|
||||||
|
if args.name:
|
||||||
|
payload["name"] = args.name
|
||||||
|
if args.shell:
|
||||||
|
payload["shell"] = args.shell
|
||||||
|
if args.ports:
|
||||||
|
payload["ports"] = [int(p) for p in args.ports.split(',')]
|
||||||
|
|
||||||
|
result = api_request(f"/snapshots/{args.clone}/clone", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
|
||||||
|
|
||||||
|
if args.type == "session":
|
||||||
|
print(f"{GREEN}Session created from snapshot{RESET}")
|
||||||
|
print(f"Session ID: {result.get('id', 'N/A')}")
|
||||||
|
else:
|
||||||
|
print(f"{GREEN}Service created from snapshot{RESET}")
|
||||||
|
print(f"Service ID: {result.get('id', 'N/A')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"{RED}Error: Specify --list, --info ID, --delete ID, or --clone ID --type TYPE{RESET}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def cmd_service(args):
|
def cmd_service(args):
|
||||||
"""Manage persistent services"""
|
"""Manage persistent services"""
|
||||||
public_key, secret_key = get_api_keys(args.api_key)
|
public_key, secret_key = get_api_keys(args.api_key)
|
||||||
|
|
@ -467,6 +551,28 @@ def cmd_service(args):
|
||||||
print(f"{GREEN}Service destroyed: {args.destroy}{RESET}")
|
print(f"{GREEN}Service destroyed: {args.destroy}{RESET}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if args.snapshot:
|
||||||
|
payload = {}
|
||||||
|
if args.snapshot_name:
|
||||||
|
payload["name"] = args.snapshot_name
|
||||||
|
if args.hot:
|
||||||
|
payload["hot"] = True
|
||||||
|
|
||||||
|
print(f"{YELLOW}Creating snapshot of service {args.snapshot}...{RESET}", file=sys.stderr)
|
||||||
|
result = api_request(f"/services/{args.snapshot}/snapshot", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
|
||||||
|
print(f"{GREEN}Snapshot created successfully{RESET}")
|
||||||
|
print(f"Snapshot ID: {result.get('id', 'N/A')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.restore:
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
print(f"{YELLOW}Restoring from snapshot {args.restore}...{RESET}", file=sys.stderr)
|
||||||
|
result = api_request(f"/snapshots/{args.restore}/restore", method="POST", public_key=public_key, secret_key=secret_key)
|
||||||
|
print(f"{GREEN}Service restored from snapshot{RESET}")
|
||||||
|
if result.get('service_id'):
|
||||||
|
print(f"New service ID: {result.get('service_id')}")
|
||||||
|
return
|
||||||
|
|
||||||
if args.execute:
|
if args.execute:
|
||||||
payload = {"command": args.command}
|
payload = {"command": args.command}
|
||||||
result = api_request(f"/services/{args.execute}/execute", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
|
result = api_request(f"/services/{args.execute}/execute", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
|
||||||
|
|
@ -586,6 +692,10 @@ Examples:
|
||||||
session_parser.add_argument("-l", "--list", action="store_true", help="List active sessions")
|
session_parser.add_argument("-l", "--list", action="store_true", help="List active sessions")
|
||||||
session_parser.add_argument("--attach", metavar="ID", help="Reconnect to session")
|
session_parser.add_argument("--attach", metavar="ID", help="Reconnect to session")
|
||||||
session_parser.add_argument("--kill", metavar="ID", help="Terminate session")
|
session_parser.add_argument("--kill", metavar="ID", help="Terminate session")
|
||||||
|
session_parser.add_argument("--snapshot", metavar="SESSION_ID", help="Create snapshot of session")
|
||||||
|
session_parser.add_argument("--restore", metavar="SNAPSHOT_ID", help="Restore from snapshot ID")
|
||||||
|
session_parser.add_argument("--snapshot-name", metavar="NAME", help="Name for snapshot")
|
||||||
|
session_parser.add_argument("--hot", action="store_true", help="Hot snapshot (no freeze)")
|
||||||
session_parser.add_argument("--audit", action="store_true", help="Record session")
|
session_parser.add_argument("--audit", action="store_true", help="Record session")
|
||||||
session_parser.add_argument("--tmux", action="store_true", help="Enable tmux persistence")
|
session_parser.add_argument("--tmux", action="store_true", help="Enable tmux persistence")
|
||||||
session_parser.add_argument("--screen", action="store_true", help="Enable screen persistence")
|
session_parser.add_argument("--screen", action="store_true", help="Enable screen persistence")
|
||||||
|
|
@ -610,6 +720,10 @@ Examples:
|
||||||
service_parser.add_argument("--freeze", metavar="ID", help="Freeze service")
|
service_parser.add_argument("--freeze", metavar="ID", help="Freeze service")
|
||||||
service_parser.add_argument("--unfreeze", metavar="ID", help="Unfreeze service")
|
service_parser.add_argument("--unfreeze", metavar="ID", help="Unfreeze service")
|
||||||
service_parser.add_argument("--destroy", metavar="ID", help="Destroy service")
|
service_parser.add_argument("--destroy", metavar="ID", help="Destroy service")
|
||||||
|
service_parser.add_argument("--snapshot", metavar="SERVICE_ID", help="Create snapshot of service")
|
||||||
|
service_parser.add_argument("--restore", metavar="SNAPSHOT_ID", help="Restore from snapshot ID")
|
||||||
|
service_parser.add_argument("--snapshot-name", metavar="NAME", help="Name for snapshot")
|
||||||
|
service_parser.add_argument("--hot", action="store_true", help="Hot snapshot (no freeze)")
|
||||||
service_parser.add_argument("--execute", metavar="ID", help="Execute command in service")
|
service_parser.add_argument("--execute", metavar="ID", help="Execute command in service")
|
||||||
service_parser.add_argument("--command", help="Command to execute (with --execute)")
|
service_parser.add_argument("--command", help="Command to execute (with --execute)")
|
||||||
service_parser.add_argument("--dump-bootstrap", metavar="ID", help="Dump bootstrap script")
|
service_parser.add_argument("--dump-bootstrap", metavar="ID", help="Dump bootstrap script")
|
||||||
|
|
@ -618,6 +732,18 @@ Examples:
|
||||||
service_parser.add_argument("-v", "--vcpu", type=int, choices=range(1, 9))
|
service_parser.add_argument("-v", "--vcpu", type=int, choices=range(1, 9))
|
||||||
service_parser.add_argument("-k", "--api-key")
|
service_parser.add_argument("-k", "--api-key")
|
||||||
|
|
||||||
|
# Snapshot subcommand
|
||||||
|
snapshot_parser = subparsers.add_parser("snapshot", help="Manage container snapshots")
|
||||||
|
snapshot_parser.add_argument("-l", "--list", action="store_true", help="List all snapshots")
|
||||||
|
snapshot_parser.add_argument("--info", metavar="ID", help="Get snapshot details")
|
||||||
|
snapshot_parser.add_argument("--delete", metavar="ID", help="Delete a snapshot")
|
||||||
|
snapshot_parser.add_argument("--clone", metavar="ID", help="Clone snapshot to new session/service")
|
||||||
|
snapshot_parser.add_argument("--type", help="Type for clone (session or service)")
|
||||||
|
snapshot_parser.add_argument("--name", help="Name for cloned session/service")
|
||||||
|
snapshot_parser.add_argument("--shell", help="Shell for cloned session")
|
||||||
|
snapshot_parser.add_argument("--ports", help="Ports for cloned service")
|
||||||
|
snapshot_parser.add_argument("-k", "--api-key")
|
||||||
|
|
||||||
# Execute options (default command)
|
# Execute options (default command)
|
||||||
parser.add_argument("source_file", nargs="?", help="Source file to execute")
|
parser.add_argument("source_file", nargs="?", help="Source file to execute")
|
||||||
parser.add_argument("-e", "--env", action="append", metavar="KEY=VALUE", help="Set environment variable")
|
parser.add_argument("-e", "--env", action="append", metavar="KEY=VALUE", help="Set environment variable")
|
||||||
|
|
@ -634,6 +760,8 @@ Examples:
|
||||||
cmd_session(args)
|
cmd_session(args)
|
||||||
elif args.command == "service":
|
elif args.command == "service":
|
||||||
cmd_service(args)
|
cmd_service(args)
|
||||||
|
elif args.command == "snapshot":
|
||||||
|
cmd_snapshot(args)
|
||||||
elif args.source_file:
|
elif args.source_file:
|
||||||
cmd_execute(args)
|
cmd_execute(args)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
185
un.r
185
un.r
|
|
@ -281,6 +281,30 @@ cmd_session <- function(args) {
|
||||||
return()
|
return()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!is.null(args$snapshot_id)) {
|
||||||
|
payload <- list()
|
||||||
|
if (!is.null(args$snapshot_name)) {
|
||||||
|
payload$name <- args$snapshot_name
|
||||||
|
}
|
||||||
|
if (!is.null(args$hot) && args$hot) {
|
||||||
|
payload$hot <- TRUE
|
||||||
|
}
|
||||||
|
|
||||||
|
cat(sprintf("%sCreating snapshot of session %s...%s\n", YELLOW, args$snapshot_id, RESET), file = stderr())
|
||||||
|
result <- api_request(paste0("/sessions/", args$snapshot_id, "/snapshot"), public_key, secret_key, method = "POST", data = payload)
|
||||||
|
cat(sprintf("%sSnapshot created successfully%s\n", GREEN, RESET))
|
||||||
|
cat(sprintf("Snapshot ID: %s\n", if (!is.null(result$id)) result$id else "N/A"))
|
||||||
|
return()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is.null(args$restore_id)) {
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
cat(sprintf("%sRestoring from snapshot %s...%s\n", YELLOW, args$restore_id, RESET), file = stderr())
|
||||||
|
result <- api_request(paste0("/snapshots/", args$restore_id, "/restore"), public_key, secret_key, method = "POST", data = list())
|
||||||
|
cat(sprintf("%sSession restored from snapshot%s\n", GREEN, RESET))
|
||||||
|
return()
|
||||||
|
}
|
||||||
|
|
||||||
# Create new session
|
# Create new session
|
||||||
payload <- list(shell = "bash")
|
payload <- list(shell = "bash")
|
||||||
|
|
||||||
|
|
@ -412,6 +436,86 @@ cmd_key <- function(args) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd_snapshot <- function(args) {
|
||||||
|
keys <- get_api_keys(args$api_key)
|
||||||
|
public_key <- keys$public_key
|
||||||
|
secret_key <- keys$secret_key
|
||||||
|
|
||||||
|
if (!is.null(args$list) && args$list) {
|
||||||
|
result <- api_request("/snapshots", public_key, secret_key)
|
||||||
|
snapshots <- if (!is.null(result$snapshots)) result$snapshots else list()
|
||||||
|
if (length(snapshots) == 0) {
|
||||||
|
cat("No snapshots found\n")
|
||||||
|
} else {
|
||||||
|
cat(sprintf("%-40s %-20s %-12s %-30s %s\n", "ID", "Name", "Type", "Source ID", "Size"))
|
||||||
|
for (s in snapshots) {
|
||||||
|
cat(sprintf("%-40s %-20s %-12s %-30s %s\n",
|
||||||
|
if (!is.null(s$id)) s$id else "N/A",
|
||||||
|
if (!is.null(s$name)) s$name else "-",
|
||||||
|
if (!is.null(s$source_type)) s$source_type else "N/A",
|
||||||
|
if (!is.null(s$source_id)) s$source_id else "N/A",
|
||||||
|
if (!is.null(s$size)) s$size else "N/A"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is.null(args$info)) {
|
||||||
|
result <- api_request(paste0("/snapshots/", args$info), public_key, secret_key)
|
||||||
|
cat(sprintf("%sSnapshot Details%s\n\n", BLUE, RESET))
|
||||||
|
cat(sprintf("Snapshot ID: %s\n", if (!is.null(result$id)) result$id else "N/A"))
|
||||||
|
cat(sprintf("Name: %s\n", if (!is.null(result$name)) result$name else "-"))
|
||||||
|
cat(sprintf("Source Type: %s\n", if (!is.null(result$source_type)) result$source_type else "N/A"))
|
||||||
|
cat(sprintf("Source ID: %s\n", if (!is.null(result$source_id)) result$source_id else "N/A"))
|
||||||
|
cat(sprintf("Size: %s\n", if (!is.null(result$size)) result$size else "N/A"))
|
||||||
|
cat(sprintf("Created: %s\n", if (!is.null(result$created_at)) result$created_at else "N/A"))
|
||||||
|
return()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is.null(args$delete)) {
|
||||||
|
result <- api_request(paste0("/snapshots/", args$delete), public_key, secret_key, method = "DELETE")
|
||||||
|
cat(sprintf("%sSnapshot deleted successfully%s\n", GREEN, RESET))
|
||||||
|
return()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is.null(args$clone)) {
|
||||||
|
if (is.null(args$type)) {
|
||||||
|
cat(sprintf("%sError: --type required for --clone (session or service)%s\n", RED, RESET), file = stderr())
|
||||||
|
quit(status = 1)
|
||||||
|
}
|
||||||
|
if (!(args$type %in% c("session", "service"))) {
|
||||||
|
cat(sprintf("%sError: --type must be 'session' or 'service'%s\n", RED, RESET), file = stderr())
|
||||||
|
quit(status = 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload <- list(type = args$type)
|
||||||
|
if (!is.null(args$clone_name)) {
|
||||||
|
payload$name <- args$clone_name
|
||||||
|
}
|
||||||
|
if (!is.null(args$shell)) {
|
||||||
|
payload$shell <- args$shell
|
||||||
|
}
|
||||||
|
if (!is.null(args$ports)) {
|
||||||
|
ports_vec <- as.integer(strsplit(args$ports, ",")[[1]])
|
||||||
|
payload$ports <- ports_vec
|
||||||
|
}
|
||||||
|
|
||||||
|
result <- api_request(paste0("/snapshots/", args$clone, "/clone"), public_key, secret_key, method = "POST", data = payload)
|
||||||
|
|
||||||
|
if (args$type == "session") {
|
||||||
|
cat(sprintf("%sSession created from snapshot%s\n", GREEN, RESET))
|
||||||
|
cat(sprintf("Session ID: %s\n", if (!is.null(result$id)) result$id else "N/A"))
|
||||||
|
} else {
|
||||||
|
cat(sprintf("%sService created from snapshot%s\n", GREEN, RESET))
|
||||||
|
cat(sprintf("Service ID: %s\n", if (!is.null(result$id)) result$id else "N/A"))
|
||||||
|
}
|
||||||
|
return()
|
||||||
|
}
|
||||||
|
|
||||||
|
cat(sprintf("%sError: Specify --list, --info ID, --delete ID, or --clone ID --type TYPE%s\n", RED, RESET), file = stderr())
|
||||||
|
quit(status = 1)
|
||||||
|
}
|
||||||
|
|
||||||
cmd_service <- function(args) {
|
cmd_service <- function(args) {
|
||||||
keys <- get_api_keys(args$api_key)
|
keys <- get_api_keys(args$api_key)
|
||||||
public_key <- keys$public_key
|
public_key <- keys$public_key
|
||||||
|
|
@ -467,6 +571,30 @@ cmd_service <- function(args) {
|
||||||
return()
|
return()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!is.null(args$snapshot_svc)) {
|
||||||
|
payload <- list()
|
||||||
|
if (!is.null(args$snapshot_name)) {
|
||||||
|
payload$name <- args$snapshot_name
|
||||||
|
}
|
||||||
|
if (!is.null(args$hot) && args$hot) {
|
||||||
|
payload$hot <- TRUE
|
||||||
|
}
|
||||||
|
|
||||||
|
cat(sprintf("%sCreating snapshot of service %s...%s\n", YELLOW, args$snapshot_svc, RESET), file = stderr())
|
||||||
|
result <- api_request(paste0("/services/", args$snapshot_svc, "/snapshot"), public_key, secret_key, method = "POST", data = payload)
|
||||||
|
cat(sprintf("%sSnapshot created successfully%s\n", GREEN, RESET))
|
||||||
|
cat(sprintf("Snapshot ID: %s\n", if (!is.null(result$id)) result$id else "N/A"))
|
||||||
|
return()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is.null(args$restore_svc)) {
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
cat(sprintf("%sRestoring from snapshot %s...%s\n", YELLOW, args$restore_svc, RESET), file = stderr())
|
||||||
|
result <- api_request(paste0("/snapshots/", args$restore_svc, "/restore"), public_key, secret_key, method = "POST", data = list())
|
||||||
|
cat(sprintf("%sService restored from snapshot%s\n", GREEN, RESET))
|
||||||
|
return()
|
||||||
|
}
|
||||||
|
|
||||||
if (!is.null(args$dump_bootstrap)) {
|
if (!is.null(args$dump_bootstrap)) {
|
||||||
cat(sprintf("Fetching bootstrap script from %s...\n", args$dump_bootstrap), file = stderr())
|
cat(sprintf("Fetching bootstrap script from %s...\n", args$dump_bootstrap), file = stderr())
|
||||||
payload <- list(command = "cat /tmp/bootstrap.sh")
|
payload <- list(command = "cat /tmp/bootstrap.sh")
|
||||||
|
|
@ -579,11 +707,22 @@ parse_args <- function() {
|
||||||
command = NULL,
|
command = NULL,
|
||||||
list = FALSE,
|
list = FALSE,
|
||||||
kill = NULL,
|
kill = NULL,
|
||||||
|
snapshot_id = NULL,
|
||||||
|
snapshot_svc = NULL,
|
||||||
|
restore_id = NULL,
|
||||||
|
restore_svc = NULL,
|
||||||
|
from_snapshot = NULL,
|
||||||
|
snapshot_name = NULL,
|
||||||
|
hot = FALSE,
|
||||||
info = NULL,
|
info = NULL,
|
||||||
logs = NULL,
|
logs = NULL,
|
||||||
sleep = NULL,
|
sleep = NULL,
|
||||||
wake = NULL,
|
wake = NULL,
|
||||||
destroy = NULL,
|
destroy = NULL,
|
||||||
|
delete = NULL,
|
||||||
|
clone = NULL,
|
||||||
|
clone_name = NULL,
|
||||||
|
shell = NULL,
|
||||||
dump_bootstrap = NULL,
|
dump_bootstrap = NULL,
|
||||||
dump_file = NULL,
|
dump_file = NULL,
|
||||||
name = NULL,
|
name = NULL,
|
||||||
|
|
@ -609,6 +748,9 @@ parse_args <- function() {
|
||||||
} else if (arg == "key") {
|
} else if (arg == "key") {
|
||||||
result$command <- "key"
|
result$command <- "key"
|
||||||
i <- i + 1
|
i <- i + 1
|
||||||
|
} else if (arg == "snapshot") {
|
||||||
|
result$command <- "snapshot"
|
||||||
|
i <- i + 1
|
||||||
} else if (arg %in% c("-k", "--api-key")) {
|
} else if (arg %in% c("-k", "--api-key")) {
|
||||||
i <- i + 1
|
i <- i + 1
|
||||||
result$api_key <- args[i]
|
result$api_key <- args[i]
|
||||||
|
|
@ -695,6 +837,45 @@ parse_args <- function() {
|
||||||
i <- i + 1
|
i <- i + 1
|
||||||
result$vcpu <- as.integer(args[i])
|
result$vcpu <- as.integer(args[i])
|
||||||
i <- i + 1
|
i <- i + 1
|
||||||
|
} else if (arg == "--snapshot") {
|
||||||
|
i <- i + 1
|
||||||
|
if (result$command == "session") {
|
||||||
|
result$snapshot_id <- args[i]
|
||||||
|
} else if (result$command == "service") {
|
||||||
|
result$snapshot_svc <- args[i]
|
||||||
|
}
|
||||||
|
i <- i + 1
|
||||||
|
} else if (arg == "--restore") {
|
||||||
|
i <- i + 1
|
||||||
|
if (result$command == "session") {
|
||||||
|
result$restore_id <- args[i]
|
||||||
|
} else if (result$command == "service") {
|
||||||
|
result$restore_svc <- args[i]
|
||||||
|
}
|
||||||
|
i <- i + 1
|
||||||
|
} else if (arg == "--from") {
|
||||||
|
i <- i + 1
|
||||||
|
result$from_snapshot <- args[i]
|
||||||
|
i <- i + 1
|
||||||
|
} else if (arg == "--snapshot-name") {
|
||||||
|
i <- i + 1
|
||||||
|
result$snapshot_name <- args[i]
|
||||||
|
i <- i + 1
|
||||||
|
} else if (arg == "--hot") {
|
||||||
|
result$hot <- TRUE
|
||||||
|
i <- i + 1
|
||||||
|
} else if (arg == "--delete") {
|
||||||
|
i <- i + 1
|
||||||
|
result$delete <- args[i]
|
||||||
|
i <- i + 1
|
||||||
|
} else if (arg == "--clone") {
|
||||||
|
i <- i + 1
|
||||||
|
result$clone <- args[i]
|
||||||
|
i <- i + 1
|
||||||
|
} else if (arg == "--shell") {
|
||||||
|
i <- i + 1
|
||||||
|
result$shell <- args[i]
|
||||||
|
i <- i + 1
|
||||||
} else if (arg == "--extend") {
|
} else if (arg == "--extend") {
|
||||||
result$extend <- TRUE
|
result$extend <- TRUE
|
||||||
i <- i + 1
|
i <- i + 1
|
||||||
|
|
@ -706,6 +887,7 @@ parse_args <- function() {
|
||||||
cat("Usage: un.r [options] <source_file>\n", file = stderr())
|
cat("Usage: un.r [options] <source_file>\n", file = stderr())
|
||||||
cat(" un.r session [options]\n", file = stderr())
|
cat(" un.r session [options]\n", file = stderr())
|
||||||
cat(" un.r service [options]\n", file = stderr())
|
cat(" un.r service [options]\n", file = stderr())
|
||||||
|
cat(" un.r snapshot [options]\n", file = stderr())
|
||||||
cat(" un.r key [options]\n", file = stderr())
|
cat(" un.r key [options]\n", file = stderr())
|
||||||
quit(status = 1)
|
quit(status = 1)
|
||||||
}
|
}
|
||||||
|
|
@ -721,6 +903,8 @@ main <- function() {
|
||||||
cmd_session(args)
|
cmd_session(args)
|
||||||
} else if (!is.null(args$command) && args$command == "service") {
|
} else if (!is.null(args$command) && args$command == "service") {
|
||||||
cmd_service(args)
|
cmd_service(args)
|
||||||
|
} else if (!is.null(args$command) && args$command == "snapshot") {
|
||||||
|
cmd_snapshot(args)
|
||||||
} else if (!is.null(args$command) && args$command == "key") {
|
} else if (!is.null(args$command) && args$command == "key") {
|
||||||
cmd_key(args)
|
cmd_key(args)
|
||||||
} else if (!is.null(args$source_file)) {
|
} else if (!is.null(args$source_file)) {
|
||||||
|
|
@ -729,6 +913,7 @@ main <- function() {
|
||||||
cat("Usage: un.r [options] <source_file>\n", file = stderr())
|
cat("Usage: un.r [options] <source_file>\n", file = stderr())
|
||||||
cat(" un.r session [options]\n", file = stderr())
|
cat(" un.r session [options]\n", file = stderr())
|
||||||
cat(" un.r service [options]\n", file = stderr())
|
cat(" un.r service [options]\n", file = stderr())
|
||||||
|
cat(" un.r snapshot [options]\n", file = stderr())
|
||||||
cat(" un.r key [options]\n", file = stderr())
|
cat(" un.r key [options]\n", file = stderr())
|
||||||
quit(status = 1)
|
quit(status = 1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
181
un.rb
181
un.rb
|
|
@ -255,6 +255,27 @@ def cmd_session(options)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if options[:snapshot_session]
|
||||||
|
payload = {}
|
||||||
|
payload[:name] = options[:snapshot_name] if options[:snapshot_name]
|
||||||
|
payload[:hot] = true if options[:hot]
|
||||||
|
|
||||||
|
warn "#{YELLOW}Creating snapshot of session #{options[:snapshot_session]}...#{RESET}"
|
||||||
|
result = api_request("/sessions/#{options[:snapshot_session]}/snapshot", method: 'POST', data: payload, keys: keys)
|
||||||
|
puts "#{GREEN}Snapshot created successfully#{RESET}"
|
||||||
|
puts "Snapshot ID: #{result['id'] || 'N/A'}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if options[:restore_session]
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
warn "#{YELLOW}Restoring from snapshot #{options[:restore_session]}...#{RESET}"
|
||||||
|
result = api_request("/snapshots/#{options[:restore_session]}/restore", method: 'POST', keys: keys)
|
||||||
|
puts "#{GREEN}Session restored from snapshot#{RESET}"
|
||||||
|
puts "New session ID: #{result['session_id']}" if result['session_id']
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if options[:attach]
|
if options[:attach]
|
||||||
puts "#{YELLOW}Attaching to session #{options[:attach]}...#{RESET}"
|
puts "#{YELLOW}Attaching to session #{options[:attach]}...#{RESET}"
|
||||||
puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}"
|
puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}"
|
||||||
|
|
@ -378,6 +399,75 @@ def cmd_key(options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cmd_snapshot(options)
|
||||||
|
keys = get_api_keys(options[:api_key])
|
||||||
|
|
||||||
|
if options[:list]
|
||||||
|
result = api_request('/snapshots', keys: keys)
|
||||||
|
snapshots = result['snapshots'] || []
|
||||||
|
if snapshots.empty?
|
||||||
|
puts 'No snapshots found'
|
||||||
|
else
|
||||||
|
puts format('%-40s %-20s %-12s %-30s %s', 'ID', 'Name', 'Type', 'Source ID', 'Size')
|
||||||
|
snapshots.each do |s|
|
||||||
|
puts format('%-40s %-20s %-12s %-30s %s',
|
||||||
|
s['id'] || 'N/A', s['name'] || '-',
|
||||||
|
s['source_type'] || 'N/A', s['source_id'] || 'N/A',
|
||||||
|
s['size'] || 'N/A')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if options[:info_snapshot]
|
||||||
|
result = api_request("/snapshots/#{options[:info_snapshot]}", keys: keys)
|
||||||
|
puts "#{BLUE}Snapshot Details#{RESET}\n"
|
||||||
|
puts "Snapshot ID: #{result['id'] || 'N/A'}"
|
||||||
|
puts "Name: #{result['name'] || '-'}"
|
||||||
|
puts "Source Type: #{result['source_type'] || 'N/A'}"
|
||||||
|
puts "Source ID: #{result['source_id'] || 'N/A'}"
|
||||||
|
puts "Size: #{result['size'] || 'N/A'}"
|
||||||
|
puts "Created: #{result['created_at'] || 'N/A'}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if options[:delete_snapshot]
|
||||||
|
api_request("/snapshots/#{options[:delete_snapshot]}", method: 'DELETE', keys: keys)
|
||||||
|
puts "#{GREEN}Snapshot deleted successfully#{RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if options[:clone_snapshot]
|
||||||
|
unless options[:clone_type]
|
||||||
|
warn "#{RED}Error: --type required for --clone (session or service)#{RESET}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
unless ['session', 'service'].include?(options[:clone_type])
|
||||||
|
warn "#{RED}Error: --type must be 'session' or 'service'#{RESET}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
payload = { type: options[:clone_type] }
|
||||||
|
payload[:name] = options[:clone_name] if options[:clone_name]
|
||||||
|
payload[:shell] = options[:clone_shell] if options[:clone_shell]
|
||||||
|
payload[:ports] = options[:clone_ports].split(',').map(&:to_i) if options[:clone_ports]
|
||||||
|
|
||||||
|
result = api_request("/snapshots/#{options[:clone_snapshot]}/clone", method: 'POST', data: payload, keys: keys)
|
||||||
|
|
||||||
|
if options[:clone_type] == 'session'
|
||||||
|
puts "#{GREEN}Session created from snapshot#{RESET}"
|
||||||
|
puts "Session ID: #{result['id'] || 'N/A'}"
|
||||||
|
else
|
||||||
|
puts "#{GREEN}Service created from snapshot#{RESET}"
|
||||||
|
puts "Service ID: #{result['id'] || 'N/A'}"
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
warn "#{RED}Error: Specify --list, --info ID, --delete ID, or --clone ID --type TYPE#{RESET}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
def cmd_service(options)
|
def cmd_service(options)
|
||||||
keys = get_api_keys(options[:api_key])
|
keys = get_api_keys(options[:api_key])
|
||||||
|
|
||||||
|
|
@ -435,6 +525,27 @@ def cmd_service(options)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if options[:snapshot_service]
|
||||||
|
payload = {}
|
||||||
|
payload[:name] = options[:snapshot_name] if options[:snapshot_name]
|
||||||
|
payload[:hot] = true if options[:hot]
|
||||||
|
|
||||||
|
warn "#{YELLOW}Creating snapshot of service #{options[:snapshot_service]}...#{RESET}"
|
||||||
|
result = api_request("/services/#{options[:snapshot_service]}/snapshot", method: 'POST', data: payload, keys: keys)
|
||||||
|
puts "#{GREEN}Snapshot created successfully#{RESET}"
|
||||||
|
puts "Snapshot ID: #{result['id'] || 'N/A'}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if options[:restore_service]
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
warn "#{YELLOW}Restoring from snapshot #{options[:restore_service]}...#{RESET}"
|
||||||
|
result = api_request("/snapshots/#{options[:restore_service]}/restore", method: 'POST', keys: keys)
|
||||||
|
puts "#{GREEN}Service restored from snapshot#{RESET}"
|
||||||
|
puts "New service ID: #{result['service_id']}" if result['service_id']
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if options[:execute]
|
if options[:execute]
|
||||||
payload = { command: options[:command] }
|
payload = { command: options[:command] }
|
||||||
result = api_request("/services/#{options[:execute]}/execute", method: 'POST', data: payload, keys: keys)
|
result = api_request("/services/#{options[:execute]}/execute", method: 'POST', data: payload, keys: keys)
|
||||||
|
|
@ -527,6 +638,20 @@ def main
|
||||||
list: false,
|
list: false,
|
||||||
attach: nil,
|
attach: nil,
|
||||||
kill: nil,
|
kill: nil,
|
||||||
|
snapshot_session: nil,
|
||||||
|
snapshot_service: nil,
|
||||||
|
restore_session: nil,
|
||||||
|
restore_service: nil,
|
||||||
|
from_snapshot: nil,
|
||||||
|
snapshot_name: nil,
|
||||||
|
hot: false,
|
||||||
|
info_snapshot: nil,
|
||||||
|
delete_snapshot: nil,
|
||||||
|
clone_snapshot: nil,
|
||||||
|
clone_type: nil,
|
||||||
|
clone_name: nil,
|
||||||
|
clone_shell: nil,
|
||||||
|
clone_ports: nil,
|
||||||
audit: false,
|
audit: false,
|
||||||
tmux: false,
|
tmux: false,
|
||||||
screen: false,
|
screen: false,
|
||||||
|
|
@ -544,7 +669,8 @@ def main
|
||||||
execute: nil,
|
execute: nil,
|
||||||
dump_bootstrap: nil,
|
dump_bootstrap: nil,
|
||||||
dump_file: nil,
|
dump_file: nil,
|
||||||
extend: false
|
extend: false,
|
||||||
|
bootstrap_file: nil
|
||||||
}
|
}
|
||||||
|
|
||||||
# Manual argument parsing
|
# Manual argument parsing
|
||||||
|
|
@ -553,7 +679,7 @@ def main
|
||||||
arg = ARGV[i]
|
arg = ARGV[i]
|
||||||
|
|
||||||
case arg
|
case arg
|
||||||
when 'session', 'service', 'key'
|
when 'session', 'service', 'key', 'snapshot'
|
||||||
options[:command] = arg
|
options[:command] = arg
|
||||||
when '-e'
|
when '-e'
|
||||||
i += 1
|
i += 1
|
||||||
|
|
@ -640,6 +766,55 @@ def main
|
||||||
when '--dump-file'
|
when '--dump-file'
|
||||||
i += 1
|
i += 1
|
||||||
options[:dump_file] = ARGV[i]
|
options[:dump_file] = ARGV[i]
|
||||||
|
when '--snapshot'
|
||||||
|
i += 1
|
||||||
|
if options[:command] == 'session'
|
||||||
|
options[:snapshot_session] = ARGV[i]
|
||||||
|
elsif options[:command] == 'service'
|
||||||
|
options[:snapshot_service] = ARGV[i]
|
||||||
|
end
|
||||||
|
when '--restore'
|
||||||
|
i += 1
|
||||||
|
if options[:command] == 'session'
|
||||||
|
options[:restore_session] = ARGV[i]
|
||||||
|
elsif options[:command] == 'service'
|
||||||
|
options[:restore_service] = ARGV[i]
|
||||||
|
end
|
||||||
|
when '--from'
|
||||||
|
i += 1
|
||||||
|
options[:from_snapshot] = ARGV[i]
|
||||||
|
when '--snapshot-name'
|
||||||
|
i += 1
|
||||||
|
options[:snapshot_name] = ARGV[i]
|
||||||
|
when '--hot'
|
||||||
|
options[:hot] = true
|
||||||
|
when '--info'
|
||||||
|
i += 1
|
||||||
|
if options[:command] == 'snapshot'
|
||||||
|
options[:info_snapshot] = ARGV[i]
|
||||||
|
else
|
||||||
|
options[:info] = ARGV[i]
|
||||||
|
end
|
||||||
|
when '--delete'
|
||||||
|
i += 1
|
||||||
|
options[:delete_snapshot] = ARGV[i]
|
||||||
|
when '--clone'
|
||||||
|
i += 1
|
||||||
|
options[:clone_snapshot] = ARGV[i]
|
||||||
|
when '--type'
|
||||||
|
i += 1
|
||||||
|
if options[:clone_snapshot]
|
||||||
|
options[:clone_type] = ARGV[i]
|
||||||
|
else
|
||||||
|
options[:type] = ARGV[i]
|
||||||
|
end
|
||||||
|
when '--shell'
|
||||||
|
i += 1
|
||||||
|
if options[:clone_snapshot]
|
||||||
|
options[:clone_shell] = ARGV[i]
|
||||||
|
else
|
||||||
|
options[:shell] = ARGV[i]
|
||||||
|
end
|
||||||
when '--extend'
|
when '--extend'
|
||||||
options[:extend] = true
|
options[:extend] = true
|
||||||
else
|
else
|
||||||
|
|
@ -659,6 +834,8 @@ def main
|
||||||
cmd_session(options)
|
cmd_session(options)
|
||||||
when 'service'
|
when 'service'
|
||||||
cmd_service(options)
|
cmd_service(options)
|
||||||
|
when 'snapshot'
|
||||||
|
cmd_snapshot(options)
|
||||||
when 'key'
|
when 'key'
|
||||||
cmd_key(options)
|
cmd_key(options)
|
||||||
else
|
else
|
||||||
|
|
|
||||||
222
un.sh
Executable file → Normal file
222
un.sh
Executable file → Normal file
|
|
@ -348,6 +348,11 @@ cmd_session() {
|
||||||
local vcpu=""
|
local vcpu=""
|
||||||
local api_key="${UNSANDBOX_API_KEY:-}"
|
local api_key="${UNSANDBOX_API_KEY:-}"
|
||||||
local -a input_files=()
|
local -a input_files=()
|
||||||
|
local snapshot=""
|
||||||
|
local restore=""
|
||||||
|
local from_snapshot=""
|
||||||
|
local snapshot_name=""
|
||||||
|
local hot=false
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -379,6 +384,26 @@ cmd_session() {
|
||||||
screen=true
|
screen=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--snapshot)
|
||||||
|
snapshot="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--restore)
|
||||||
|
restore="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--from)
|
||||||
|
from_snapshot="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--snapshot-name)
|
||||||
|
snapshot_name="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--hot)
|
||||||
|
hot=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-f)
|
-f)
|
||||||
input_files+=("$2")
|
input_files+=("$2")
|
||||||
shift 2
|
shift 2
|
||||||
|
|
@ -426,6 +451,26 @@ cmd_session() {
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$snapshot" ]]; then
|
||||||
|
local payload=$(jq -n --arg name "$snapshot_name" --argjson hot "$hot" '{name: $name, hot: $hot}')
|
||||||
|
echo -e "${YELLOW}Creating snapshot of session $snapshot...${RESET}"
|
||||||
|
local result=$(api_request "/sessions/$snapshot/snapshot" "POST" "$payload" "$api_key")
|
||||||
|
local snapshot_id=$(echo "$result" | jq -r '.id // "N/A"')
|
||||||
|
echo -e "${GREEN}Snapshot created successfully${RESET}"
|
||||||
|
echo "Snapshot ID: $snapshot_id"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$restore" ]]; then
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
echo -e "${YELLOW}Restoring from snapshot $restore...${RESET}"
|
||||||
|
local result=$(api_request "/snapshots/$restore/restore" "POST" "{}" "$api_key")
|
||||||
|
echo -e "${GREEN}Session restored from snapshot${RESET}"
|
||||||
|
local new_id=$(echo "$result" | jq -r '.session_id // empty')
|
||||||
|
[[ -n "$new_id" ]] && echo "New session ID: $new_id"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -n "$attach" ]]; then
|
if [[ -n "$attach" ]]; then
|
||||||
echo -e "${YELLOW}Attaching to session $attach...${RESET}"
|
echo -e "${YELLOW}Attaching to session $attach...${RESET}"
|
||||||
echo -e "${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}"
|
echo -e "${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}"
|
||||||
|
|
@ -483,6 +528,11 @@ cmd_service() {
|
||||||
local vcpu=""
|
local vcpu=""
|
||||||
local api_key="${UNSANDBOX_API_KEY:-}"
|
local api_key="${UNSANDBOX_API_KEY:-}"
|
||||||
local -a input_files=()
|
local -a input_files=()
|
||||||
|
local snapshot=""
|
||||||
|
local restore=""
|
||||||
|
local from_snapshot=""
|
||||||
|
local snapshot_name=""
|
||||||
|
local hot=false
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -558,6 +608,26 @@ cmd_service() {
|
||||||
dump_file="$2"
|
dump_file="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--snapshot)
|
||||||
|
snapshot="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--restore)
|
||||||
|
restore="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--from)
|
||||||
|
from_snapshot="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--snapshot-name)
|
||||||
|
snapshot_name="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--hot)
|
||||||
|
hot=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-n)
|
-n)
|
||||||
network="$2"
|
network="$2"
|
||||||
shift 2
|
shift 2
|
||||||
|
|
@ -595,6 +665,26 @@ cmd_service() {
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$snapshot" ]]; then
|
||||||
|
local payload=$(jq -n --arg name "$snapshot_name" --argjson hot "$hot" '{name: $name, hot: $hot}')
|
||||||
|
echo -e "${YELLOW}Creating snapshot of service $snapshot...${RESET}"
|
||||||
|
local result=$(api_request "/services/$snapshot/snapshot" "POST" "$payload" "$api_key")
|
||||||
|
local snapshot_id=$(echo "$result" | jq -r '.id // "N/A"')
|
||||||
|
echo -e "${GREEN}Snapshot created successfully${RESET}"
|
||||||
|
echo "Snapshot ID: $snapshot_id"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$restore" ]]; then
|
||||||
|
# --restore takes snapshot ID directly, calls /snapshots/:id/restore
|
||||||
|
echo -e "${YELLOW}Restoring from snapshot $restore...${RESET}"
|
||||||
|
local result=$(api_request "/snapshots/$restore/restore" "POST" "{}" "$api_key")
|
||||||
|
echo -e "${GREEN}Service restored from snapshot${RESET}"
|
||||||
|
local new_id=$(echo "$result" | jq -r '.service_id // empty')
|
||||||
|
[[ -n "$new_id" ]] && echo "New service ID: $new_id"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -n "$info" ]]; then
|
if [[ -n "$info" ]]; then
|
||||||
local result=$(api_request "/services/$info" "GET" "" "$api_key")
|
local result=$(api_request "/services/$info" "GET" "" "$api_key")
|
||||||
echo "$result" | jq '.'
|
echo "$result" | jq '.'
|
||||||
|
|
@ -728,6 +818,116 @@ cmd_service() {
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd_snapshot() {
|
||||||
|
local api_key="${UNSANDBOX_API_KEY:-}"
|
||||||
|
local list=false
|
||||||
|
local info=""
|
||||||
|
local delete=""
|
||||||
|
local clone=""
|
||||||
|
local clone_type=""
|
||||||
|
local clone_name=""
|
||||||
|
local clone_shell=""
|
||||||
|
local clone_ports=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-l|--list)
|
||||||
|
list=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--info)
|
||||||
|
info="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--delete)
|
||||||
|
delete="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--clone)
|
||||||
|
clone="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--type)
|
||||||
|
clone_type="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--name)
|
||||||
|
clone_name="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--shell)
|
||||||
|
clone_shell="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ports)
|
||||||
|
clone_ports="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-k)
|
||||||
|
api_key="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
echo -e "${RED}Unknown option: $1${RESET}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$list" == true ]]; then
|
||||||
|
local result=$(api_request "/snapshots" "GET" "" "$api_key")
|
||||||
|
echo "$result" | jq '.'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$info" ]]; then
|
||||||
|
local result=$(api_request "/snapshots/$info" "GET" "" "$api_key")
|
||||||
|
echo "$result" | jq '.'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$delete" ]]; then
|
||||||
|
api_request "/snapshots/$delete" "DELETE" "" "$api_key" > /dev/null
|
||||||
|
echo -e "${GREEN}Snapshot deleted successfully${RESET}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$clone" ]]; then
|
||||||
|
if [[ -z "$clone_type" ]]; then
|
||||||
|
echo -e "${RED}Error: --type required with --clone (session or service)${RESET}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local payload=$(jq -n --arg type "$clone_type" '{type: $type}')
|
||||||
|
[[ -n "$clone_name" ]] && payload=$(echo "$payload" | jq --arg n "$clone_name" '. + {name: $n}')
|
||||||
|
[[ -n "$clone_shell" ]] && payload=$(echo "$payload" | jq --arg s "$clone_shell" '. + {shell: $s}')
|
||||||
|
if [[ -n "$clone_ports" ]]; then
|
||||||
|
local ports_json="[$(echo "$clone_ports" | sed 's/,/,/g')]"
|
||||||
|
payload=$(echo "$payload" | jq --argjson p "$ports_json" '. + {ports: $p}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Cloning snapshot $clone to create new $clone_type...${RESET}"
|
||||||
|
local result=$(api_request "/snapshots/$clone/clone" "POST" "$payload" "$api_key")
|
||||||
|
|
||||||
|
if [[ "$clone_type" == "session" ]]; then
|
||||||
|
local session_id=$(echo "$result" | jq -r '.session_id // "N/A"')
|
||||||
|
echo -e "${GREEN}Session created from snapshot${RESET}"
|
||||||
|
echo "Session ID: $session_id"
|
||||||
|
else
|
||||||
|
local service_id=$(echo "$result" | jq -r '.service_id // "N/A"')
|
||||||
|
echo -e "${GREEN}Service created from snapshot${RESET}"
|
||||||
|
echo "Service ID: $service_id"
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${RED}Error: Specify --list, --info, --delete, or --clone${RESET}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
validate_key() {
|
validate_key() {
|
||||||
local api_key="$1"
|
local api_key="$1"
|
||||||
local extend_mode="$2"
|
local extend_mode="$2"
|
||||||
|
|
@ -884,6 +1084,7 @@ Usage:
|
||||||
$0 [options] <source_file>
|
$0 [options] <source_file>
|
||||||
$0 session [options]
|
$0 session [options]
|
||||||
$0 service [options]
|
$0 service [options]
|
||||||
|
$0 snapshot [options]
|
||||||
$0 key [options]
|
$0 key [options]
|
||||||
|
|
||||||
Execute options:
|
Execute options:
|
||||||
|
|
@ -903,6 +1104,10 @@ Session options:
|
||||||
--audit Record session
|
--audit Record session
|
||||||
--tmux Enable tmux persistence
|
--tmux Enable tmux persistence
|
||||||
--screen Enable screen persistence
|
--screen Enable screen persistence
|
||||||
|
--snapshot SESSION_ID Create snapshot of session
|
||||||
|
--restore SNAPSHOT_ID Restore from snapshot ID
|
||||||
|
--snapshot-name NAME Optional name for snapshot
|
||||||
|
--hot Take snapshot without freezing (live snapshot)
|
||||||
|
|
||||||
Service options:
|
Service options:
|
||||||
--name NAME Service name
|
--name NAME Service name
|
||||||
|
|
@ -922,6 +1127,20 @@ Service options:
|
||||||
--command CMD Command to execute (with --execute)
|
--command CMD Command to execute (with --execute)
|
||||||
--dump-bootstrap ID Dump bootstrap script
|
--dump-bootstrap ID Dump bootstrap script
|
||||||
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
|
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
|
||||||
|
--snapshot SERVICE_ID Create snapshot of service
|
||||||
|
--restore SNAPSHOT_ID Restore from snapshot ID
|
||||||
|
--snapshot-name NAME Optional name for snapshot
|
||||||
|
--hot Take snapshot without freezing (live snapshot)
|
||||||
|
|
||||||
|
Snapshot options:
|
||||||
|
-l, --list List all snapshots
|
||||||
|
--info ID Get snapshot details
|
||||||
|
--delete ID Delete a snapshot
|
||||||
|
--clone ID Clone snapshot to new session/service (--type required)
|
||||||
|
--type TYPE Type for clone: session or service
|
||||||
|
--name NAME Name for cloned session/service
|
||||||
|
--shell NAME Shell for cloned session
|
||||||
|
--ports PORTS Ports for cloned service
|
||||||
|
|
||||||
Key options:
|
Key options:
|
||||||
-k KEY API key to validate
|
-k KEY API key to validate
|
||||||
|
|
@ -942,6 +1161,9 @@ if [[ "$1" == "session" ]]; then
|
||||||
elif [[ "$1" == "service" ]]; then
|
elif [[ "$1" == "service" ]]; then
|
||||||
shift
|
shift
|
||||||
cmd_service "$@"
|
cmd_service "$@"
|
||||||
|
elif [[ "$1" == "snapshot" ]]; then
|
||||||
|
shift
|
||||||
|
cmd_snapshot "$@"
|
||||||
elif [[ "$1" == "key" ]]; then
|
elif [[ "$1" == "key" ]]; then
|
||||||
shift
|
shift
|
||||||
cmd_key "$@"
|
cmd_key "$@"
|
||||||
|
|
|
||||||
13
un.ts
13
un.ts
|
|
@ -112,6 +112,19 @@ interface Args {
|
||||||
execute: string | null;
|
execute: string | null;
|
||||||
command_arg: string | null;
|
command_arg: string | null;
|
||||||
extend: boolean;
|
extend: boolean;
|
||||||
|
snapshot: string | null;
|
||||||
|
restore: string | null;
|
||||||
|
from: string | null;
|
||||||
|
snapshotName: string | null;
|
||||||
|
hot: boolean;
|
||||||
|
deleteSnapshot: string | null;
|
||||||
|
clone: string | null;
|
||||||
|
cloneType: string | null;
|
||||||
|
cloneName: string | null;
|
||||||
|
cloneShell: string | null;
|
||||||
|
clonePorts: string | null;
|
||||||
|
dumpBootstrap: string | null;
|
||||||
|
dumpFile: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiKeys {
|
interface ApiKeys {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue