@ -53,11 +53,29 @@ SPORESTACK_USE_TOR_ENDPOINT
TOR_PROXY ( defaults to socks5h : / / 127.0 .0 .1 : 9050 which is fine for most )
"""
_home = os . getenv ( " HOME " , None )
assert _home is not None , " Unable to detect $HOME environment variable? "
HOME = Path ( _home )
SPORESTACK_DIR = HOME / " .sporestack "
cli = typer . Typer ( help = HELP )
token_cli = typer . Typer ( )
HOME = Path ( _home )
cli . add_typer ( token_cli , name = " token " )
server_cli = typer . Typer ( )
cli . add_typer ( server_cli , name = " server " )
logging . basicConfig ( level = logging . INFO )
DEFAULT_TOKEN = " primary "
DEFAULT_FLAVOR = " vps-1vcpu-1gb "
# Users may have a different key file, but this is the most common.
DEFAULT_SSH_KEY_FILE = HOME / " .ssh " / " id_rsa.pub "
# On disk format
TOKEN_VERSION = 1
WAITING_PAYMENT_TO_PROCESS = " Waiting for payment to process... "
@ -86,15 +104,14 @@ Press ctrl+c to abort."""
input ( " [Press enter once you have made payment.] " )
@ cli.command( )
@ server_ cli.command( )
def launch (
hostname : str ,
days : int = typer . Option ( . . . ) ,
ssh_key_file : Path = typer . Option ( . . . ) ,
operating_system : str = typer . Option ( . . . ) ,
ssh_key_file : Path = DEFAULT_SSH_KEY_FILE ,
flavor : str = DEFAULT_FLAVOR ,
currency : Optional [ str ] = None ,
settlement_token : Optional [ str ] = None ,
token : str = DEFAULT_TOKEN ,
region : Optional [ str ] = None ,
) - > None :
"""
@ -103,70 +120,37 @@ def launch(
from . import utils
if settlement_token is not None :
if currency is None or currency == " settlement " :
currency = " settlement "
else :
msg = " Cannot use non-settlement --currency with --settlement-token "
typer . echo ( msg , err = True )
raise typer . Exit ( code = 2 )
if currency is None :
typer . echo ( " --currency must be set. " , err = True )
raise typer . Exit ( code = 2 )
typer . echo ( f " Launching server with token { token } ... " , err = True )
_token = load_token ( token )
if machine_exists ( hostname ) :
typer . echo ( f " { hostname } already created. " )
raise typer . Exit ( code = 1 )
typer . echo ( f " Loading SSH key from { ssh_key_file } ... " )
if not ssh_key_file . exists ( ) :
msg = f " { ssh_key_file } does not exist. "
msg + = " You can try generating a key file with `ssh-keygen` "
typer . echo ( msg , err = True )
raise typer . Exit ( code = 1 )
ssh_key = ssh_key_file . read_text ( )
machine_id = utils . random_machine_id ( )
assert currency is not None
response = api_client . launch (
machine_id = machine_id ,
days = days ,
flavor = flavor ,
operating_system = operating_system ,
ssh_key = ssh_key ,
currency = currency ,
currency = " settlement " ,
region = region ,
settlement_ token= settlement _token,
token= _token,
api_endpoint = get_api_endpoint ( ) ,
retry = True ,
)
# This will be false at least the first time if paying with BTC or BCH.
if response . payment . paid is False :
assert response . payment . uri is not None
make_payment (
currency = currency ,
uri = response . payment . uri ,
usd = response . payment . usd ,
)
tries = 360
while tries > 0 :
tries = tries - 1
typer . echo ( WAITING_PAYMENT_TO_PROCESS , err = True )
# FIXME: Wait one hour in a smarter way.
# Waiting for payment to set in.
time . sleep ( 10 )
response = api_client . launch (
machine_id = machine_id ,
days = days ,
flavor = flavor ,
operating_system = operating_system ,
ssh_key = ssh_key ,
currency = currency ,
region = region ,
settlement_token = settlement_token ,
api_endpoint = get_api_endpoint ( ) ,
retry = True ,
)
if response . payment . paid is True :
break
if response . created is False :
tries = 360
while tries > 0 :
@ -180,9 +164,9 @@ def launch(
flavor = flavor ,
operating_system = operating_system ,
ssh_key = ssh_key ,
currency = currency ,
currency = " settlement " ,
region = region ,
settlement_ token= settlement _token,
token= _token,
api_endpoint = get_api_endpoint ( ) ,
retry = True ,
)
@ -200,103 +184,69 @@ def launch(
typer . echo ( json . dumps ( created_dict , indent = 4 ) )
@ cli.command( )
@ server_ cli.command( )
def topup (
hostname : str ,
days : int = typer . Option ( . . . ) ,
currency : Optional [ str ] = None ,
settlement_token : Optional [ str ] = None ,
token : str = DEFAULT_TOKEN ,
) - > None :
"""
tops up an existing vm .
"""
if settlement_token is not None :
if currency is None or currency == " settlement " :
currency = " settlement "
else :
msg = " Cannot use non-settlement --currency with --settlement-token "
typer . echo ( msg , err = True )
raise typer . Exit ( code = 2 )
if currency is None :
typer . echo ( " --currency must be set. " , err = True )
raise typer . Exit ( code = 2 )
if not machine_exists ( hostname ) :
typer . echo ( f " { hostname } does not exist. " )
raise typer . Exit ( code = 1 )
_token = load_token ( token )
machine_info = get_machine_info ( hostname )
machine_id = machine_info [ " machine_id " ]
assert currency is not None
response = api_client . topup (
machine_id = machine_id ,
days = days ,
currency = currency ,
currency = " settlement " ,
api_endpoint = get_api_endpoint ( ) ,
settlement_ token= settlement _token,
token= _token,
retry = True ,
)
# This will be false at least the first time if paying with anything
# but settlement.
if response . payment . paid is False :
assert response . payment . uri is not None
make_payment (
currency = currency ,
uri = response . payment . uri ,
usd = response . payment . usd ,
)
tries = 360
while tries > 0 :
typer . echo ( WAITING_PAYMENT_TO_PROCESS , err = True )
tries = tries - 1
# FIXME: Wait one hour in a smarter way.
# Waiting for payment to set in.
time . sleep ( 10 )
response = api_client . topup (
machine_id = machine_id ,
days = days ,
currency = currency ,
api_endpoint = get_api_endpoint ( ) ,
settlement_token = settlement_token ,
retry = True ,
)
if response . payment . paid is True :
break
machine_info [ " expiration " ] = response . expiration
save_machine_info ( machine_info , overwrite = True )
typer . echo ( machine_info [ " expiration " ] )
def server_info_path ( ) - > Path :
home = os . getenv ( " HOME " )
assert home is not None , " Unable to detect $HOME environment variable? "
sporestack_dir = Path ( home , " .sporestack " )
# Put servers in a subdirectory
servers_dir = sporestack_dir. joinpath ( " servers " )
servers_dir = SPORESTACK_DIR / " servers "
# Migrate existing server.json files into servers subdirectory
if sporestack_dir . exists ( ) and not servers_dir . exists ( ) :
if SPORESTACK_DIR . exists ( ) and not servers_dir . exists ( ) :
typer . echo (
f " Migrating server profiles found in { sporestack_dir } to { servers_dir } . " ,
f " Migrating server profiles found in { SPORESTACK_DIR } to { servers_dir } . " ,
err = True ,
)
servers_dir . mkdir ( )
for json_file in sporestack_dir . glob ( " *.json " ) :
json_file . rename ( servers_dir . joinpath ( json_file . name ) )
for json_file in SPORESTACK_DIR . glob ( " *.json " ) :
json_file . rename ( servers_dir / json_file . name )
# Make it, if it doesn't exist already.
sporestack_dir . mkdir ( exist_ok = True )
SPORESTACK_DIR . mkdir ( exist_ok = True )
servers_dir . mkdir ( exist_ok = True )
return servers_dir
def token_path ( ) - > Path :
token_dir = SPORESTACK_DIR / " tokens "
# Make it, if it doesn't exist already.
token_dir . mkdir ( exist_ok = True , parents = True )
return token_dir
def save_machine_info ( machine_info : Dict [ str , Any ] , overwrite : bool = False ) - > None :
"""
Save info to disk .
@ -304,7 +254,7 @@ def save_machine_info(machine_info: Dict[str, Any], overwrite: bool = False) ->
os . umask ( 0o0077 )
directory = server_info_path ( )
hostname = machine_info [ " vm_hostname " ]
json_file = directory . joinpath ( f " { hostname } .json " )
json_file = directory / f " { hostname } .json "
if overwrite is False :
assert json_file . exists ( ) is False , f " { json_file } already exists. "
json_file . write_text ( json . dumps ( machine_info ) )
@ -315,7 +265,7 @@ def get_machine_info(hostname: str) -> Dict[str, Any]:
Get info from disk .
"""
directory = server_info_path ( )
json_file = directory . joinpath ( f " { hostname } .json " )
json_file = directory / f " { hostname } .json "
if not json_file . exists ( ) :
raise ValueError ( f " { hostname } does not exist in { directory } as { json_file } " )
machine_info = json . loads ( json_file . read_bytes ( ) )
@ -343,8 +293,8 @@ def pretty_machine_info(info: Dict[str, Any]) -> str:
return msg
@ cli.command( )
def list( ) - > None :
@ server_ cli.command( name = " list " )
def server_ list( ) - > None :
"""
List all locally known servers .
"""
@ -386,7 +336,7 @@ def machine_exists(hostname: str) -> bool:
return server_info_path ( ) . joinpath ( f " { hostname } .json " ) . exists ( )
@ cli.command( )
@ server_ cli.command( )
def get_attribute ( hostname : str , attribute : str ) - > None :
"""
Returns an attribute about the VM .
@ -395,7 +345,7 @@ def get_attribute(hostname: str, attribute: str) -> None:
typer . echo ( machine_info [ attribute ] )
@ cli.command( )
@ server_ cli.command( )
def info ( hostname : str ) - > None :
"""
Info on the VM
@ -407,7 +357,7 @@ def info(hostname: str) -> None:
)
@ cli.command( )
@ server_ cli.command( )
def start ( hostname : str ) - > None :
"""
Boots the VM .
@ -418,7 +368,7 @@ def start(hostname: str) -> None:
typer . echo ( f " { hostname } started. " )
@ cli.command( )
@ server_ cli.command( )
def stop ( hostname : str ) - > None :
"""
Immediately kills the VM .
@ -429,7 +379,7 @@ def stop(hostname: str) -> None:
typer . echo ( f " { hostname } stopped. " )
@ cli.command( )
@ server_ cli.command( )
def delete ( hostname : str ) - > None :
"""
Deletes the VM before expiration ( no refunds / credits )
@ -442,7 +392,7 @@ def delete(hostname: str) -> None:
typer . echo ( f " { hostname } was deleted. " )
@ cli.command( )
@ server_ cli.command( )
def rebuild ( hostname : str ) - > None :
"""
Rebuilds the VM with the operating system and SSH key given at launch time .
@ -455,9 +405,38 @@ def rebuild(hostname: str) -> None:
typer . echo ( f " { hostname } rebuilding. " )
@cli.command ( )
def settlement_token_enable (
token : str ,
def load_token ( token : str ) - > str :
token_file = token_path ( ) . joinpath ( f " { token } .json " )
if not token_file . exists ( ) :
msg = f " Token ' { token } ' ( { token_file } ) does not exist. Create it with: \n "
msg + = f " sporestack token create { token } --dollars 20 --currency xmr \n "
msg + = " (Can do more than $20, or a different currency, like btc.) \n "
msg + = (
" With the token credited, you can launch servers, renew existing ones, etc. "
)
typer . echo ( msg , err = True )
raise typer . Exit ( code = 1 )
token_data = json . loads ( token_file . read_text ( ) )
assert token_data [ " version " ] == 1
assert isinstance ( token_data [ " key " ] , str )
return token_data [ " key " ]
def save_token ( token : str , key : str ) - > None :
token_file = token_path ( ) . joinpath ( f " { token } .json " )
if token_file . exists ( ) :
msg = " Token ' {token} ' already exists in {token_file} . Aborting! "
typer . echo ( msg , err = True )
raise typer . Exit ( code = 1 )
token_data = { " version " : TOKEN_VERSION , " name " : token , " key " : key }
token_file . write_text ( json . dumps ( token_data ) )
@token_cli.command ( name = " create " )
def token_create (
token : str = typer . Argument ( DEFAULT_TOKEN ) ,
dollars : int = typer . Option ( . . . ) ,
currency : str = typer . Option ( . . . ) ,
) - > None :
@ -466,9 +445,18 @@ def settlement_token_enable(
Dollars is starting balance .
"""
from . import utils
_token = utils . random_token ( )
typer . echo ( f " Generated key { _token } for use with token { token } " , err = True )
if Path ( SPORESTACK_DIR / " tokens " / f " { token } .json " ) . exists ( ) :
typer . echo ( " Token already created! Did you mean to `topup`? " , err = True )
raise typer . Exit ( 1 )
response = api_client . token_enable (
token = token ,
token = _ token,
dollars = dollars ,
currency = currency ,
api_endpoint = get_api_endpoint ( ) ,
@ -489,29 +477,42 @@ def settlement_token_enable(
# Waiting for payment to set in.
time . sleep ( 10 )
response = api_client . token_enable (
token = token,
token = _ token,
dollars = dollars ,
currency = currency ,
api_endpoint = get_api_endpoint ( ) ,
retry = True ,
)
if response . payment . paid is True :
typer . echo (
f " { token } has been enabled with $ { dollars } . Save it and don ' t lose it! "
)
typer . echo ( f " { token } has been enabled with $ { dollars } . " )
typer . echo ( f " { token } ' s key is { _token } . " )
typer . echo ( " Save it, don ' t share it, and don ' t lose it! " )
save_token ( token , _token )
return
raise ValueError ( f " { token } did not get enabled in time. " )
@cli.command ( )
def settlement_token_add (
token : str ,
@token_cli.command ( name = " import " )
def token_import (
name : str = typer . Argument ( DEFAULT_TOKEN ) ,
key : str = typer . Option ( . . . ) ,
) - > None :
"""
Imports a token from a key .
"""
save_token ( name , key )
@token_cli.command ( name = " topup " )
def token_topup (
token : str = typer . Argument ( DEFAULT_TOKEN ) ,
dollars : int = typer . Option ( . . . ) ,
currency : str = typer . Option ( . . . ) ,
) - > None :
"""
Adds balance to an existing settlement token .
"""
token = load_token ( token )
response = api_client . token_add (
token ,
@ -547,25 +548,30 @@ def settlement_token_add(
raise ValueError ( f " { token } did not get enabled in time. " )
@ cli.command( )
def settlement_token_ balance( token : str ) - > None :
@ token_ cli.command( )
def balance( token : str = typer . Argument ( DEFAULT_TOKEN ) ) - > None :
"""
Gets balance for a settlement token .
"""
_token = load_token ( token )
typer . echo (
api_client . token_balance ( token = token, api_endpoint = get_api_endpoint ( ) ) . usd
api_client . token_balance ( token = _ token, api_endpoint = get_api_endpoint ( ) ) . usd
)
@ cli.command( )
def settlement_token_generate ( ) - > None :
@ token_ cli.command( name = " list " )
def token_list ( ) - > None :
"""
Ge nerates a settlement token that can be enabled .
Ge ts bala nc e fo r a settlement token .
"""
from . import utils
typer . echo ( utils . random_token ( ) )
token_dir = token_path ( )
typer . echo ( f " SporeStack tokens present in { token_dir } : " , err = True )
typer . echo ( " (Name): (Key) " , err = True )
for token_file in token_dir . glob ( " *.json " ) :
token = token_file . stem
key = load_token ( token )
typer . echo ( f " { token } : { key } " )
@cli.command ( )