Browse Source

Seems to be working well, added mypy, removed WalkingLiberty

v2
Teran McKinney 1 month ago
parent
commit
d3a7a9f0db
  1. 1
      .gitignore
  2. 4
      Makefile
  3. 45
      README.md
  4. 16
      mypy.ini
  5. 1
      setup.py
  6. 243
      sporestack/client.py
  7. 3
      sporestack/validate.py

1
.gitignore

@ -6,3 +6,4 @@ dist
*.egg-info
.eggs
__pycache__
.pytest_cache

4
Makefile

@ -4,4 +4,8 @@ format:
test:
black --check .
flake8 .
mypy .
pytest
install:
python3 -m pip install -r requirements.txt

45
README.md

@ -2,7 +2,23 @@
## Installation
* `pip3 install sporestack || pip install sporestack`
* `python3 -m pip install sporestack`
* Optional: Create a virtual environment, first.
## Running without installing
* Make sure `pipx` is installed.
* `pipx run sporestack`
## Upgrade notes for going from v1.4 to v2
* `sporestackv2` was renamed to `sporestack`.
* CLI options with `_` have been changed to `-`. So `sporestackv2 settlement_token_balance` is not `sporestack settlement-token-balance. `--settlement_token` is now `--settlement-token`.
* `sporestack launch` cores/memory/disk removed entirely in favor of flavor.
* Package is now following [semver](https://semver.org).
* Torified instance support has been removed.
* `sporestack launch`'s `--ssh_key_file` has been replaced with `--ssh-key`.
* WalkingLiberty support was removed. Best to use settlement tokens instead.
## Screenshot
@ -10,33 +26,24 @@
## Usage
* `sporestack launch SomeHostname --flavor vps-1vcpu-1gb --days 7 --ssh_key_file ~/.ssh/id_rsa.pub --operating_system debian-9 --currency btc`
* `sporestack topup SomeHostname --days 3 --currency bsv`
* `sporestack launch SomeOtherHostname --flavor vps-1vcpu-2gb --days 7 --ssh_key_file ~/.ssh/id_rsa.pub --operating_system debian-10 --currency btc`
* `sporestack launch SomeHostname --flavor vps-1vcpu-1gb --days 7 --ssh-key ~/.ssh/id_rsa.pub --operating-system debian-9 --currency btc`
* `sporestack topup SomeHostname --days 3 --currency xmr`
* `sporestack launch SomeOtherHostname --flavor vps-1vcpu-2gb --days 7 --ssh-key ~/.ssh/id_rsa.pub --operating-system debian-10 --currency btc`
* `sporestack stop SomeHostname`
* `sporestack start SomeHostname`
* `sporestack list`
* `sporestack remove SomeHostname # If expired`
* `sporestack settlement_token_generate`
* `sporestack settlement_token_enable (token) --dollars 10 --currency xmr`
* `sporestack settlement_token_add (token) --dollars 25 --currency btc`
* `sporestack settlement_token_balance (token)`
* `sporestack settlement-token-generate`
* `sporestack settlement-token-enable (token) --dollars 10 --currency xmr`
* `sporestack settlement-token-add (token) --dollars 25 --currency btc`
* `sporestack settlement-token-balance (token)`
More examples on the [website](https://sporestack.com).
## Notes
* 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.)
## 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
```
* You can use `--settlement-token` if you don't want to pay with QR codes all the time.
* If using a .onion API endpoint, will try to use a local Tor proxy if connecting to a .onion URL. (127.0.0.1:9050)
## Licence

16
mypy.ini

@ -0,0 +1,16 @@
[mypy]
[mypy-pytest.*]
ignore_missing_imports = True
[mypy-sshpubkeys.*]
ignore_missing_imports = True
[mypy-aaargh.*]
ignore_missing_imports = True
[mypy-pyqrcode.*]
ignore_missing_imports = True
[mypy-setuptools.*]
ignore_missing_imports = True

1
setup.py

@ -45,7 +45,6 @@ setup(
"pyqrcode",
"requests[socks]>=2.22.0",
"aaargh",
"walkingliberty",
"sshpubkeys",
],
entry_points={"console_scripts": ["sporestack = sporestack.client:main"]},

243
sporestack/client.py

@ -12,7 +12,6 @@ import time
import aaargh
import pyqrcode
from walkingliberty import WalkingLiberty
from . import api_client
from .flavors import all_sporestack_flavors
@ -37,42 +36,35 @@ def random_machine_id():
return secrets.token_hex(32)
def make_payment(currency, uri, usd=None, walkingliberty_wallet=None):
if walkingliberty_wallet is not None:
walkingliberty = WalkingLiberty(currency)
txid = walkingliberty.pay(private_key=walkingliberty_wallet, uri=uri)
logging.debug("WalkingLiberty txid: {}".format(txid))
else:
premessage = """Payment URI: {}
def make_payment(currency: str, uri: str, usd: str) -> None:
premessage = """Payment URI: {}
Pay *exactly* the specified amount. No more, no less. Pay within
one hour at the very most.
Resize your terminal and try again if QR code above is not readable.
Press ctrl+c to abort."""
message = premessage.format(uri)
qr = pyqrcode.create(uri)
print(qr.terminal(module_color="black", background="white", quiet_zone=1))
print(message)
if usd is not None:
print("Approximate price in USD: {}".format(usd))
input("[Press enter once you have made payment.]")
message = premessage.format(uri)
qr = pyqrcode.create(uri)
print(qr.terminal(module_color="black", background="white", quiet_zone=1))
print(message)
print(f"Approximate price in USD: {usd}")
input("[Press enter once you have made payment.]")
# FIXME: ordering...
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("--region", type=str, default=None)
@cli.cmd_arg("hostname")
@cli.cmd_arg("--ssh-key", type=str, required=True)
@cli.cmd_arg("--days", type=int, required=True)
@cli.cmd_arg("--operating-system", type=str, required=True)
@cli.cmd_arg("--currency", type=str, default=None)
@cli.cmd_arg("--settlement-token", type=str, default=None)
@cli.cmd_arg("--flavor", type=str, default=DEFAULT_FLAVOR)
@cli.cmd_arg("--days", type=int, required=True)
@cli.cmd_arg("--walkingliberty-wallet", type=str, default=None)
@cli.cmd_arg("--region", type=str, default=None)
@cli.cmd_arg("--api-endpoint", type=str, default=API_ENDPOINT)
@cli.cmd_arg("--operating-system", type=str, default=None)
@cli.cmd_arg("--ssh-key", type=str, default=None)
@cli.cmd_arg("--affiliate-amount", type=int, default=None)
@cli.cmd_arg("--affiliate-token", type=str, default=None)
def launch(
vm_hostname,
hostname,
days,
settlement_token,
operating_system,
@ -81,29 +73,28 @@ def launch(
currency=None,
region=None,
ssh_key=None,
ssh_key_file=None,
walkingliberty_wallet=None,
affiliate_amount=None,
affiliate_token=None,
):
"""
Attempts to launch a server.
"""
if settlement_token is not None:
if currency is None:
if currency is None or currency == "settlement":
currency = "settlement"
else:
msg = "Cannot use non-settlement --currency with --settlement-token"
raise ValueError(msg)
else:
if currency is None:
raise ValueError("--currency must be set.")
if machine_exists(vm_hostname):
message = "{} already created.".format(vm_hostname)
if machine_exists(hostname):
message = "{} already created.".format(hostname)
raise ValueError(message)
if ssh_key is not None and ssh_key_file is not None:
raise ValueError("Only ssh_key or ssh_key_file can be set.")
if ssh_key_file is not None:
with open(ssh_key_file) as fp:
ssh_key = fp.read()
with open(ssh_key) as fp:
ssh_key = fp.read()
machine_id = random_machine_id()
@ -133,13 +124,13 @@ def launch(
if "usd" in created_dict["payment"]:
usd = created_dict["payment"]["usd"]
else:
usd = None
# Whoops?
usd = ""
make_payment(
currency=currency,
uri=uri,
usd=usd,
walkingliberty_wallet=walkingliberty_wallet,
)
tries = 360
@ -171,29 +162,27 @@ def launch(
# FIXME: Bad exception type.
raise ValueError("Server creation failed, tries exceeded.")
created_dict["vm_hostname"] = vm_hostname
created_dict["vm_hostname"] = hostname
created_dict["machine_id"] = machine_id
created_dict["api_endpoint"] = api_endpoint
save_machine_info(created_dict)
print(pretty_machine_info(created_dict), file=sys.stderr)
return created_dict
return json.dumps(created_dict, indent=4)
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("hostname")
@cli.cmd_arg("--currency", type=str, default=None)
@cli.cmd_arg("--settlement_token", type=str, default=None)
@cli.cmd_arg("--settlement-token", type=str, default=None)
@cli.cmd_arg("--days", type=int, required=True)
@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)
@cli.cmd_arg("--affiliate_token", type=str, default=None)
@cli.cmd_arg("--affiliate-amount", type=int, default=None)
@cli.cmd_arg("--affiliate-token", type=str, default=None)
def topup(
vm_hostname,
hostname,
days,
currency,
settlement_token=None,
walkingliberty_wallet=None,
api_endpoint=None,
affiliate_amount=None,
affiliate_token=None,
@ -202,13 +191,19 @@ def topup(
tops up an existing vm.
"""
if settlement_token is not None:
if currency is None:
if currency is None or currency == "settlement":
currency = "settlement"
else:
msg = "Cannot use non-settlement --currency with --settlement-token"
raise ValueError(msg)
else:
if currency is None:
raise ValueError("--currency must be set.")
if not machine_exists(vm_hostname):
message = "{} does not exist.".format(vm_hostname)
if not machine_exists(hostname):
message = f"{hostname} does not exist."
raise ValueError(message)
machine_info = get_machine_info(vm_hostname)
machine_info = get_machine_info(hostname)
machine_id = machine_info["machine_id"]
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
@ -240,7 +235,6 @@ def topup(
currency=currency,
uri=uri,
usd=usd,
walkingliberty_wallet=walkingliberty_wallet,
)
tries = 360
@ -276,25 +270,25 @@ def save_machine_info(machine_info, overwrite=False):
directory = machine_info_directory()
if not os.path.exists(directory):
os.mkdir(directory)
vm_hostname = machine_info["vm_hostname"]
json_path = os.path.join(directory, f"{vm_hostname}.json")
hostname = machine_info["vm_hostname"]
json_path = os.path.join(directory, f"{hostname}.json")
with open(json_path, mode) as json_file:
json.dump(machine_info, json_file)
return True
def get_machine_info(vm_hostname):
def get_machine_info(hostname):
"""
Get info from disk.
"""
directory = machine_info_directory()
if not machine_exists(vm_hostname):
raise ValueError(f"{vm_hostname} does not exist in {directory}")
json_path = os.path.join(directory, f"{vm_hostname}.json")
if not machine_exists(hostname):
raise ValueError(f"{hostname} does not exist in {directory}")
json_path = os.path.join(directory, f"{hostname}.json")
with open(json_path) as json_file:
machine_info = json.load(json_file)
if machine_info["vm_hostname"] != vm_hostname:
raise ValueError("vm_hostname does not match filename.")
if machine_info["vm_hostname"] != hostname:
raise ValueError("hostname does not match filename.")
return machine_info
@ -332,9 +326,9 @@ def list():
"""
directory = machine_info_directory()
infos = []
for vm_hostname_json in os.listdir(directory):
vm_hostname = vm_hostname_json.split(".")[0]
saved_vm_info = get_machine_info(vm_hostname)
for hostname_json in os.listdir(directory):
hostname = hostname_json.split(".")[0]
saved_vm_info = get_machine_info(hostname)
try:
upstream_vm_info = api_client.info(
machine_id=saved_vm_info["machine_id"],
@ -348,7 +342,7 @@ def list():
human_expiration = time.strftime(
"%Y-%m-%d %H:%M:%S %z", time.localtime(expiration)
)
msg = vm_hostname
msg = hostname
msg += " expired ({} {}): ".format(expiration, human_expiration)
msg += str(e)
print(msg)
@ -361,68 +355,70 @@ def list():
return None
def remove(vm_hostname):
def remove(hostname):
"""
Removes a server's .json file.
"""
os.remove(machine_info_directory() + "/" + vm_hostname + ".json")
os.remove(machine_info_directory() + "/" + hostname + ".json")
@cli.cmd(name="remove")
@cli.cmd_arg("vm_hostname")
def remove_cli(vm_hostname):
info = get_machine_info(vm_hostname)
@cli.cmd_arg("hostname")
def remove_cli(hostname):
info = get_machine_info(hostname)
print(info)
print(pretty_machine_info(info))
remove(vm_hostname)
print("{} removed.".format(vm_hostname))
remove(hostname)
print("{} removed.".format(hostname))
def machine_exists(vm_hostname):
def machine_exists(hostname):
"""
Check if the VM's JSON exists locally.
"""
directory = machine_info_directory()
file_path = os.path.join(directory, "{}.json".format(vm_hostname))
file_path = os.path.join(directory, "{}.json".format(hostname))
if os.path.exists(file_path):
return True
else:
return False
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd(name="get-attribute")
@cli.cmd_arg("hostname")
@cli.cmd_arg("attribute")
def get_attribute(vm_hostname, attribute):
def get_attribute(hostname, attribute):
"""
Returns an attribute about the VM.
"""
machine_info = get_machine_info(vm_hostname)
machine_info = get_machine_info(hostname)
return machine_info[attribute]
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("hostname")
@cli.cmd_arg("--api-endpoint", type=str, default=None)
def info(vm_hostname, api_endpoint=None):
def info(hostname, api_endpoint=None):
"""
Info on the VM
"""
machine_info = get_machine_info(vm_hostname)
machine_info = get_machine_info(hostname)
machine_id = machine_info["machine_id"]
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
return api_client.info(machine_id=machine_id, api_endpoint=api_endpoint)
return json.dumps(
api_client.info(machine_id=machine_id, api_endpoint=api_endpoint), indent=4
)
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("hostname")
@cli.cmd_arg("--api-endpoint", type=str, default=None)
def start(vm_hostname, api_endpoint=None):
def start(hostname, api_endpoint=None):
"""
Boots the VM.
"""
machine_info = get_machine_info(vm_hostname)
machine_info = get_machine_info(hostname)
machine_id = machine_info["machine_id"]
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
@ -430,13 +426,13 @@ def start(vm_hostname, api_endpoint=None):
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("hostname")
@cli.cmd_arg("--api-endpoint", type=str, default=None)
def stop(vm_hostname, api_endpoint=None):
def stop(hostname, api_endpoint=None):
"""
Immediately kills the VM.
"""
machine_info = get_machine_info(vm_hostname)
machine_info = get_machine_info(hostname)
machine_id = machine_info["machine_id"]
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
@ -444,34 +440,32 @@ def stop(vm_hostname, api_endpoint=None):
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("hostname")
@cli.cmd_arg("--api-endpoint", type=str, default=None)
def delete(vm_hostname, api_endpoint=None):
def delete(hostname, api_endpoint=None):
"""
Deletes the VM (most likely prematurely.
"""
machine_info = get_machine_info(vm_hostname)
machine_info = get_machine_info(hostname)
machine_id = machine_info["machine_id"]
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
api_client.delete(machine_id=machine_id, api_endpoint=api_endpoint)
# Also remove the .json file
remove(vm_hostname)
remove(hostname)
@cli.cmd
@cli.cmd_arg("settlement_token")
@cli.cmd(name="settlement-token-enable")
@cli.cmd_arg("token")
@cli.cmd_arg("--dollars", 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("--currency", type=str, required=True)
@cli.cmd_arg("--api-endpoint", type=str, default=API_ENDPOINT)
def settlement_token_enable(
settlement_token,
dollars=None,
currency=None,
walkingliberty_wallet=None,
api_endpoint=None,
):
token: str,
dollars: int,
currency: str,
api_endpoint: str = API_ENDPOINT,
) -> None:
"""
Enables a new settlement token.
@ -482,7 +476,7 @@ def settlement_token_enable(
def enable_token():
return api_client.settlement_token_enable(
settlement_token=settlement_token,
settlement_token=token,
cents=cents,
currency=currency,
api_endpoint=api_endpoint,
@ -493,9 +487,7 @@ def settlement_token_enable(
uri = enable_dict["payment_uri"]
usd = enable_dict["usd"]
make_payment(
currency=currency, uri=uri, usd=usd, walkingliberty_wallet=walkingliberty_wallet
)
make_payment(currency=currency, uri=uri, usd=usd)
tries = 360
while tries > 0:
@ -507,23 +499,20 @@ def settlement_token_enable(
enable_dict = enable_token()
if enable_dict["paid"] is True:
break
logging.info(f"{token} has been enabled. Save it and don't lose it!")
return True
@cli.cmd
@cli.cmd_arg("settlement_token")
@cli.cmd_arg("--dollars", 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(name="settlement-token-add")
@cli.cmd_arg("token")
@cli.cmd_arg("--dollars", type=int)
@cli.cmd_arg("--currency", type=str, required=True)
@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,
token: str,
dollars: int,
cents: int,
currency: str,
api_endpoint: str = API_ENDPOINT,
):
"""
Adds balance to an existing settlement token.
@ -533,7 +522,7 @@ def settlement_token_add(
def add_to_token():
return api_client.settlement_token_add(
settlement_token,
token,
cents,
currency=currency,
api_endpoint=api_endpoint,
@ -544,9 +533,7 @@ def settlement_token_add(
uri = add_dict["payment_uri"]
usd = add_dict["usd"]
make_payment(
currency=currency, uri=uri, usd=usd, walkingliberty_wallet=walkingliberty_wallet
)
make_payment(currency=currency, uri=uri, usd=usd)
tries = 360
while tries > 0:
@ -562,21 +549,21 @@ def settlement_token_add(
return True
@cli.cmd
@cli.cmd_arg("settlement_token")
@cli.cmd(name="settlement-token-balance")
@cli.cmd_arg("token")
@cli.cmd_arg("--api-endpoint", type=str, default=API_ENDPOINT)
def settlement_token_balance(settlement_token, api_endpoint=None):
def settlement_token_balance(token: str, api_endpoint: str = API_ENDPOINT):
"""
Gets balance for a settlement token.
"""
return api_client.settlement_token_balance(
settlement_token=settlement_token, api_endpoint=api_endpoint
settlement_token=token, api_endpoint=api_endpoint
)
@cli.cmd
def settlement_token_generate():
@cli.cmd(name="settlement-token-generate")
def settlement_token_generate() -> str:
"""
Generates a settlement token that can be enabled.
"""
@ -584,14 +571,14 @@ def settlement_token_generate():
@cli.cmd
def version():
def version() -> str:
"""
Returns the installed version.
"""
return __version__
def main():
def main() -> None:
output = cli.run()
if output is True:
exit(0)

3
sporestack/validate.py

@ -23,7 +23,6 @@ def machine_id(machine_id: str) -> None:
for letter in machine_id:
if letter not in "0123456789abcdef":
raise ValueError("machine_id must be only 0-9, a-f (lowercase)")
return True
def settlement_token(settlement_token: str) -> None:
@ -34,7 +33,7 @@ def settlement_token(settlement_token: str) -> None:
try:
machine_id(settlement_token)
except ValueError:
raise ValueError("settlement_token must be only 0-9, a-f (lowercase)")
raise ValueError("settlement_token must be 64 characters, 0-9, a-f (lowercase)")
def operating_system(operating_system: str) -> None:

Loading…
Cancel
Save