Browse Source

Add settlement_token CLI features, lots of cleanups, minor tweaks

master
Teran McKinney 1 year ago
parent
commit
6223bcc4e5
  1. 21
      README.md
  2. 130
      sporestackv2/api_client.py
  3. 178
      sporestackv2/client.py
  4. 2
      sporestackv2/utilities.py
  5. 22
      sporestackv2/validate.py
  6. 32
      sporestackv2/validate_test.py
  7. 2
      sporestackv2/version.py

21
README.md

@ -16,18 +16,31 @@
* `sporestackv2 start SomeHostname`
* `sporestackv2 list`
* `sporestackv2 remove SomeHostname # If expired`
* `sporestackv2 settlement_token_generate`
* `sporestackv2 settlement_token_enable (token) --dollars 10 --currency xmr`
* `sporestackv2 settlement_token_add (token) --dollars 25 --currency btc`
* `sporestackv2 settlement_token_balance (token)`
More examples on the [website](https://sporestack.com).
# Notes
## Notes
* You can use --walkingliberty_wallet if you don't want to pay by QR codes all the time.
* You can use --walkingliberty_wallet if you don't want to pay by QR codes all the time, or you can use --settlement_token. --settlement_token is probably better for most.
* As of 1.0.7, will try to use a local Tor proxy if connecting to a .onion URL. (127.0.0.1:9050) (However, this does not apply to `serialconsole` for the time being.)
# Deprecation notice
## Tips
If using Hidden Hosting, configure ~/.ssh/config like this (fixes serialconsole and you can ssh without torsocks):
```
Host *.onion
ProxyCommand nc -x localhost:9050 %h %p
```
## Deprecation notice
Use `sporestackv2` instead of `sporestack`.
# Licence
## Licence
[Unlicense/Public domain](LICENSE.txt)

130
sporestackv2/api_client.py

@ -5,13 +5,10 @@ import sys
import os
from time import sleep
import aaargh
import requests
from . import validate
cli = aaargh.App()
LATEST_API_VERSION = 2
GET_TIMEOUT = 60
@ -152,33 +149,6 @@ def get_url(api_endpoint, host, target):
return '{}/v{}/{}'.format(api_endpoint, LATEST_API_VERSION, target)
# FIXME: ordering
@cli.cmd
@cli.cmd_arg('machine_id')
@cli.cmd_arg('--days', type=int)
@cli.cmd_arg('--disk', type=int)
@cli.cmd_arg('--memory', type=int)
@cli.cmd_arg('--ipv4')
@cli.cmd_arg('--ipv6')
@cli.cmd_arg('--bandwidth', type=int)
@cli.cmd_arg('--hostaccess', type=bool, default=False)
@cli.cmd_arg('--override_code', type=str, default=None)
@cli.cmd_arg('--organization', type=str, default=None)
@cli.cmd_arg('--managed', type=bool, default=False)
@cli.cmd_arg('--currency', type=str)
@cli.cmd_arg('--cores', type=int, default=1)
@cli.cmd_arg('--region', type=str, default=None)
@cli.cmd_arg('--settlement_token', type=str, default=None)
@cli.cmd_arg('--refund_address', type=str, default=None)
@cli.cmd_arg('--qemuopts', type=str, default=None)
@cli.cmd_arg('--api_endpoint', type=str, default=None)
@cli.cmd_arg('--host', type=str, default=None)
@cli.cmd_arg('--ipxescript', type=str, default=None)
@cli.cmd_arg('--operating_system', type=str, default=None)
@cli.cmd_arg('--ssh_key', type=str, default=None)
@cli.cmd_arg('--want_topup', type=bool, default=False)
@cli.cmd_arg('--affiliate_amount', type=int, default=None)
@cli.cmd_arg('--affiliate_token', type=str, default=None)
def launch(machine_id,
days,
disk,
@ -192,7 +162,6 @@ def launch(machine_id,
operating_system=None,
ssh_key=None,
organization=None,
refund_address=None,
cores=1,
managed=False,
override_code=None,
@ -230,7 +199,6 @@ def launch(machine_id,
'days': days,
'disk': disk,
'memory': memory,
'refund_address': refund_address,
'cores': cores,
'managed': managed,
'currency': currency,
@ -255,22 +223,10 @@ def launch(machine_id,
return api_request(url=url, json_params=json_params, retry=retry)
@cli.cmd
@cli.cmd_arg('machine_id')
@cli.cmd_arg('--settlement_token', type=str, default=None)
@cli.cmd_arg('--override_code', type=str, default=None)
@cli.cmd_arg('--currency', type=str, default=None)
@cli.cmd_arg('--days', type=int)
@cli.cmd_arg('--refund_address', type=str, default=None)
@cli.cmd_arg('--api_endpoint', type=str, default=None)
@cli.cmd_arg('--host', type=str, default=None)
@cli.cmd_arg('--affiliate_amount', type=int, default=None)
@cli.cmd_arg('--affiliate_token', type=str, default=None)
def topup(machine_id,
days,
currency,
settlement_token=None,
refund_address=None,
override_code=None,
api_endpoint=None,
host=None,
@ -283,7 +239,6 @@ def topup(machine_id,
json_params = {'machine_id': machine_id,
'days': days,
'refund_address': refund_address,
'settlement_token': settlement_token,
'currency': currency,
'host': host,
@ -294,10 +249,6 @@ def topup(machine_id,
return api_request(url=url, json_params=json_params, retry=retry)
@cli.cmd
@cli.cmd_arg('host')
@cli.cmd_arg('machine_id')
@cli.cmd_arg('--api_endpoint', type=str, default=None)
def start(host, machine_id, api_endpoint=None):
"""
Boots the VM.
@ -310,10 +261,6 @@ def start(host, machine_id, api_endpoint=None):
return True
@cli.cmd
@cli.cmd_arg('host')
@cli.cmd_arg('machine_id')
@cli.cmd_arg('--api_endpoint', type=str, default=None)
def stop(host, machine_id, api_endpoint=None):
"""
Immediately kills the VM.
@ -326,10 +273,6 @@ def stop(host, machine_id, api_endpoint=None):
return True
@cli.cmd
@cli.cmd_arg('host')
@cli.cmd_arg('machine_id')
@cli.cmd_arg('--api_endpoint', type=str, default=None)
def delete(host, machine_id, api_endpoint=None):
"""
Immediately deletes the VM.
@ -342,10 +285,6 @@ def delete(host, machine_id, api_endpoint=None):
return True
@cli.cmd
@cli.cmd_arg('host')
@cli.cmd_arg('machine_id')
@cli.cmd_arg('--api_endpoint', type=str, default=None)
def sshhostname(host, machine_id, api_endpoint=None):
"""
Returns a hostname that we can SSH into to reach
@ -358,10 +297,6 @@ def sshhostname(host, machine_id, api_endpoint=None):
return api_request(url, get_params=get_params)
@cli.cmd
@cli.cmd_arg('host')
@cli.cmd_arg('machine_id')
@cli.cmd_arg('--api_endpoint', type=str, default=None)
def info(host, machine_id, api_endpoint=None):
"""
Returns info about the VM.
@ -373,10 +308,6 @@ def info(host, machine_id, api_endpoint=None):
return api_request(url, get_params=get_params)
@cli.cmd
@cli.cmd_arg('host')
@cli.cmd_arg('machine_id')
@cli.cmd_arg('--api_endpoint', type=str, default=None)
def ipxescript(host, machine_id, ipxescript=None, api_endpoint=None):
"""
Trying to make this both useful as a CLI tool and
@ -397,11 +328,6 @@ def ipxescript(host, machine_id, ipxescript=None, api_endpoint=None):
return api_request(url, json_params=json_params)
@cli.cmd
@cli.cmd_arg('host')
@cli.cmd_arg('machine_id')
@cli.cmd_arg('bootorder')
@cli.cmd_arg('--api_endpoint', type=str, default=None)
def bootorder(host, machine_id, bootorder, api_endpoint=None):
"""
Updates the boot order for a VM.
@ -416,9 +342,6 @@ def bootorder(host, machine_id, bootorder, api_endpoint=None):
return api_request(url, json_params=json_params)
@cli.cmd
@cli.cmd_arg('host')
@cli.cmd_arg('--api_endpoint', type=str, default=None)
def host_info(host, api_endpoint=None):
"""
Returns info about the host.
@ -427,9 +350,6 @@ def host_info(host, api_endpoint=None):
return api_request(url)
@cli.cmd
@cli.cmd_arg('host')
@cli.cmd_arg('machine_id')
def serialconsole(host, machine_id):
"""
This needs to be adjusted to use a Tor socks proxy of the host is a .onion.
@ -444,15 +364,47 @@ def serialconsole(host, machine_id):
arguments.append('-p')
arguments.append('1060')
arguments.append('serialconsole {}'.format(machine_id))
logging.info(command, arguments)
os.execv(command, arguments)
if __name__ == '__main__':
output = cli.run()
if output is True:
exit(0)
elif output is False:
exit(1)
else:
print(output)
exit(0)
def settlement_token_enable(settlement_token,
cents,
currency,
api_endpoint=None,
retry=False):
validate.settlement_token(settlement_token)
validate.cents(cents)
validate.currency(currency)
json_params = {'settlement_token': settlement_token,
'cents': cents,
'currency': currency}
url = api_endpoint + '/settlement/enable'
return api_request(url=url, json_params=json_params, retry=retry)
def settlement_token_add(settlement_token,
cents,
currency,
api_endpoint=None,
retry=False):
validate.settlement_token(settlement_token)
validate.cents(cents)
validate.currency(currency)
json_params = {'settlement_token': settlement_token,
'cents': cents,
'currency': currency}
url = api_endpoint + '/settlement/add'
return api_request(url=url, json_params=json_params, retry=retry)
def settlement_token_balance(settlement_token,
api_endpoint=None,
retry=False):
validate.settlement_token(settlement_token)
get_params = {'settlement_token': settlement_token}
url = api_endpoint + '/settlement/balance'
return api_request(url=url, get_params=get_params, retry=retry)

178
sporestackv2/client.py

@ -17,6 +17,7 @@ import pyqrcode
from walkingliberty import WalkingLiberty
from . import api_client
from . import validate
from .version import __version__
cli = aaargh.App()
@ -29,6 +30,8 @@ TOR_ENDPOINT = 'http://spore64'\
API_ENDPOINT = CLEARNET_ENDPOINT
WAITING_PAYMENT_TO_PROCESS = 'Waiting for payment to process...'
def i_am_root():
if os.getuid() == 0:
@ -52,16 +55,13 @@ def random_machine_id():
def make_payment(currency,
address,
satoshis,
uri,
usd=None,
walkingliberty_wallet=None):
if walkingliberty_wallet is not None:
walkingliberty = WalkingLiberty(currency)
txid = walkingliberty.send(private_key=walkingliberty_wallet,
address=address,
satoshis=satoshis)
txid = walkingliberty.pay(private_key=walkingliberty_wallet,
uri=uri)
logging.debug('WalkingLiberty txid: {}'.format(txid))
else:
premessage = '''Payment URI: {}
@ -98,7 +98,6 @@ Press ctrl+c to abort.'''
@cli.cmd_arg('--ipv6', default='/128')
@cli.cmd_arg('--disk', type=int, default=5)
@cli.cmd_arg('--days', type=int, required=True)
@cli.cmd_arg('--refund_address', type=str, default=None)
@cli.cmd_arg('--qemuopts', type=str, default=None)
@cli.cmd_arg('--walkingliberty_wallet', type=str, default=None)
@cli.cmd_arg('--api_endpoint', type=str, default=API_ENDPOINT)
@ -121,7 +120,6 @@ def launch(vm_hostname,
bandwidth,
host=None,
api_endpoint=API_ENDPOINT,
refund_address=None,
cores=1,
currency='bch',
region=None,
@ -151,6 +149,10 @@ def launch(vm_hostname,
want_topup = api_client.normalize_argument(want_topup)
ipxescript_stdin = api_client.normalize_argument(ipxescript_stdin)
if settlement_token is not None:
if currency is None:
currency = 'settlement'
if machine_exists(vm_hostname):
message = '{} already created.'.format(vm_hostname)
raise ValueError(message)
@ -195,7 +197,6 @@ def launch(vm_hostname,
ipxescript=ipxescript,
operating_system=operating_system,
ssh_key=ssh_key,
refund_address=refund_address,
cores=cores,
ipv4=ipv4,
ipv6=ipv6,
@ -221,9 +222,6 @@ def launch(vm_hostname,
host = created_dict['host']
# This will be false at least the first time if paying with BTC or BCH.
if created_dict['paid'] is False:
address = created_dict['payment']['address']
satoshis = created_dict['payment']['amount']
uri = created_dict['payment']['uri']
if 'usd' in created_dict['payment']:
@ -232,8 +230,6 @@ def launch(vm_hostname,
usd = None
make_payment(currency=currency,
address=address,
satoshis=satoshis,
uri=uri,
usd=usd,
walkingliberty_wallet=walkingliberty_wallet)
@ -241,7 +237,7 @@ def launch(vm_hostname,
tries = 360
while tries > 0:
tries = tries - 1
logging.info('Waiting for payment to process...')
logging.info(WAITING_PAYMENT_TO_PROCESS)
# FIXME: Wait one hour in a smarter way.
# Waiting for payment to set in.
time.sleep(10)
@ -280,10 +276,9 @@ def launch(vm_hostname,
@cli.cmd
@cli.cmd_arg('vm_hostname')
@cli.cmd_arg('--override_code', type=str, default=None)
@cli.cmd_arg('--currency', type=str, default=None, required=True)
@cli.cmd_arg('--currency', type=str, default=None)
@cli.cmd_arg('--settlement_token', type=str, default=None)
@cli.cmd_arg('--days', type=int, required=True)
@cli.cmd_arg('--refund_address', type=str, default=None)
@cli.cmd_arg('--walkingliberty_wallet', type=str, default=None)
@cli.cmd_arg('--api_endpoint', type=str, default=None)
@cli.cmd_arg('--affiliate_amount', type=int, default=None)
@ -291,7 +286,6 @@ def launch(vm_hostname,
def topup(vm_hostname,
days,
currency,
refund_address=None,
override_code=None,
settlement_token=None,
walkingliberty_wallet=None,
@ -301,6 +295,9 @@ def topup(vm_hostname,
"""
tops up an existing vm.
"""
if settlement_token is not None:
if currency is None:
currency = 'settlement'
if not machine_exists(vm_hostname):
message = '{} does not exist.'.format(vm_hostname)
@ -323,7 +320,6 @@ def topup(vm_hostname,
return api_client.topup(host=host,
machine_id=machine_id,
days=days,
refund_address=refund_address,
currency=currency,
override_code=override_code,
api_endpoint=api_endpoint,
@ -336,9 +332,6 @@ def topup(vm_hostname,
# This will be false at least the first time if paying with anything
# but settlement.
if topped_dict['paid'] is False:
address = topped_dict['payment']['address']
satoshis = topped_dict['payment']['amount']
uri = topped_dict['payment']['uri']
if 'usd' in topped_dict['payment']:
@ -347,15 +340,13 @@ def topup(vm_hostname,
usd = None
make_payment(currency=currency,
address=address,
satoshis=satoshis,
uri=uri,
usd=usd,
walkingliberty_wallet=walkingliberty_wallet)
tries = 360
while tries > 0:
logging.info('Waiting for payment to process...')
logging.info(WAITING_PAYMENT_TO_PROCESS)
tries = tries - 1
# FIXME: Wait one hour in a smarter way.
# Waiting for payment to set in.
@ -425,6 +416,8 @@ def pretty_machine_info(info):
expiration = info["expiration"]
human_expiration = time.strftime('%Y-%m-%d %H:%M:%S %z',
time.localtime(expiration))
if 'running' in info:
msg += 'Running: {}\n'.format(info['running'])
msg += 'Expiration: {} ({})\n'.format(expiration, human_expiration)
time_to_live = expiration - int(time.time())
if time_to_live > 0:
@ -445,7 +438,13 @@ def list():
infos = []
for vm_hostname_json in os.listdir(directory):
vm_hostname = vm_hostname_json.split('.')[0]
infos.append(get_machine_info(vm_hostname))
saved_vm_info = get_machine_info(vm_hostname)
upstream_vm_info = api_client.info(host=saved_vm_info['host'],
machine_id=saved_vm_info['machine_id'],
api_endpoint=saved_vm_info['api_endpoint'])
saved_vm_info['expiration'] = upstream_vm_info['expiration']
saved_vm_info['running'] = upstream_vm_info['running']
infos.append(saved_vm_info)
for info in infos:
print()
@ -644,6 +643,135 @@ def serialconsole(vm_hostname):
return api_client.serialconsole(host, machine_id)
@cli.cmd
@cli.cmd_arg('settlement_token')
@cli.cmd_arg('--dollars', type=int, default=None)
@cli.cmd_arg('--cents', type=int, default=None)
@cli.cmd_arg('--currency', type=str, default=None, required=True)
@cli.cmd_arg('--walkingliberty_wallet', type=str, default=None)
@cli.cmd_arg('--api_endpoint', type=str, default=API_ENDPOINT)
def settlement_token_enable(settlement_token,
dollars=None,
cents=None,
currency=None,
walkingliberty_wallet=None,
api_endpoint=None):
"""
Enables a new settlement token.
Cents is starting balance.
"""
cents = _get_cents(dollars, cents)
def enable_token():
return api_client.settlement_token_enable(settlement_token=settlement_token,
cents=cents,
currency=currency,
api_endpoint=api_endpoint,
retry=True)
enable_dict = enable_token()
uri = enable_dict['payment_uri']
usd = enable_dict['usd']
make_payment(currency=currency,
uri=uri,
usd=usd,
walkingliberty_wallet=walkingliberty_wallet)
tries = 360
while tries > 0:
logging.info(WAITING_PAYMENT_TO_PROCESS)
tries = tries - 1
# FIXME: Wait one hour in a smarter way.
# Waiting for payment to set in.
time.sleep(10)
enable_dict = enable_token()
if enable_dict['paid'] is True:
break
return True
def _get_cents(dollars, cents):
validate._further_dollars_cents(dollars, cents)
if dollars is not None:
validate.cents(dollars)
cents = dollars * 100
return cents
@cli.cmd
@cli.cmd_arg('settlement_token')
@cli.cmd_arg('--dollars', type=int, default=None)
@cli.cmd_arg('--cents', type=int, default=None)
@cli.cmd_arg('--currency', type=str, default=None, required=True)
@cli.cmd_arg('--walkingliberty_wallet', type=str, default=None)
@cli.cmd_arg('--api_endpoint', type=str, default=API_ENDPOINT)
def settlement_token_add(settlement_token,
dollars=None,
cents=None,
currency=None,
walkingliberty_wallet=None,
api_endpoint=None):
"""
Adds balance to an existing settlement token.
"""
cents = _get_cents(dollars, cents)
def add_to_token():
return api_client.settlement_token_add(settlement_token,
cents,
currency=currency,
api_endpoint=api_endpoint,
retry=True)
add_dict = add_to_token()
uri = add_dict['payment_uri']
usd = add_dict['usd']
make_payment(currency=currency,
uri=uri,
usd=usd,
walkingliberty_wallet=walkingliberty_wallet)
tries = 360
while tries > 0:
logging.info(WAITING_PAYMENT_TO_PROCESS)
tries = tries - 1
# FIXME: Wait one hour in a smarter way.
# Waiting for payment to set in.
time.sleep(10)
add_dict = add_to_token()
if add_dict['paid'] is True:
break
return True
@cli.cmd
@cli.cmd_arg('settlement_token')
@cli.cmd_arg('--api_endpoint', type=str, default=API_ENDPOINT)
def settlement_token_balance(settlement_token,
api_endpoint=None):
"""
Gets balance for a settlement token.
"""
return api_client.settlement_token_balance(settlement_token=settlement_token,
api_endpoint=api_endpoint)
@cli.cmd
def settlement_token_generate():
"""
Generates a settlement token that can be enabled.
"""
return random_machine_id()
@cli.cmd
def version():
"""

2
sporestackv2/utilities.py

@ -38,7 +38,7 @@ def payment_to_uri(address, currency, amount):
xmr_decimal_amount = "{0:.12f}".format(amount * 0.000000000001)
uri = 'monero:{}?tx_amount={}'.format(address, xmr_decimal_amount)
else:
raise ValueError('Currency must be one of: btc, bch, bsv')
raise ValueError('Currency must be one of: btc, bch, bsv, xmr')
return uri

22
sporestackv2/validate.py

@ -25,6 +25,14 @@ def machine_id(machine_id):
return True
def settlement_token(settlement_token):
"""
Validates a settlement token.
Identical format to machine IDs.
"""
return machine_id(settlement_token)
def operating_system(operating_system):
"""
Validates an operating_system argument.
@ -268,6 +276,14 @@ def further_ipv4_ipv6(ipv4, ipv6):
return True
def _further_dollars_cents(dollars, cents):
if dollars is None and cents is None:
raise ValueError("dollars or cents must be set.")
if dollars is not None and cents is not None:
raise ValueError("dollars or cents must be set, not both.")
return True
def ipxescript(ipxescript):
if ipxescript is None:
return True
@ -305,3 +321,9 @@ def affiliate_amount(amount):
if amount != 0:
return True
raise TypeError('affiliate_amount must be null or non-zero unsigned int.')
def cents(cents):
if not unsigned_int(cents):
raise TypeError("cents must be unsigned integer.")
return True

32
sporestackv2/validate_test.py

@ -18,6 +18,16 @@ def test_machine_id():
validate.machine_id(invalid_id)
def test_settlement_token():
assert validate.settlement_token(valid_id) is True
with pytest.raises(TypeError):
validate.settlement_token(1337)
with pytest.raises(ValueError):
validate.settlement_token(valid_id + 'b')
with pytest.raises(ValueError):
validate.settlement_token(invalid_id)
def test_days():
with pytest.raises(ValueError):
validate.days(0)
@ -82,6 +92,19 @@ def test_bandwidth():
validate.bandwidth(1.0)
def test_cents():
assert validate.cents(1) is True
assert validate.cents(0) is True
assert validate.cents(10) is True
assert validate.cents(1000000) is True
with pytest.raises(TypeError):
validate.cents(-1)
with pytest.raises(TypeError):
validate.cents('a')
with pytest.raises(TypeError):
validate.cents(None)
def test_further_ipv4_ipv6():
assert validate.further_ipv4_ipv6('tor', 'tor') is True
assert validate.further_ipv4_ipv6('nat', 'nat') is True
@ -97,6 +120,15 @@ def test_further_ipv4_ipv6():
validate.further_ipv4_ipv6('/32', 'tor')
def test_further_dollars_cents():
assert validate._further_dollars_cents(10, None) is True
assert validate._further_dollars_cents(None, 1000) is True
with pytest.raises(ValueError):
validate._further_dollars_cents(10, 10)
with pytest.raises(ValueError):
validate._further_dollars_cents(None, None)
def test_organization():
assert validate.organization(None) is True
assert validate.organization('Corporation') is True

2
sporestackv2/version.py

@ -1 +1 @@
__version__ = '1.1.4'
__version__ = '1.2.0'
Loading…
Cancel
Save