CI/CD fixes release: - Lint script now finds SDKs in clients/ directory structure - Perl lint gracefully skips when modules missing - Python examples exit 0 when API keys missing (CI-friendly) - validate-examples.sh race condition fixed
1582 lines
50 KiB
Perl
1582 lines
50 KiB
Perl
#!/usr/bin/env perl
|
|
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
|
#
|
|
# unsandbox.com Perl SDK (Synchronous)
|
|
# Full API with execution, sessions, services, snapshots, and images.
|
|
#
|
|
# Library Usage:
|
|
# use Un;
|
|
# my $result = Un::execute("python", "print(42)");
|
|
# print $result->{stdout};
|
|
#
|
|
# CLI Usage:
|
|
# perl un.pl script.py
|
|
# perl un.pl -s python 'print(42)'
|
|
# perl un.pl session --list
|
|
# perl un.pl service --list
|
|
# perl un.pl snapshot --list
|
|
# perl un.pl image --list
|
|
#
|
|
# Authentication Priority (4-tier):
|
|
# 1. Function arguments (public_key, secret_key)
|
|
# 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
|
# 3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
|
# 4. Local directory (./accounts.csv, line 0 by default)
|
|
#
|
|
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
|
|
|
use strict;
|
|
use warnings;
|
|
use JSON::PP;
|
|
use LWP::UserAgent;
|
|
use HTTP::Request;
|
|
use Digest::SHA qw(hmac_sha256_hex);
|
|
use MIME::Base64;
|
|
use File::Basename;
|
|
use File::Path qw(make_path);
|
|
use Time::HiRes qw(time sleep);
|
|
|
|
package Un;
|
|
|
|
our $VERSION = "4.3.1";
|
|
our $API_BASE = 'https://api.unsandbox.com';
|
|
our $PORTAL_BASE = 'https://unsandbox.com';
|
|
|
|
# Thread-local error storage
|
|
our $LAST_ERROR = "";
|
|
|
|
# Colors
|
|
my $BLUE = "\033[34m";
|
|
my $RED = "\033[31m";
|
|
my $GREEN = "\033[32m";
|
|
my $YELLOW = "\033[33m";
|
|
my $RESET = "\033[0m";
|
|
|
|
# Extension to language mapping
|
|
my %EXT_MAP = (
|
|
'.py' => 'python', '.js' => 'javascript', '.ts' => 'typescript',
|
|
'.rb' => 'ruby', '.php' => 'php', '.pl' => 'perl', '.lua' => 'lua',
|
|
'.sh' => 'bash', '.go' => 'go', '.rs' => 'rust', '.c' => 'c',
|
|
'.cpp' => 'cpp', '.cc' => 'cpp', '.cxx' => 'cpp',
|
|
'.java' => 'java', '.kt' => 'kotlin', '.cs' => 'csharp', '.fs' => 'fsharp',
|
|
'.hs' => 'haskell', '.ml' => 'ocaml', '.clj' => 'clojure', '.scm' => 'scheme',
|
|
'.lisp' => 'commonlisp', '.erl' => 'erlang', '.ex' => 'elixir', '.exs' => 'elixir',
|
|
'.jl' => 'julia', '.r' => 'r', '.R' => 'r', '.cr' => 'crystal',
|
|
'.d' => 'd', '.nim' => 'nim', '.zig' => 'zig', '.v' => 'v',
|
|
'.dart' => 'dart', '.groovy' => 'groovy', '.scala' => 'scala',
|
|
'.f90' => 'fortran', '.f95' => 'fortran', '.cob' => 'cobol',
|
|
'.pro' => 'prolog', '.forth' => 'forth', '.4th' => 'forth',
|
|
'.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc'
|
|
);
|
|
|
|
# ============================================================================
|
|
# Utility Functions
|
|
# ============================================================================
|
|
|
|
sub version { return $VERSION; }
|
|
|
|
sub last_error { return $LAST_ERROR; }
|
|
|
|
sub set_error {
|
|
my ($msg) = @_;
|
|
$LAST_ERROR = $msg;
|
|
}
|
|
|
|
sub detect_language {
|
|
my ($filename) = @_;
|
|
return undef unless $filename;
|
|
my ($name, $dir, $ext) = fileparse($filename, qr/\.[^.]*/);
|
|
return $EXT_MAP{lc($ext)};
|
|
}
|
|
|
|
sub hmac_sign {
|
|
my ($secret, $message) = @_;
|
|
return undef unless defined $secret && defined $message;
|
|
return hmac_sha256_hex($message, $secret);
|
|
}
|
|
|
|
# ============================================================================
|
|
# Credential Management
|
|
# ============================================================================
|
|
|
|
sub load_accounts_csv {
|
|
my ($path) = @_;
|
|
$path ||= "$ENV{HOME}/.unsandbox/accounts.csv";
|
|
return [] unless -e $path;
|
|
|
|
my @accounts;
|
|
open my $fh, '<', $path or return [];
|
|
while (my $line = <$fh>) {
|
|
chomp $line;
|
|
next if !$line || $line =~ /^\s*#/;
|
|
my ($pk, $sk) = split /,/, $line, 2;
|
|
push @accounts, [$pk, $sk] if $pk && $sk;
|
|
}
|
|
close $fh;
|
|
return \@accounts;
|
|
}
|
|
|
|
sub get_credentials {
|
|
my (%opts) = @_;
|
|
|
|
# Tier 1: Arguments
|
|
return ($opts{public_key}, $opts{secret_key}) if $opts{public_key} && $opts{secret_key};
|
|
|
|
# Tier 2: Environment
|
|
if ($ENV{UNSANDBOX_PUBLIC_KEY} && $ENV{UNSANDBOX_SECRET_KEY}) {
|
|
return ($ENV{UNSANDBOX_PUBLIC_KEY}, $ENV{UNSANDBOX_SECRET_KEY});
|
|
}
|
|
|
|
# Legacy fallback
|
|
if ($ENV{UNSANDBOX_API_KEY}) {
|
|
return ($ENV{UNSANDBOX_API_KEY}, '');
|
|
}
|
|
|
|
# Tier 3: Home directory
|
|
my $home_accounts = load_accounts_csv();
|
|
return @{$home_accounts->[0]} if @$home_accounts;
|
|
|
|
# Tier 4: Local directory
|
|
my $local_accounts = load_accounts_csv("./accounts.csv");
|
|
return @{$local_accounts->[0]} if @$local_accounts;
|
|
|
|
set_error("No credentials found");
|
|
return (undef, undef);
|
|
}
|
|
|
|
# ============================================================================
|
|
# API Communication
|
|
# ============================================================================
|
|
|
|
sub api_request {
|
|
my ($method, $endpoint, $body, %opts) = @_;
|
|
my $extra_headers = delete $opts{extra_headers} || {};
|
|
my $content_type = delete $opts{content_type} || 'application/json';
|
|
my ($pk, $sk) = get_credentials(%opts);
|
|
|
|
unless ($pk) {
|
|
set_error("No credentials available");
|
|
return undef;
|
|
}
|
|
|
|
my $timestamp = int(time);
|
|
my $body_str = '';
|
|
if ($body) {
|
|
$body_str = ref($body) ? encode_json($body) : $body;
|
|
}
|
|
|
|
my $ua = LWP::UserAgent->new(timeout => 300);
|
|
my $url = "$API_BASE$endpoint";
|
|
my $req = HTTP::Request->new($method, $url);
|
|
|
|
$req->header('Authorization' => "Bearer $pk");
|
|
$req->header('Content-Type' => $content_type);
|
|
|
|
# HMAC signature
|
|
if ($sk) {
|
|
my $sig_input = "$timestamp:$method:$endpoint:$body_str";
|
|
my $signature = hmac_sign($sk, $sig_input);
|
|
$req->header('X-Timestamp' => $timestamp);
|
|
$req->header('X-Signature' => $signature);
|
|
}
|
|
|
|
# Add extra headers
|
|
for my $key (keys %$extra_headers) {
|
|
$req->header($key => $extra_headers->{$key});
|
|
}
|
|
|
|
$req->content($body_str) if $body_str;
|
|
|
|
my $res = $ua->request($req);
|
|
|
|
unless ($res->is_success) {
|
|
set_error("API error (" . $res->code . "): " . $res->content);
|
|
return undef;
|
|
}
|
|
|
|
return decode_json($res->content) if $res->content;
|
|
return { success => 1 };
|
|
}
|
|
|
|
sub api_request_with_sudo {
|
|
my ($method, $endpoint, $body, %opts) = @_;
|
|
my ($pk, $sk) = get_credentials(%opts);
|
|
|
|
my $timestamp = int(time);
|
|
my $body_str = '';
|
|
if ($body) {
|
|
$body_str = ref($body) ? encode_json($body) : $body;
|
|
}
|
|
|
|
my $ua = LWP::UserAgent->new(timeout => 300);
|
|
my $url = "$API_BASE$endpoint";
|
|
my $req = HTTP::Request->new($method, $url);
|
|
|
|
$req->header('Authorization' => "Bearer $pk");
|
|
$req->header('Content-Type' => 'application/json');
|
|
|
|
if ($sk) {
|
|
my $sig_input = "$timestamp:$method:$endpoint:$body_str";
|
|
my $signature = hmac_sign($sk, $sig_input);
|
|
$req->header('X-Timestamp' => $timestamp);
|
|
$req->header('X-Signature' => $signature);
|
|
}
|
|
|
|
$req->content($body_str) if $body_str;
|
|
|
|
my $res = $ua->request($req);
|
|
|
|
# Handle 428 - Sudo OTP required
|
|
if ($res->code == 428) {
|
|
my $response_data = eval { decode_json($res->content) } || {};
|
|
my $challenge_id = $response_data->{challenge_id} || '';
|
|
|
|
print STDERR "${YELLOW}Confirmation required. Check your email for a one-time code.${RESET}\n";
|
|
print STDERR "Enter OTP: ";
|
|
|
|
my $otp = <STDIN>;
|
|
return undef unless defined $otp;
|
|
chomp $otp;
|
|
$otp =~ s/\r//g;
|
|
|
|
if ($otp eq '') {
|
|
set_error("Operation cancelled");
|
|
return undef;
|
|
}
|
|
|
|
my $extra = { 'X-Sudo-OTP' => $otp };
|
|
$extra->{'X-Sudo-Challenge'} = $challenge_id if $challenge_id;
|
|
|
|
return api_request($method, $endpoint, $body, %opts, extra_headers => $extra);
|
|
}
|
|
|
|
unless ($res->is_success) {
|
|
set_error("API error (" . $res->code . "): " . $res->content);
|
|
return undef;
|
|
}
|
|
|
|
return decode_json($res->content) if $res->content;
|
|
return { success => 1 };
|
|
}
|
|
|
|
# ============================================================================
|
|
# Execution Functions (8)
|
|
# ============================================================================
|
|
|
|
sub execute {
|
|
my ($language, $code, %opts) = @_;
|
|
my $body = {
|
|
language => $language,
|
|
code => $code,
|
|
network_mode => $opts{network_mode} || 'zerotrust',
|
|
ttl => $opts{ttl} || 60
|
|
};
|
|
$body->{env} = $opts{env} if $opts{env};
|
|
$body->{input_files} = $opts{input_files} if $opts{input_files};
|
|
$body->{return_artifacts} = JSON::PP::true if $opts{return_artifacts};
|
|
return api_request('POST', '/execute', $body, %opts);
|
|
}
|
|
|
|
sub execute_async {
|
|
my ($language, $code, %opts) = @_;
|
|
my $body = {
|
|
language => $language,
|
|
code => $code,
|
|
network_mode => $opts{network_mode} || 'zerotrust',
|
|
ttl => $opts{ttl} || 300
|
|
};
|
|
return api_request('POST', '/execute/async', $body, %opts);
|
|
}
|
|
|
|
sub wait_job {
|
|
my ($job_id, %opts) = @_;
|
|
my $timeout = $opts{timeout} || 120;
|
|
my @delays = (300, 450, 700, 900, 650, 1600, 2000);
|
|
|
|
for my $i (0..119) {
|
|
my $job = get_job($job_id, %opts);
|
|
return $job if $job && $job->{status} eq 'completed';
|
|
if ($job && $job->{status} eq 'failed') {
|
|
set_error("Job failed: " . ($job->{error} || 'unknown'));
|
|
return undef;
|
|
}
|
|
|
|
my $delay = $delays[$i % 7] || 2000;
|
|
sleep($delay / 1000);
|
|
}
|
|
|
|
set_error("Max polls exceeded");
|
|
return undef;
|
|
}
|
|
|
|
sub get_job {
|
|
my ($job_id, %opts) = @_;
|
|
return api_request('GET', "/jobs/$job_id", undef, %opts);
|
|
}
|
|
|
|
sub cancel_job {
|
|
my ($job_id, %opts) = @_;
|
|
return api_request('DELETE', "/jobs/$job_id", undef, %opts);
|
|
}
|
|
|
|
sub list_jobs {
|
|
my (%opts) = @_;
|
|
return api_request('GET', '/jobs', undef, %opts);
|
|
}
|
|
|
|
sub get_languages {
|
|
my (%opts) = @_;
|
|
my $cache_ttl = $opts{cache_ttl} || 3600;
|
|
my $cache_path = "$ENV{HOME}/.unsandbox/languages.json";
|
|
|
|
if (-e $cache_path) {
|
|
my $age = time - (stat $cache_path)[9];
|
|
if ($age < $cache_ttl) {
|
|
open my $fh, '<', $cache_path;
|
|
my $content = do { local $/; <$fh> };
|
|
close $fh;
|
|
return decode_json($content);
|
|
}
|
|
}
|
|
|
|
my $result = api_request('GET', '/languages', undef, %opts);
|
|
my $langs = $result->{languages} || [];
|
|
|
|
my $cache_dir = "$ENV{HOME}/.unsandbox";
|
|
mkdir $cache_dir unless -d $cache_dir;
|
|
if (open my $fh, '>', $cache_path) {
|
|
print $fh encode_json($langs);
|
|
close $fh;
|
|
}
|
|
|
|
return $langs;
|
|
}
|
|
|
|
# ============================================================================
|
|
# Session Functions (9)
|
|
# ============================================================================
|
|
|
|
sub session_list {
|
|
my (%opts) = @_;
|
|
return api_request('GET', '/sessions', undef, %opts);
|
|
}
|
|
|
|
sub session_get {
|
|
my ($session_id, %opts) = @_;
|
|
return api_request('GET', "/sessions/$session_id", undef, %opts);
|
|
}
|
|
|
|
sub session_create {
|
|
my (%opts) = @_;
|
|
my $body = {
|
|
shell => $opts{shell} || 'bash',
|
|
};
|
|
$body->{network} = $opts{network} if $opts{network};
|
|
$body->{vcpu} = $opts{vcpu} if $opts{vcpu};
|
|
$body->{input_files} = $opts{input_files} if $opts{input_files};
|
|
$body->{persistence} = $opts{persistence} if $opts{persistence};
|
|
return api_request('POST', '/sessions', $body, %opts);
|
|
}
|
|
|
|
sub session_destroy {
|
|
my ($session_id, %opts) = @_;
|
|
return api_request('DELETE', "/sessions/$session_id", undef, %opts);
|
|
}
|
|
|
|
sub session_freeze {
|
|
my ($session_id, %opts) = @_;
|
|
return api_request('POST', "/sessions/$session_id/freeze", {}, %opts);
|
|
}
|
|
|
|
sub session_unfreeze {
|
|
my ($session_id, %opts) = @_;
|
|
return api_request('POST', "/sessions/$session_id/unfreeze", {}, %opts);
|
|
}
|
|
|
|
sub session_boost {
|
|
my ($session_id, $vcpu, %opts) = @_;
|
|
return api_request('POST', "/sessions/$session_id/boost", { vcpu => $vcpu }, %opts);
|
|
}
|
|
|
|
sub session_unboost {
|
|
my ($session_id, %opts) = @_;
|
|
return api_request('POST', "/sessions/$session_id/unboost", {}, %opts);
|
|
}
|
|
|
|
sub session_execute {
|
|
my ($session_id, $command, %opts) = @_;
|
|
return api_request('POST', "/sessions/$session_id/execute", { command => $command }, %opts);
|
|
}
|
|
|
|
# ============================================================================
|
|
# Service Functions (17)
|
|
# ============================================================================
|
|
|
|
sub service_list {
|
|
my (%opts) = @_;
|
|
return api_request('GET', '/services', undef, %opts);
|
|
}
|
|
|
|
sub service_get {
|
|
my ($service_id, %opts) = @_;
|
|
return api_request('GET', "/services/$service_id", undef, %opts);
|
|
}
|
|
|
|
sub service_create {
|
|
my (%opts) = @_;
|
|
my $body = { name => $opts{name} };
|
|
$body->{ports} = $opts{ports} if $opts{ports};
|
|
$body->{domains} = $opts{domains} if $opts{domains};
|
|
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
|
|
$body->{bootstrap_content} = $opts{bootstrap_content} if $opts{bootstrap_content};
|
|
$body->{network} = $opts{network} if $opts{network};
|
|
$body->{vcpu} = $opts{vcpu} if $opts{vcpu};
|
|
$body->{service_type} = $opts{service_type} if $opts{service_type};
|
|
$body->{input_files} = $opts{input_files} if $opts{input_files};
|
|
$body->{unfreeze_on_demand} = JSON::PP::true if $opts{unfreeze_on_demand};
|
|
return api_request('POST', '/services', $body, %opts);
|
|
}
|
|
|
|
sub service_destroy {
|
|
my ($service_id, %opts) = @_;
|
|
return api_request_with_sudo('DELETE', "/services/$service_id", undef, %opts);
|
|
}
|
|
|
|
sub service_freeze {
|
|
my ($service_id, %opts) = @_;
|
|
return api_request('POST', "/services/$service_id/freeze", {}, %opts);
|
|
}
|
|
|
|
sub service_unfreeze {
|
|
my ($service_id, %opts) = @_;
|
|
return api_request('POST', "/services/$service_id/unfreeze", {}, %opts);
|
|
}
|
|
|
|
sub service_lock {
|
|
my ($service_id, %opts) = @_;
|
|
return api_request('POST', "/services/$service_id/lock", {}, %opts);
|
|
}
|
|
|
|
sub service_unlock {
|
|
my ($service_id, %opts) = @_;
|
|
return api_request_with_sudo('POST', "/services/$service_id/unlock", {}, %opts);
|
|
}
|
|
|
|
sub service_set_unfreeze_on_demand {
|
|
my ($service_id, $enabled, %opts) = @_;
|
|
my $body = { unfreeze_on_demand => ($enabled ? JSON::PP::true : JSON::PP::false) };
|
|
return api_request('PATCH', "/services/$service_id", $body, %opts);
|
|
}
|
|
|
|
sub service_redeploy {
|
|
my ($service_id, %opts) = @_;
|
|
my $body = {};
|
|
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
|
|
return api_request('POST', "/services/$service_id/redeploy", $body, %opts);
|
|
}
|
|
|
|
sub service_logs {
|
|
my ($service_id, %opts) = @_;
|
|
my $endpoint = "/services/$service_id/logs";
|
|
$endpoint .= "?lines=$opts{lines}" if $opts{lines};
|
|
return api_request('GET', $endpoint, undef, %opts);
|
|
}
|
|
|
|
sub service_execute {
|
|
my ($service_id, $command, %opts) = @_;
|
|
my $body = { command => $command };
|
|
$body->{timeout} = $opts{timeout} if $opts{timeout};
|
|
return api_request('POST', "/services/$service_id/execute", $body, %opts);
|
|
}
|
|
|
|
sub service_env_get {
|
|
my ($service_id, %opts) = @_;
|
|
return api_request('GET', "/services/$service_id/env", undef, %opts);
|
|
}
|
|
|
|
sub service_env_set {
|
|
my ($service_id, $env_content, %opts) = @_;
|
|
return api_request('PUT', "/services/$service_id/env", $env_content,
|
|
%opts, content_type => 'text/plain');
|
|
}
|
|
|
|
sub service_env_delete {
|
|
my ($service_id, %opts) = @_;
|
|
return api_request('DELETE', "/services/$service_id/env", undef, %opts);
|
|
}
|
|
|
|
sub service_env_export {
|
|
my ($service_id, %opts) = @_;
|
|
return api_request('POST', "/services/$service_id/env/export", {}, %opts);
|
|
}
|
|
|
|
sub service_resize {
|
|
my ($service_id, $vcpu, %opts) = @_;
|
|
return api_request('PATCH', "/services/$service_id", { vcpu => $vcpu }, %opts);
|
|
}
|
|
|
|
# ============================================================================
|
|
# Snapshot Functions (9)
|
|
# ============================================================================
|
|
|
|
sub snapshot_list {
|
|
my (%opts) = @_;
|
|
return api_request('GET', '/snapshots', undef, %opts);
|
|
}
|
|
|
|
sub snapshot_get {
|
|
my ($snapshot_id, %opts) = @_;
|
|
return api_request('GET', "/snapshots/$snapshot_id", undef, %opts);
|
|
}
|
|
|
|
sub snapshot_session {
|
|
my ($session_id, %opts) = @_;
|
|
my $body = {};
|
|
$body->{name} = $opts{name} if $opts{name};
|
|
$body->{hot} = JSON::PP::true if $opts{hot};
|
|
return api_request('POST', "/sessions/$session_id/snapshot", $body, %opts);
|
|
}
|
|
|
|
sub snapshot_service {
|
|
my ($service_id, %opts) = @_;
|
|
my $body = {};
|
|
$body->{name} = $opts{name} if $opts{name};
|
|
$body->{hot} = JSON::PP::true if $opts{hot};
|
|
return api_request('POST', "/services/$service_id/snapshot", $body, %opts);
|
|
}
|
|
|
|
sub snapshot_restore {
|
|
my ($snapshot_id, %opts) = @_;
|
|
return api_request('POST', "/snapshots/$snapshot_id/restore", {}, %opts);
|
|
}
|
|
|
|
sub snapshot_delete {
|
|
my ($snapshot_id, %opts) = @_;
|
|
return api_request_with_sudo('DELETE', "/snapshots/$snapshot_id", undef, %opts);
|
|
}
|
|
|
|
sub snapshot_lock {
|
|
my ($snapshot_id, %opts) = @_;
|
|
return api_request('POST', "/snapshots/$snapshot_id/lock", {}, %opts);
|
|
}
|
|
|
|
sub snapshot_unlock {
|
|
my ($snapshot_id, %opts) = @_;
|
|
return api_request_with_sudo('POST', "/snapshots/$snapshot_id/unlock", {}, %opts);
|
|
}
|
|
|
|
sub snapshot_clone {
|
|
my ($snapshot_id, %opts) = @_;
|
|
my $body = { clone_type => $opts{clone_type} || 'session' };
|
|
$body->{name} = $opts{name} if $opts{name};
|
|
$body->{ports} = $opts{ports} if $opts{ports};
|
|
$body->{shell} = $opts{shell} if $opts{shell};
|
|
return api_request('POST', "/snapshots/$snapshot_id/clone", $body, %opts);
|
|
}
|
|
|
|
# ============================================================================
|
|
# Image Functions (13)
|
|
# ============================================================================
|
|
|
|
sub image_list {
|
|
my (%opts) = @_;
|
|
my $endpoint = '/images';
|
|
$endpoint .= "?filter=$opts{filter}" if $opts{filter};
|
|
return api_request('GET', $endpoint, undef, %opts);
|
|
}
|
|
|
|
sub image_get {
|
|
my ($image_id, %opts) = @_;
|
|
return api_request('GET', "/images/$image_id", undef, %opts);
|
|
}
|
|
|
|
sub image_publish {
|
|
my (%opts) = @_;
|
|
my $body = {
|
|
source_type => $opts{source_type},
|
|
source_id => $opts{source_id}
|
|
};
|
|
$body->{name} = $opts{name} if $opts{name};
|
|
$body->{description} = $opts{description} if $opts{description};
|
|
return api_request('POST', '/images/publish', $body, %opts);
|
|
}
|
|
|
|
sub image_delete {
|
|
my ($image_id, %opts) = @_;
|
|
return api_request_with_sudo('DELETE', "/images/$image_id", undef, %opts);
|
|
}
|
|
|
|
sub image_lock {
|
|
my ($image_id, %opts) = @_;
|
|
return api_request('POST', "/images/$image_id/lock", {}, %opts);
|
|
}
|
|
|
|
sub image_unlock {
|
|
my ($image_id, %opts) = @_;
|
|
return api_request_with_sudo('POST', "/images/$image_id/unlock", {}, %opts);
|
|
}
|
|
|
|
sub image_set_visibility {
|
|
my ($image_id, $visibility, %opts) = @_;
|
|
return api_request('POST', "/images/$image_id/visibility", { visibility => $visibility }, %opts);
|
|
}
|
|
|
|
sub image_grant_access {
|
|
my ($image_id, $trusted_api_key, %opts) = @_;
|
|
return api_request('POST', "/images/$image_id/access", { api_key => $trusted_api_key }, %opts);
|
|
}
|
|
|
|
sub image_revoke_access {
|
|
my ($image_id, $trusted_api_key, %opts) = @_;
|
|
return api_request('DELETE', "/images/$image_id/access/$trusted_api_key", undef, %opts);
|
|
}
|
|
|
|
sub image_list_trusted {
|
|
my ($image_id, %opts) = @_;
|
|
return api_request('GET', "/images/$image_id/access", undef, %opts);
|
|
}
|
|
|
|
sub image_transfer {
|
|
my ($image_id, $to_api_key, %opts) = @_;
|
|
return api_request('POST', "/images/$image_id/transfer", { to_api_key => $to_api_key }, %opts);
|
|
}
|
|
|
|
sub image_spawn {
|
|
my ($image_id, %opts) = @_;
|
|
my $body = {};
|
|
$body->{name} = $opts{name} if $opts{name};
|
|
$body->{ports} = $opts{ports} if $opts{ports};
|
|
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
|
|
$body->{network_mode} = $opts{network_mode} if $opts{network_mode};
|
|
return api_request('POST', "/images/$image_id/spawn", $body, %opts);
|
|
}
|
|
|
|
sub image_clone {
|
|
my ($image_id, %opts) = @_;
|
|
my $body = {};
|
|
$body->{name} = $opts{name} if $opts{name};
|
|
$body->{description} = $opts{description} if $opts{description};
|
|
return api_request('POST', "/images/$image_id/clone", $body, %opts);
|
|
}
|
|
|
|
# ============================================================================
|
|
# PaaS Logs Functions (2)
|
|
# ============================================================================
|
|
|
|
sub logs_fetch {
|
|
my (%opts) = @_;
|
|
my $body = {
|
|
source => $opts{source} || 'all',
|
|
lines => $opts{lines} || 100,
|
|
since => $opts{since} || '1h'
|
|
};
|
|
$body->{grep} = $opts{grep} if $opts{grep};
|
|
return api_request('POST', '/paas/logs', $body, %opts);
|
|
}
|
|
|
|
sub logs_stream {
|
|
my (%opts) = @_;
|
|
# SSE streaming - returns immediately, callback for each line
|
|
my $callback = $opts{callback};
|
|
return undef unless $callback;
|
|
|
|
# SSE streaming not easily supported in sync Perl, return placeholder
|
|
set_error("logs_stream requires async support");
|
|
return undef;
|
|
}
|
|
|
|
# ============================================================================
|
|
# Key Validation
|
|
# ============================================================================
|
|
|
|
sub validate_keys {
|
|
my (%opts) = @_;
|
|
my ($pk, $sk) = get_credentials(%opts);
|
|
|
|
my $ua = LWP::UserAgent->new(timeout => 30);
|
|
my $url = "$PORTAL_BASE/keys/validate";
|
|
my $req = HTTP::Request->new('POST', $url);
|
|
|
|
$req->header('Authorization' => "Bearer $pk");
|
|
$req->header('Content-Type' => 'application/json');
|
|
|
|
if ($sk) {
|
|
my $timestamp = int(time);
|
|
my $sig_input = "$timestamp:POST:/keys/validate:";
|
|
my $signature = hmac_sign($sk, $sig_input);
|
|
$req->header('X-Timestamp' => $timestamp);
|
|
$req->header('X-Signature' => $signature);
|
|
}
|
|
|
|
my $res = $ua->request($req);
|
|
return decode_json($res->content) if $res->content;
|
|
return undef;
|
|
}
|
|
|
|
sub health_check {
|
|
my (%opts) = @_;
|
|
my $ua = LWP::UserAgent->new(timeout => 10);
|
|
my $res = $ua->get("$API_BASE/health");
|
|
return $res->is_success ? 1 : 0;
|
|
}
|
|
|
|
# ============================================================================
|
|
# CLI Implementation
|
|
# ============================================================================
|
|
|
|
package main;
|
|
|
|
sub build_input_files {
|
|
my @files = @_;
|
|
my @input_files;
|
|
foreach my $filepath (@files) {
|
|
unless (-e $filepath) {
|
|
print STDERR "${RED}Error: Input file not found: $filepath${RESET}\n";
|
|
exit 1;
|
|
}
|
|
open my $f, '<:raw', $filepath or die "Cannot read file: $!";
|
|
my $content = do { local $/; <$f> };
|
|
close $f;
|
|
push @input_files, {
|
|
filename => basename($filepath),
|
|
content_base64 => encode_base64($content, '')
|
|
};
|
|
}
|
|
return \@input_files;
|
|
}
|
|
|
|
sub build_env_content {
|
|
my ($envs, $env_file) = @_;
|
|
my @parts;
|
|
|
|
if ($env_file && -e $env_file) {
|
|
open my $fh, '<', $env_file or die "Cannot read file: $!";
|
|
my $content = do { local $/; <$fh> };
|
|
close $fh;
|
|
push @parts, $content;
|
|
}
|
|
|
|
foreach my $e (@$envs) {
|
|
push @parts, $e if $e =~ /=/;
|
|
}
|
|
|
|
return join("\n", @parts);
|
|
}
|
|
|
|
sub cmd_execute {
|
|
my ($options) = @_;
|
|
my $source = $options->{source_file};
|
|
my $inline_code = $options->{inline_code};
|
|
my $language = $options->{language};
|
|
|
|
my $code;
|
|
if ($inline_code) {
|
|
$code = $inline_code;
|
|
} else {
|
|
unless (-e $source) {
|
|
print STDERR "${RED}Error: File not found: $source${RESET}\n";
|
|
exit 1;
|
|
}
|
|
open my $fh, '<', $source or die "Cannot read file: $!";
|
|
$code = do { local $/; <$fh> };
|
|
close $fh;
|
|
$language ||= Un::detect_language($source);
|
|
}
|
|
|
|
unless ($language) {
|
|
print STDERR "${RED}Error: Could not detect language${RESET}\n";
|
|
exit 1;
|
|
}
|
|
|
|
my %opts = (network_mode => $options->{network} || 'zerotrust');
|
|
$opts{vcpu} = $options->{vcpu} if $options->{vcpu};
|
|
|
|
if ($options->{env} && @{$options->{env}}) {
|
|
my %env_vars;
|
|
foreach my $e (@{$options->{env}}) {
|
|
if ($e =~ /^([^=]+)=(.*)$/) {
|
|
$env_vars{$1} = $2;
|
|
}
|
|
}
|
|
$opts{env} = \%env_vars if %env_vars;
|
|
}
|
|
|
|
if ($options->{files} && @{$options->{files}}) {
|
|
$opts{input_files} = build_input_files(@{$options->{files}});
|
|
}
|
|
|
|
$opts{return_artifacts} = 1 if $options->{artifacts};
|
|
|
|
my $result = Un::execute($language, $code, %opts);
|
|
|
|
unless ($result) {
|
|
print STDERR "${RED}Error: " . Un::last_error() . "${RESET}\n";
|
|
exit 1;
|
|
}
|
|
|
|
print $result->{stdout} if $result->{stdout};
|
|
print STDERR $result->{stderr} if $result->{stderr};
|
|
|
|
if ($options->{artifacts} && $result->{artifacts}) {
|
|
my $out_dir = $options->{output_dir} || '.';
|
|
make_path($out_dir) unless -d $out_dir;
|
|
foreach my $artifact (@{$result->{artifacts}}) {
|
|
my $filename = $artifact->{filename} || 'artifact';
|
|
my $content = decode_base64($artifact->{content_base64});
|
|
my $filepath = "$out_dir/$filename";
|
|
open my $f, '>:raw', $filepath or die "Cannot write file: $!";
|
|
print $f $content;
|
|
close $f;
|
|
chmod 0755, $filepath;
|
|
print STDERR "${GREEN}Saved: $filepath${RESET}\n";
|
|
}
|
|
}
|
|
|
|
exit($result->{exit_code} || 0);
|
|
}
|
|
|
|
sub cmd_session {
|
|
my ($options) = @_;
|
|
|
|
if ($options->{list}) {
|
|
my $result = Un::session_list();
|
|
my $sessions = $result->{sessions} || [];
|
|
if (@$sessions == 0) {
|
|
print "No active sessions\n";
|
|
} else {
|
|
printf "%-40s %-10s %-10s %s\n", 'ID', 'Shell', 'Status', 'Created';
|
|
foreach my $s (@$sessions) {
|
|
printf "%-40s %-10s %-10s %s\n",
|
|
$s->{id} // 'N/A', $s->{shell} // 'N/A',
|
|
$s->{status} // 'N/A', $s->{created_at} // 'N/A';
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ($options->{kill}) {
|
|
Un::session_destroy($options->{kill});
|
|
print "${GREEN}Session terminated: $options->{kill}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{info}) {
|
|
my $result = Un::session_get($options->{info});
|
|
print encode_json($result) . "\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{freeze}) {
|
|
Un::session_freeze($options->{freeze});
|
|
print "${GREEN}Session frozen: $options->{freeze}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{unfreeze}) {
|
|
Un::session_unfreeze($options->{unfreeze});
|
|
print "${GREEN}Session unfreezing: $options->{unfreeze}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{boost}) {
|
|
my $vcpu = $options->{vcpu} || 2;
|
|
Un::session_boost($options->{boost}, $vcpu);
|
|
print "${GREEN}Session boosted: $options->{boost}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{unboost}) {
|
|
Un::session_unboost($options->{unboost});
|
|
print "${GREEN}Session unboosted: $options->{unboost}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{execute}) {
|
|
my $result = Un::session_execute($options->{execute}, $options->{command});
|
|
print $result->{stdout} if $result->{stdout};
|
|
print STDERR $result->{stderr} if $result->{stderr};
|
|
return;
|
|
}
|
|
|
|
if ($options->{snapshot}) {
|
|
my $result = Un::snapshot_session($options->{snapshot},
|
|
name => $options->{snapshot_name}, hot => $options->{hot});
|
|
print "${GREEN}Snapshot created${RESET}\n";
|
|
print encode_json($result) . "\n";
|
|
return;
|
|
}
|
|
|
|
# Create new session
|
|
my %opts = (shell => $options->{shell} || 'bash');
|
|
$opts{network} = $options->{network} if $options->{network};
|
|
$opts{vcpu} = $options->{vcpu} if $options->{vcpu};
|
|
$opts{persistence} = 'tmux' if $options->{tmux};
|
|
$opts{persistence} = 'screen' if $options->{screen};
|
|
|
|
if ($options->{files} && @{$options->{files}}) {
|
|
$opts{input_files} = build_input_files(@{$options->{files}});
|
|
}
|
|
|
|
print "${YELLOW}Creating session...${RESET}\n";
|
|
my $result = Un::session_create(%opts);
|
|
print "${GREEN}Session created: " . ($result->{id} // 'N/A') . "${RESET}\n";
|
|
print "${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}\n";
|
|
}
|
|
|
|
sub cmd_service {
|
|
my ($options) = @_;
|
|
|
|
if ($options->{list}) {
|
|
my $result = Un::service_list();
|
|
my $services = $result->{services} || [];
|
|
if (@$services == 0) {
|
|
print "No services\n";
|
|
} else {
|
|
printf "%-20s %-15s %-10s %-15s %s\n", 'ID', 'Name', 'Status', 'Ports', 'Domains';
|
|
foreach my $s (@$services) {
|
|
my $ports = join(',', @{$s->{ports} || []});
|
|
my $domains = join(',', @{$s->{domains} || []});
|
|
printf "%-20s %-15s %-10s %-15s %s\n",
|
|
$s->{id} // 'N/A', $s->{name} // 'N/A',
|
|
$s->{status} // 'N/A', $ports, $domains;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ($options->{info}) {
|
|
my $result = Un::service_get($options->{info});
|
|
print encode_json($result) . "\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{logs}) {
|
|
my $result = Un::service_logs($options->{logs}, lines => $options->{lines});
|
|
print $result->{logs} // '';
|
|
return;
|
|
}
|
|
|
|
if ($options->{freeze}) {
|
|
Un::service_freeze($options->{freeze});
|
|
print "${GREEN}Service frozen: $options->{freeze}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{unfreeze}) {
|
|
Un::service_unfreeze($options->{unfreeze});
|
|
print "${GREEN}Service unfreezing: $options->{unfreeze}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{lock}) {
|
|
Un::service_lock($options->{lock});
|
|
print "${GREEN}Service locked: $options->{lock}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{unlock}) {
|
|
Un::service_unlock($options->{unlock});
|
|
print "${GREEN}Service unlocked: $options->{unlock}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{destroy}) {
|
|
Un::service_destroy($options->{destroy});
|
|
print "${GREEN}Service destroyed: $options->{destroy}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{resize}) {
|
|
unless ($options->{vcpu}) {
|
|
print STDERR "${RED}Error: --vcpu is required with --resize${RESET}\n";
|
|
exit 1;
|
|
}
|
|
Un::service_resize($options->{resize}, $options->{vcpu});
|
|
my $ram = $options->{vcpu} * 2;
|
|
print "${GREEN}Service resized to $options->{vcpu} vCPU, $ram GB RAM${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{redeploy}) {
|
|
Un::service_redeploy($options->{redeploy}, bootstrap => $options->{bootstrap});
|
|
print "${GREEN}Service redeployed: $options->{redeploy}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{execute}) {
|
|
my $result = Un::service_execute($options->{execute}, $options->{command});
|
|
print $result->{stdout} if $result->{stdout};
|
|
print STDERR $result->{stderr} if $result->{stderr};
|
|
return;
|
|
}
|
|
|
|
if ($options->{set_unfreeze_on_demand}) {
|
|
my $enabled = $options->{set_unfreeze_on_demand_value};
|
|
Un::service_set_unfreeze_on_demand($options->{set_unfreeze_on_demand}, $enabled);
|
|
my $status = $enabled ? 'enabled' : 'disabled';
|
|
print "${GREEN}Unfreeze-on-demand $status${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{snapshot}) {
|
|
my $result = Un::snapshot_service($options->{snapshot},
|
|
name => $options->{snapshot_name}, hot => $options->{hot});
|
|
print "${GREEN}Snapshot created${RESET}\n";
|
|
print encode_json($result) . "\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{name}) {
|
|
my %opts = (name => $options->{name});
|
|
if ($options->{ports}) {
|
|
$opts{ports} = [map { int($_) } split(',', $options->{ports})];
|
|
}
|
|
if ($options->{domains}) {
|
|
$opts{domains} = [split(',', $options->{domains})];
|
|
}
|
|
$opts{service_type} = $options->{type} if $options->{type};
|
|
$opts{bootstrap} = $options->{bootstrap} if $options->{bootstrap};
|
|
|
|
if ($options->{bootstrap_file}) {
|
|
if (! -e $options->{bootstrap_file}) {
|
|
print STDERR "${RED}Error: Bootstrap file not found${RESET}\n";
|
|
exit 1;
|
|
}
|
|
open my $fh, '<', $options->{bootstrap_file};
|
|
$opts{bootstrap_content} = do { local $/; <$fh> };
|
|
close $fh;
|
|
}
|
|
|
|
if ($options->{files} && @{$options->{files}}) {
|
|
$opts{input_files} = build_input_files(@{$options->{files}});
|
|
}
|
|
|
|
$opts{network} = $options->{network} if $options->{network};
|
|
$opts{vcpu} = $options->{vcpu} if $options->{vcpu};
|
|
$opts{unfreeze_on_demand} = 1 if $options->{unfreeze_on_demand};
|
|
|
|
my $result = Un::service_create(%opts);
|
|
print "${GREEN}Service created: " . ($result->{id} // 'N/A') . "${RESET}\n";
|
|
print "Name: " . ($result->{name} // 'N/A') . "\n";
|
|
print "URL: $result->{url}\n" if $result->{url};
|
|
|
|
# Auto-set vault
|
|
my $env_content = build_env_content($options->{env} || [], $options->{env_file});
|
|
if ($env_content && $result->{id}) {
|
|
Un::service_env_set($result->{id}, $env_content);
|
|
print "${GREEN}Vault configured${RESET}\n";
|
|
}
|
|
return;
|
|
}
|
|
|
|
print STDERR "${RED}Error: Specify --name to create or use --list, --info, etc.${RESET}\n";
|
|
exit 1;
|
|
}
|
|
|
|
sub cmd_service_env {
|
|
my ($action, $target, $envs, $env_file) = @_;
|
|
|
|
unless ($target) {
|
|
print STDERR "${RED}Error: Service ID required${RESET}\n";
|
|
exit 1;
|
|
}
|
|
|
|
if ($action eq 'status') {
|
|
my $result = Un::service_env_get($target);
|
|
print encode_json($result) . "\n";
|
|
} elsif ($action eq 'set') {
|
|
my $content = build_env_content($envs, $env_file);
|
|
unless ($content) {
|
|
print STDERR "${RED}Error: No env content provided${RESET}\n";
|
|
exit 1;
|
|
}
|
|
Un::service_env_set($target, $content);
|
|
print "${GREEN}Vault updated${RESET}\n";
|
|
} elsif ($action eq 'export') {
|
|
my $result = Un::service_env_export($target);
|
|
print $result->{env} // $result->{content} // '';
|
|
} elsif ($action eq 'delete') {
|
|
Un::service_env_delete($target);
|
|
print "${GREEN}Vault deleted${RESET}\n";
|
|
} else {
|
|
print STDERR "${RED}Error: Unknown env action '$action'${RESET}\n";
|
|
exit 1;
|
|
}
|
|
}
|
|
|
|
sub cmd_snapshot {
|
|
my ($options) = @_;
|
|
|
|
if ($options->{list}) {
|
|
my $result = Un::snapshot_list();
|
|
my $snapshots = $result->{snapshots} || [];
|
|
if (@$snapshots == 0) {
|
|
print "No snapshots\n";
|
|
} else {
|
|
printf "%-40s %-20s %-10s %s\n", 'ID', 'Name', 'Type', 'Created';
|
|
foreach my $s (@$snapshots) {
|
|
printf "%-40s %-20s %-10s %s\n",
|
|
$s->{id} // 'N/A', $s->{name} // 'N/A',
|
|
$s->{type} // 'N/A', $s->{created_at} // 'N/A';
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ($options->{info}) {
|
|
my $result = Un::snapshot_get($options->{info});
|
|
print encode_json($result) . "\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{delete}) {
|
|
Un::snapshot_delete($options->{delete});
|
|
print "${GREEN}Snapshot deleted: $options->{delete}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{restore}) {
|
|
Un::snapshot_restore($options->{restore});
|
|
print "${GREEN}Snapshot restored${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{lock}) {
|
|
Un::snapshot_lock($options->{lock});
|
|
print "${GREEN}Snapshot locked: $options->{lock}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{unlock}) {
|
|
Un::snapshot_unlock($options->{unlock});
|
|
print "${GREEN}Snapshot unlocked: $options->{unlock}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{clone}) {
|
|
my $result = Un::snapshot_clone($options->{clone},
|
|
clone_type => $options->{clone_type} || 'session',
|
|
name => $options->{clone_name});
|
|
print "${GREEN}Snapshot cloned${RESET}\n";
|
|
print encode_json($result) . "\n";
|
|
return;
|
|
}
|
|
|
|
print STDERR "${RED}Error: Use --list, --info, --delete, --restore, --lock, --unlock, or --clone${RESET}\n";
|
|
exit 1;
|
|
}
|
|
|
|
sub cmd_image {
|
|
my ($options) = @_;
|
|
|
|
if ($options->{list}) {
|
|
my $result = Un::image_list(filter => $options->{filter});
|
|
my $images = $result->{images} || [];
|
|
if (@$images == 0) {
|
|
print "No images\n";
|
|
} else {
|
|
printf "%-40s %-20s %-10s %s\n", 'ID', 'Name', 'Visibility', 'Created';
|
|
foreach my $i (@$images) {
|
|
printf "%-40s %-20s %-10s %s\n",
|
|
$i->{id} // 'N/A', $i->{name} // 'N/A',
|
|
$i->{visibility} // 'N/A', $i->{created_at} // 'N/A';
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ($options->{info}) {
|
|
my $result = Un::image_get($options->{info});
|
|
print encode_json($result) . "\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{delete}) {
|
|
Un::image_delete($options->{delete});
|
|
print "${GREEN}Image deleted: $options->{delete}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{lock}) {
|
|
Un::image_lock($options->{lock});
|
|
print "${GREEN}Image locked: $options->{lock}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{unlock}) {
|
|
Un::image_unlock($options->{unlock});
|
|
print "${GREEN}Image unlocked: $options->{unlock}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{publish}) {
|
|
unless ($options->{source_type}) {
|
|
print STDERR "${RED}Error: --source-type required${RESET}\n";
|
|
exit 1;
|
|
}
|
|
my $result = Un::image_publish(
|
|
source_type => $options->{source_type},
|
|
source_id => $options->{publish},
|
|
name => $options->{pub_name});
|
|
print "${GREEN}Image published${RESET}\n";
|
|
print encode_json($result) . "\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{visibility_id} && $options->{visibility_mode}) {
|
|
Un::image_set_visibility($options->{visibility_id}, $options->{visibility_mode});
|
|
print "${GREEN}Visibility set to $options->{visibility_mode}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{grant_access}) {
|
|
Un::image_grant_access($options->{grant_access}, $options->{trusted_key});
|
|
print "${GREEN}Access granted${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{revoke_access}) {
|
|
Un::image_revoke_access($options->{revoke_access}, $options->{trusted_key});
|
|
print "${GREEN}Access revoked${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{list_trusted}) {
|
|
my $result = Un::image_list_trusted($options->{list_trusted});
|
|
print encode_json($result) . "\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{transfer}) {
|
|
Un::image_transfer($options->{transfer}, $options->{to_key});
|
|
print "${GREEN}Image transferred${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{spawn}) {
|
|
my $result = Un::image_spawn($options->{spawn},
|
|
name => $options->{spawn_name},
|
|
ports => $options->{spawn_ports} ? [map { int($_) } split(',', $options->{spawn_ports})] : undef);
|
|
print "${GREEN}Service spawned from image${RESET}\n";
|
|
print encode_json($result) . "\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{clone}) {
|
|
my $result = Un::image_clone($options->{clone}, name => $options->{clone_name});
|
|
print "${GREEN}Image cloned${RESET}\n";
|
|
print encode_json($result) . "\n";
|
|
return;
|
|
}
|
|
|
|
print STDERR "${RED}Error: Use --list, --info, --delete, etc.${RESET}\n";
|
|
exit 1;
|
|
}
|
|
|
|
sub cmd_key {
|
|
my ($options) = @_;
|
|
my $result = Un::validate_keys();
|
|
|
|
if ($options->{extend}) {
|
|
my $pk = $result->{public_key};
|
|
if ($pk) {
|
|
my $url = "$Un::PORTAL_BASE/keys/extend?pk=$pk";
|
|
print "${BLUE}Opening browser to extend key...${RESET}\n";
|
|
system("xdg-open '$url' 2>/dev/null || open '$url' 2>/dev/null &");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ($result->{expired}) {
|
|
print "${RED}Expired${RESET}\n";
|
|
print "Public Key: " . ($result->{public_key} // 'N/A') . "\n";
|
|
print "Tier: " . ($result->{tier} // 'N/A') . "\n";
|
|
print "${YELLOW}To renew: Visit $Un::PORTAL_BASE/keys/extend${RESET}\n";
|
|
exit 1;
|
|
}
|
|
|
|
print "${GREEN}Valid${RESET}\n";
|
|
print "Public Key: " . ($result->{public_key} // 'N/A') . "\n";
|
|
print "Tier: " . ($result->{tier} // 'N/A') . "\n";
|
|
print "Status: " . ($result->{status} // 'N/A') . "\n";
|
|
print "Expires: " . ($result->{expires_at} // 'N/A') . "\n";
|
|
print "Time Remaining: " . ($result->{time_remaining} // 'N/A') . "\n";
|
|
}
|
|
|
|
sub cmd_languages {
|
|
my ($options) = @_;
|
|
my $langs = Un::get_languages();
|
|
|
|
if ($options->{json}) {
|
|
print encode_json($langs) . "\n";
|
|
} else {
|
|
foreach my $lang (@$langs) {
|
|
print "$lang\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
sub show_help {
|
|
print <<"HELP";
|
|
Unsandbox CLI - Execute code in secure sandboxes
|
|
|
|
Usage:
|
|
perl un.pl [options] <source_file>
|
|
perl un.pl -s <language> '<code>'
|
|
perl un.pl session [options]
|
|
perl un.pl service [options]
|
|
perl un.pl service env <action> <service_id> [options]
|
|
perl un.pl snapshot [options]
|
|
perl un.pl image [options]
|
|
perl un.pl languages [--json]
|
|
perl un.pl key [--extend]
|
|
|
|
Execute options:
|
|
-e KEY=VALUE Environment variable (multiple allowed)
|
|
-f FILE Input file (multiple allowed)
|
|
-a Return artifacts
|
|
-o DIR Output directory for artifacts
|
|
-n MODE Network mode (zerotrust|semitrusted)
|
|
-v N vCPU count (1-8)
|
|
-s LANG Language for inline code
|
|
|
|
Session options:
|
|
--list List sessions
|
|
--info ID Get session details
|
|
--kill ID Terminate session
|
|
--freeze ID Freeze session
|
|
--unfreeze ID Unfreeze session
|
|
--boost ID Boost session (with -v)
|
|
--unboost ID Unboost session
|
|
--execute ID Execute command (with --command)
|
|
--snapshot ID Create snapshot
|
|
--shell SHELL Shell/REPL (default: bash)
|
|
--tmux Enable tmux persistence
|
|
--screen Enable screen persistence
|
|
|
|
Service options:
|
|
--list List services
|
|
--info ID Get service details
|
|
--name NAME Create service with name
|
|
--ports PORTS Comma-separated ports
|
|
--domains DOMS Custom domains
|
|
--bootstrap CMD Bootstrap command
|
|
--bootstrap-file Bootstrap script file
|
|
--logs ID Get logs
|
|
--freeze ID Freeze service
|
|
--unfreeze ID Unfreeze service
|
|
--lock ID Lock service
|
|
--unlock ID Unlock service
|
|
--destroy ID Destroy service
|
|
--resize ID Resize (with -v)
|
|
--redeploy ID Redeploy service
|
|
--execute ID Execute command (with --command)
|
|
--snapshot ID Create snapshot
|
|
|
|
Service env commands:
|
|
env status ID Check vault status
|
|
env set ID Set vault (use -e or --env-file)
|
|
env export ID Export vault contents
|
|
env delete ID Delete vault
|
|
|
|
Snapshot options:
|
|
--list List snapshots
|
|
--info ID Get snapshot details
|
|
--delete ID Delete snapshot
|
|
--restore ID Restore from snapshot
|
|
--lock ID Lock snapshot
|
|
--unlock ID Unlock snapshot
|
|
--clone ID Clone snapshot (with --clone-type, --clone-name)
|
|
|
|
Image options:
|
|
--list List images (with optional --filter)
|
|
--info ID Get image details
|
|
--delete ID Delete image
|
|
--lock ID Lock image
|
|
--unlock ID Unlock image
|
|
--publish ID Publish from service/snapshot (--source-type)
|
|
--visibility ID MODE Set visibility (private|unlisted|public)
|
|
--grant-access ID Grant access (with --trusted-key)
|
|
--revoke-access ID Revoke access (with --trusted-key)
|
|
--list-trusted ID List trusted keys
|
|
--transfer ID Transfer ownership (with --to-key)
|
|
--spawn ID Spawn service from image
|
|
--clone ID Clone image
|
|
HELP
|
|
exit 1;
|
|
}
|
|
|
|
sub main {
|
|
my %options = (
|
|
env => [],
|
|
files => []
|
|
);
|
|
|
|
my $i = 0;
|
|
while ($i < @ARGV) {
|
|
my $arg = $ARGV[$i];
|
|
|
|
if ($arg eq 'session' || $arg eq 'service' || $arg eq 'snapshot' ||
|
|
$arg eq 'image' || $arg eq 'key' || $arg eq 'languages') {
|
|
$options{command} = $arg;
|
|
} elsif ($arg eq '-e') {
|
|
push @{$options{env}}, $ARGV[++$i];
|
|
} elsif ($arg eq '-f') {
|
|
push @{$options{files}}, $ARGV[++$i];
|
|
} elsif ($arg eq '-a') {
|
|
$options{artifacts} = 1;
|
|
} elsif ($arg eq '-o') {
|
|
$options{output_dir} = $ARGV[++$i];
|
|
} elsif ($arg eq '-n') {
|
|
$options{network} = $ARGV[++$i];
|
|
} elsif ($arg eq '-v') {
|
|
$options{vcpu} = int($ARGV[++$i]);
|
|
} elsif ($arg eq '-s' || $arg eq '--shell') {
|
|
$options{language} = $ARGV[++$i] if !$options{command};
|
|
$options{shell} = $ARGV[$i] if $options{command} && $options{command} eq 'session';
|
|
} elsif ($arg eq '-l' || $arg eq '--list') {
|
|
$options{list} = 1;
|
|
} elsif ($arg eq '--info') {
|
|
$options{info} = $ARGV[++$i];
|
|
} elsif ($arg eq '--kill') {
|
|
$options{kill} = $ARGV[++$i];
|
|
} elsif ($arg eq '--freeze') {
|
|
$options{freeze} = $ARGV[++$i];
|
|
} elsif ($arg eq '--unfreeze') {
|
|
$options{unfreeze} = $ARGV[++$i];
|
|
} elsif ($arg eq '--lock') {
|
|
$options{lock} = $ARGV[++$i];
|
|
} elsif ($arg eq '--unlock') {
|
|
$options{unlock} = $ARGV[++$i];
|
|
} elsif ($arg eq '--boost') {
|
|
$options{boost} = $ARGV[++$i];
|
|
} elsif ($arg eq '--unboost') {
|
|
$options{unboost} = $ARGV[++$i];
|
|
} elsif ($arg eq '--destroy') {
|
|
$options{destroy} = $ARGV[++$i];
|
|
} elsif ($arg eq '--resize') {
|
|
$options{resize} = $ARGV[++$i];
|
|
} elsif ($arg eq '--redeploy') {
|
|
$options{redeploy} = $ARGV[++$i];
|
|
} elsif ($arg eq '--logs') {
|
|
$options{logs} = $ARGV[++$i];
|
|
} elsif ($arg eq '--lines') {
|
|
$options{lines} = $ARGV[++$i];
|
|
} elsif ($arg eq '--execute') {
|
|
$options{execute} = $ARGV[++$i];
|
|
} elsif ($arg eq '--command') {
|
|
$options{command_str} = $ARGV[++$i];
|
|
} elsif ($arg eq '--name') {
|
|
$options{name} = $ARGV[++$i];
|
|
} elsif ($arg eq '--ports') {
|
|
$options{ports} = $ARGV[++$i];
|
|
} elsif ($arg eq '--domains') {
|
|
$options{domains} = $ARGV[++$i];
|
|
} elsif ($arg eq '--type') {
|
|
$options{type} = $ARGV[++$i];
|
|
} elsif ($arg eq '--bootstrap') {
|
|
$options{bootstrap} = $ARGV[++$i];
|
|
} elsif ($arg eq '--bootstrap-file') {
|
|
$options{bootstrap_file} = $ARGV[++$i];
|
|
} elsif ($arg eq '--env-file') {
|
|
$options{env_file} = $ARGV[++$i];
|
|
} elsif ($arg eq '--snapshot') {
|
|
$options{snapshot} = $ARGV[++$i];
|
|
} elsif ($arg eq '--snapshot-name') {
|
|
$options{snapshot_name} = $ARGV[++$i];
|
|
} elsif ($arg eq '--hot') {
|
|
$options{hot} = 1;
|
|
} elsif ($arg eq '--restore') {
|
|
$options{restore} = $ARGV[++$i];
|
|
} elsif ($arg eq '--delete') {
|
|
$options{delete} = $ARGV[++$i];
|
|
} elsif ($arg eq '--clone') {
|
|
$options{clone} = $ARGV[++$i];
|
|
} elsif ($arg eq '--clone-type') {
|
|
$options{clone_type} = $ARGV[++$i];
|
|
} elsif ($arg eq '--clone-name') {
|
|
$options{clone_name} = $ARGV[++$i];
|
|
} elsif ($arg eq '--publish') {
|
|
$options{publish} = $ARGV[++$i];
|
|
} elsif ($arg eq '--source-type') {
|
|
$options{source_type} = $ARGV[++$i];
|
|
} elsif ($arg eq '--visibility') {
|
|
$options{visibility_id} = $ARGV[++$i];
|
|
$options{visibility_mode} = $ARGV[++$i];
|
|
} elsif ($arg eq '--spawn') {
|
|
$options{spawn} = $ARGV[++$i];
|
|
} elsif ($arg eq '--grant-access') {
|
|
$options{grant_access} = $ARGV[++$i];
|
|
} elsif ($arg eq '--revoke-access') {
|
|
$options{revoke_access} = $ARGV[++$i];
|
|
} elsif ($arg eq '--list-trusted') {
|
|
$options{list_trusted} = $ARGV[++$i];
|
|
} elsif ($arg eq '--trusted-key') {
|
|
$options{trusted_key} = $ARGV[++$i];
|
|
} elsif ($arg eq '--transfer') {
|
|
$options{transfer} = $ARGV[++$i];
|
|
} elsif ($arg eq '--to-key') {
|
|
$options{to_key} = $ARGV[++$i];
|
|
} elsif ($arg eq '--filter') {
|
|
$options{filter} = $ARGV[++$i];
|
|
} elsif ($arg eq '--tmux') {
|
|
$options{tmux} = 1;
|
|
} elsif ($arg eq '--screen') {
|
|
$options{screen} = 1;
|
|
} elsif ($arg eq '--unfreeze-on-demand') {
|
|
$options{unfreeze_on_demand} = 1;
|
|
} elsif ($arg eq '--set-unfreeze-on-demand') {
|
|
$options{set_unfreeze_on_demand} = $ARGV[++$i];
|
|
$options{set_unfreeze_on_demand_value} = ($ARGV[++$i] =~ /^(true|1)$/i);
|
|
} elsif ($arg eq '--json') {
|
|
$options{json} = 1;
|
|
} elsif ($arg eq '--extend') {
|
|
$options{extend} = 1;
|
|
} elsif ($arg eq 'env' && $options{command} && $options{command} eq 'service') {
|
|
$options{env_action} = $ARGV[++$i];
|
|
$options{env_target} = $ARGV[++$i] if defined $ARGV[$i+1] && $ARGV[$i+1] !~ /^-/;
|
|
} elsif ($arg eq '--help' || $arg eq '-h') {
|
|
show_help();
|
|
} elsif ($arg =~ /^-/) {
|
|
print STDERR "${RED}Unknown option: $arg${RESET}\n";
|
|
exit 1;
|
|
} else {
|
|
if ($options{language} && !$options{inline_code}) {
|
|
$options{inline_code} = $arg;
|
|
} else {
|
|
$options{source_file} = $arg;
|
|
}
|
|
}
|
|
$i++;
|
|
}
|
|
|
|
# Handle commands
|
|
if ($options{command}) {
|
|
if ($options{command} eq 'session') {
|
|
cmd_session(\%options);
|
|
} elsif ($options{command} eq 'service') {
|
|
if ($options{env_action}) {
|
|
cmd_service_env($options{env_action}, $options{env_target},
|
|
$options{env}, $options{env_file});
|
|
} else {
|
|
$options{command} = $options{command_str} if $options{command_str};
|
|
cmd_service(\%options);
|
|
}
|
|
} elsif ($options{command} eq 'snapshot') {
|
|
cmd_snapshot(\%options);
|
|
} elsif ($options{command} eq 'image') {
|
|
cmd_image(\%options);
|
|
} elsif ($options{command} eq 'languages') {
|
|
cmd_languages(\%options);
|
|
} elsif ($options{command} eq 'key') {
|
|
cmd_key(\%options);
|
|
}
|
|
} elsif ($options{source_file} || $options{inline_code}) {
|
|
cmd_execute(\%options);
|
|
} else {
|
|
show_help();
|
|
}
|
|
}
|
|
|
|
main() unless caller;
|
|
1;
|