Browse Source

semver V2: Initial commit (now sporestack, not sporestackv2)

v2
Teran McKinney 1 month ago
parent
commit
4ad1bbd3b2
  1. 2
      .flake8
  2. 1
      .gitignore
  3. 7
      Makefile
  4. 28
      README.md
  5. 15
      setup.py
  6. 0
      sporestack/__init__.py
  7. 200
      sporestack/api_client.py
  8. 0
      sporestack/api_client_test.py
  9. 253
      sporestack/client.py
  10. 9
      sporestack/client_test.py
  11. 49
      sporestack/flavors.py
  12. 0
      sporestack/utilities.py
  13. 0
      sporestack/utilities_test.py
  14. 185
      sporestack/validate.py
  15. 146
      sporestack/validate_test.py
  16. 1
      sporestack/version.py
  17. 14
      sporestackv2/client_test.py
  18. 1
      sporestackv2/version.py

2
.flake8

@ -0,0 +1,2 @@
[flake8]
max-line-length = 88

1
.gitignore

@ -1,4 +1,5 @@
*.pyc
.venv
venv
build
dist

7
Makefile

@ -0,0 +1,7 @@
format:
black .
test:
black --check .
flake8 .
pytest

28
README.md

@ -6,21 +6,21 @@
## Screenshot
![sporestackv2 CLI screenshot](https://sporestack.com/static/sporestackv2-screenshot.png)
![sporestack CLI screenshot](https://sporestack.com/static/sporestackv2-screenshot.png)
## Usage
* `sporestackv2 launch SomeHostname --flavor vps-1vcpu-1gb --days 7 --ssh_key_file ~/.ssh/id_rsa.pub --operating_system debian-9 --currency btc`
* `sporestackv2 topup SomeHostname --days 3 --currency bsv`
* `sporestackv2 launch SomeOtherHostname --flavor vps-1vcpu-2gb --days 7 --ssh_key_file ~/.ssh/id_rsa.pub --operating_system debian-10 --currency btc`
* `sporestackv2 stop SomeHostname`
* `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)`
* `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 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)`
More examples on the [website](https://sporestack.com).
@ -38,10 +38,6 @@ Host *.onion
ProxyCommand nc -x localhost:9050 %h %p
```
## Deprecation notice
Use `sporestackv2` instead of `sporestack`.
## Licence
[Unlicense/Public domain](LICENSE.txt)

15
setup.py

@ -1,11 +1,11 @@
#!/usr/bin/env python
#!/usr/bin/env python3
from setuptools import setup
# Not sure how necessary this is. Would be nice to just
# import .sporestackv2.__version__
# import .sporestack.__version__
VERSION = None
with open("sporestackv2/version.py") as f:
with open("sporestack/version.py") as f:
for line in f:
if line.startswith("__version__"):
VERSION = line.replace('"', "").split("=")[1].strip()
@ -14,7 +14,8 @@ if VERSION is None:
raise ValueError("__version__ not found in __init__.py")
DOWNLOAD_HOST = "https://git.sporestack.com"
DOWNLOAD_URL = f"{DOWNLOAD_HOST}/SporeStack/sporestack-python/archive/{VERSION}.tar.gz"
REPO_URL = f"{DOWNLOAD_HOST}/SporeStack/sporestack-python"
DOWNLOAD_URL = f"{REPO_URL}/archive/{VERSION}.tar.gz"
DESCRIPTION = "SporeStack.com library and client. Launch servers with Bitcoin."
KEYWORDS = [
@ -29,7 +30,7 @@ KEYWORDS = [
]
setup(
python_requires=">=3.6",
python_requires=">=3.7",
name="sporestack",
version=VERSION,
author="SporeStack",
@ -39,7 +40,7 @@ setup(
license="Unlicense",
url="https://sporestack.com/",
download_url=DOWNLOAD_URL,
packages=["sporestackv2"],
packages=["sporestack"],
install_requires=[
"pyqrcode",
"requests[socks]>=2.22.0",
@ -47,5 +48,5 @@ setup(
"walkingliberty",
"sshpubkeys",
],
entry_points={"console_scripts": ["sporestackv2 = sporestackv2.client:main"]},
entry_points={"console_scripts": ["sporestack = sporestack.client:main"]},
)

0
sporestackv2/__init__.py → sporestack/__init__.py

200
sporestackv2/api_client.py → sporestack/api_client.py

@ -1,9 +1,6 @@
#!/usr/bin/python3
import logging
import sys
import os
from time import sleep
from typing import Any, Optional
import requests
@ -11,6 +8,11 @@ from . import validate
LATEST_API_VERSION = 2
CLEARNET_ENDPOINT = "https://api.sporestack.com"
TOR_ENDPOINT = "http://spore64i5sofqlfz5gq2ju4msgzojjwifls7rok2cti624zyq3fcelad.onion"
API_ENDPOINT = CLEARNET_ENDPOINT
GET_TIMEOUT = 60
POST_TIMEOUT = 90
USE_TOR_PROXY = "auto"
@ -29,7 +31,7 @@ def validate_use_tor_proxy(use_tor_proxy):
raise ValueError('use_tor_proxy must be True, False, or "auto"')
def is_onion_url(url):
def is_onion_url(url: str) -> bool:
"""
returns True/False depending on if a URL looks like a Tor hidden service
(.onion) or not.
@ -126,7 +128,7 @@ def api_request(
raise Exception("Stuff broke strangely.")
def normalize_argument(argument):
def normalize_argument(argument: Any) -> Any:
"""
Helps normalize arguments from aaargh that may not be what we want.
"""
@ -140,48 +142,23 @@ def normalize_argument(argument):
return argument
def get_url(api_endpoint, host, target):
"""
Has nothing to do with GET requests.
"""
if api_endpoint is None:
api_endpoint = "http://{}".format(host)
return "{}/v{}/{}".format(api_endpoint, LATEST_API_VERSION, target)
def launch(
machine_id,
days,
currency,
flavor=None,
disk=None,
memory=None,
ipv4=None,
ipv6=None,
region=None,
ipxescript=None,
operating_system=None,
ssh_key=None,
organization=None,
cores=1,
settlement_token=None,
api_endpoint=None,
host=None,
machine_id: str,
days: int,
currency: str,
flavor: str,
operating_system: str,
ssh_key: str,
api_endpoint: str = API_ENDPOINT,
region: Optional[str] = None,
settlement_token: Optional[str] = None,
retry=False,
affiliate_amount=None,
affiliate_token=None,
):
"""
Only ipxescript or operating_system + ssh_key can be None.
flavor overrides cores, memory, etc settings.
"""
) -> Any:
validate.currency(currency)
validate.flavor(flavor)
validate.organization(organization)
validate.machine_id(machine_id)
validate.ipxescript(ipxescript)
validate.operating_system(operating_system)
validate.ssh_key(ssh_key)
validate.affiliate_amount(affiliate_amount)
@ -192,32 +169,14 @@ def launch(
"flavor": flavor,
"currency": currency,
"region": region,
"organization": organization,
"settlement_token": settlement_token,
"ipxescript": ipxescript,
"operating_system": operating_system,
"ssh_key": ssh_key,
"host": host,
"affiliate_amount": affiliate_amount,
"affiliate_token": affiliate_token,
}
# If flavor is None, check these
if flavor is None:
ipv4 = normalize_argument(ipv4)
ipv6 = normalize_argument(ipv6)
validate.ipv4(ipv4)
validate.ipv6(ipv6)
validate.cores(cores)
validate.disk(disk)
validate.memory(memory)
json_params["ipv4"] = ipv4
json_params["ipv6"] = ipv6
json_params["cores"] = cores
json_params["disk"] = disk
json_params["memory"] = memory
url = get_url(api_endpoint=api_endpoint, host=host, target="launch")
url = f"{api_endpoint}/v2/launch"
return api_request(url=url, json_params=json_params, retry=retry)
@ -225,13 +184,12 @@ def topup(
machine_id,
days,
currency,
api_endpoint: str = API_ENDPOINT,
settlement_token=None,
api_endpoint=None,
host=None,
retry=False,
affiliate_amount=None,
affiliate_token=None,
):
) -> Any:
validate.machine_id(machine_id)
validate.currency(currency)
validate.affiliate_amount(affiliate_amount)
@ -241,131 +199,63 @@ def topup(
"days": days,
"settlement_token": settlement_token,
"currency": currency,
"host": host,
"affiliate_amount": affiliate_amount,
"affiliate_token": affiliate_token,
}
url = get_url(api_endpoint=api_endpoint, host=host, target="topup")
url = f"{api_endpoint}/v2/topup"
return api_request(url=url, json_params=json_params, retry=retry)
def start(host, machine_id, api_endpoint=None):
def start(machine_id: str, api_endpoint: str = API_ENDPOINT) -> None:
"""
Boots the VM.
"""
validate.machine_id(machine_id)
url = get_url(api_endpoint=api_endpoint, host=host, target="start")
json_params = {"machine_id": machine_id, "host": host}
url = f"{api_endpoint}/v2/start"
json_params = {"machine_id": machine_id}
api_request(url, json_params=json_params)
return True
def stop(host, machine_id, api_endpoint=None):
def stop(machine_id: str, api_endpoint: str = API_ENDPOINT) -> None:
"""
Immediately kills the VM.
"""
validate.machine_id(machine_id)
url = get_url(api_endpoint=api_endpoint, host=host, target="stop")
json_params = {"machine_id": machine_id, "host": host}
url = f"{api_endpoint}/v2/stop"
json_params = {"machine_id": machine_id}
api_request(url, json_params=json_params)
return True
def delete(host, machine_id, api_endpoint=None):
def delete(machine_id: str, api_endpoint: str = API_ENDPOINT) -> None:
"""
Immediately deletes the VM.
"""
validate.machine_id(machine_id)
url = get_url(api_endpoint=api_endpoint, host=host, target="delete")
json_params = {"machine_id": machine_id, "host": host}
url = f"{api_endpoint}/v2/delete"
json_params = {"machine_id": machine_id}
api_request(url, json_params=json_params)
return True
def sshhostname(host, machine_id, api_endpoint=None):
"""
Returns a hostname that we can SSH into to reach
port 22 on the VM.
"""
validate.machine_id(machine_id)
url = get_url(api_endpoint=api_endpoint, host=host, target="sshhostname")
get_params = {"machine_id": machine_id, "host": host}
return api_request(url, get_params=get_params)
def info(host, machine_id, api_endpoint=None):
def info(machine_id: str, api_endpoint: str = API_ENDPOINT) -> Any:
"""
Returns info about the VM.
"""
validate.machine_id(machine_id)
url = get_url(api_endpoint=api_endpoint, host=host, target="info")
get_params = {"machine_id": machine_id, "host": host}
url = f"{api_endpoint}/v2/info"
get_params = {"machine_id": machine_id}
return api_request(url, get_params=get_params)
def ipxescript(host, machine_id, ipxescript=None, api_endpoint=None):
"""
Trying to make this both useful as a CLI tool and
as a library. Not really sure how to do that best.
"""
validate.machine_id(machine_id)
if ipxescript is None:
if __name__ == "__main__":
ipxescript = sys.stdin.read()
else:
raise ValueError("ipxescript must be set.")
url = get_url(api_endpoint=api_endpoint, host=host, target="ipxescript")
json_params = {"machine_id": machine_id, "host": host, "ipxescript": ipxescript}
return api_request(url, json_params=json_params)
def bootorder(host, machine_id, bootorder, api_endpoint=None):
"""
Updates the boot order for a VM.
"""
validate.machine_id(machine_id)
validate.bootorder(bootorder)
url = get_url(api_endpoint=api_endpoint, host=host, target="ipxescript")
json_params = {"machine_id": machine_id, "host": host, "bootorder": bootorder}
return api_request(url, json_params=json_params)
def host_info(host, api_endpoint=None):
"""
Returns info about the host.
"""
url = get_url(api_endpoint=api_endpoint, host=host, target="host_info")
return api_request(url)
def serialconsole(host, machine_id):
"""
This needs to be adjusted to use a Tor socks proxy of the host is a .onion.
"""
validate.machine_id(machine_id)
command = "/usr/bin/ssh"
arguments = []
arguments.append(command)
arguments.append("-t")
arguments.append("vmmanagement@{}".format(host))
arguments.append("-p")
arguments.append("1060")
arguments.append("serialconsole {}".format(machine_id))
logging.info(command, arguments)
os.execv(command, arguments)
def settlement_token_enable(
settlement_token, cents, currency, api_endpoint=None, retry=False
settlement_token: str,
cents: int,
currency: str,
api_endpoint: str = API_ENDPOINT,
retry=False,
):
validate.settlement_token(settlement_token)
validate.cents(cents)
@ -381,7 +271,11 @@ def settlement_token_enable(
def settlement_token_add(
settlement_token, cents, currency, api_endpoint=None, retry=False
settlement_token: str,
cents: int,
currency: str,
api_endpoint: str = API_ENDPOINT,
retry=False,
):
validate.settlement_token(settlement_token)
validate.cents(cents)
@ -396,9 +290,11 @@ def settlement_token_add(
return api_request(url=url, json_params=json_params, retry=retry)
def settlement_token_balance(settlement_token, api_endpoint=None, retry=False):
def settlement_token_balance(
settlement_token: str, api_endpoint: str = API_ENDPOINT
) -> str:
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)
return api_request(url=url, get_params=get_params)

0
sporestackv2/api_client_test.py → sporestack/api_client_test.py

253
sporestackv2/client.py → sporestack/client.py

@ -1,5 +1,3 @@
#!/usr/bin/python3
"""
Cleaner interface into api_client, for the most part.
"""
@ -11,28 +9,20 @@ import secrets
import os
import logging
import time
from argparse import SUPPRESS
import aaargh
import pyqrcode
from walkingliberty import WalkingLiberty
from . import api_client
from . import validate
from .flavors import all_sporestack_flavors
from .api_client import API_ENDPOINT
from .version import __version__
cli = aaargh.App()
logging.basicConfig(level=logging.INFO)
CLEARNET_ENDPOINT = "https://api.sporestack.com"
TOR_ENDPOINT = (
"http://spore64" "i5sofqlfz5gq2ju4msgzojjwifls7rok2cti624zyq3fcelad.onion"
)
API_ENDPOINT = CLEARNET_ENDPOINT
DEFAULT_FLAVOR = "vps-1vcpu-1gb"
WAITING_PAYMENT_TO_PROCESS = "Waiting for payment to process..."
@ -70,85 +60,36 @@ Press ctrl+c to abort."""
# FIXME: ordering...
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("--host", type=str, default=None)
@cli.cmd_arg("--save", type=bool, default=True)
@cli.cmd_arg("--region", type=str, default=None)
@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("--flavor", type=str, default=DEFAULT_FLAVOR)
@cli.cmd_arg("--cores", type=int, help=SUPPRESS, default=None)
@cli.cmd_arg("--memory", type=int, help=SUPPRESS, default=None)
@cli.cmd_arg("--ipv4", help=SUPPRESS, default=None)
@cli.cmd_arg("--ipv6", help=SUPPRESS, default=None)
@cli.cmd_arg("--disk", type=int, 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=API_ENDPOINT)
@cli.cmd_arg("--want_topup", type=bool, default=False, help=SUPPRESS)
@cli.cmd_arg("--organization", type=str, default=None)
@cli.cmd_arg("--ipxescript", type=str, default=None)
@cli.cmd_arg("--ipxescript_stdin", type=bool, default=False)
@cli.cmd_arg("--ipxescript_file", 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("--ssh_key_file", 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("--walkingliberty-wallet", 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,
days,
flavor=None,
disk=None,
memory=None,
ipv4=None,
ipv6=None,
host=None,
settlement_token,
operating_system,
flavor=DEFAULT_FLAVOR,
api_endpoint=API_ENDPOINT,
cores=None,
currency=None,
region=None,
organization=None,
settlement_token=None,
ipxescript=None,
ipxescript_stdin=False,
ipxescript_file=None,
operating_system=None,
ssh_key=None,
ssh_key_file=None,
walkingliberty_wallet=None,
want_topup=False,
save=True,
affiliate_amount=None,
affiliate_token=None,
):
"""
Attempts to launch a server.
Flavor overrides cores, memory, etc settings.
Flavor is highly preferred, core/memory/disk/ipv4/ipv6 are deprecated.
"""
if memory is not None:
logging.warning("--memory is deprecated, please use --flavor instead.")
if cores is not None:
logging.warning("--cores is deprecated, please use --flavor instead.")
if disk is not None:
logging.warning("--disk is deprecated, please use --flavor instead.")
if want_topup is True:
# want_topup is ignored, always true basically now.
logging.warning("--want_topup is deprecated, please use --flavor instead.")
ipv4 = api_client.normalize_argument(ipv4)
ipv6 = api_client.normalize_argument(ipv6)
if ipv4 is not None:
logging.warning("--ipv4 is deprecated, please use --flavor instead.")
if ipv6 is not None:
logging.warning("--ipv6 is deprecated, please use --flavor instead.")
ipxescript_stdin = api_client.normalize_argument(ipxescript_stdin)
if settlement_token is not None:
if currency is None:
currency = "settlement"
@ -157,48 +98,25 @@ def launch(
message = "{} already created.".format(vm_hostname)
raise ValueError(message)
if host is None and api_endpoint is None:
raise ValueError("host and/or api_endpoint must be set.")
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()
ipxe_not_none_or_false = 0
for ipxe_option in [ipxescript, ipxescript_stdin, ipxescript_file]:
if ipxe_option not in [False, None]:
ipxe_not_none_or_false = ipxe_not_none_or_false + 1
msg = "Only set one of ipxescript, ipxescript_stdin, ipxescript_file"
if ipxe_not_none_or_false > 1:
raise ValueError(msg)
if ipxescript_stdin is True:
ipxescript = sys.stdin.read()
elif ipxescript_file is not None:
with open(ipxescript_file) as fp:
ipxescript = fp.read()
machine_id = random_machine_id()
def create_vm(host):
def create_vm():
create = api_client.launch
return create(
host=host,
machine_id=machine_id,
days=days,
flavor=flavor,
disk=disk,
memory=memory,
ipxescript=ipxescript,
operating_system=operating_system,
ssh_key=ssh_key,
cores=cores,
ipv4=ipv4,
ipv6=ipv6,
currency=currency,
region=region,
organization=organization,
settlement_token=settlement_token,
api_endpoint=api_endpoint,
affiliate_amount=affiliate_amount,
@ -206,11 +124,8 @@ def launch(
retry=True,
)
created_dict = create_vm(host)
created_dict = create_vm()
logging.debug(created_dict)
if api_endpoint is not None:
# Adjust host to whatever it gives us.
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:
uri = created_dict["payment"]["uri"]
@ -234,7 +149,7 @@ def launch(
# FIXME: Wait one hour in a smarter way.
# Waiting for payment to set in.
time.sleep(10)
created_dict = create_vm(host)
created_dict = create_vm()
logging.debug(created_dict)
if created_dict["paid"] is True:
break
@ -246,7 +161,7 @@ def launch(
tries = tries + 1
# Waiting for server to spin up.
time.sleep(10)
created_dict = create_vm(host)
created_dict = create_vm()
logging.debug(created_dict)
if created_dict["created"] is True:
break
@ -256,8 +171,6 @@ def launch(
# FIXME: Bad exception type.
raise ValueError("Server creation failed, tries exceeded.")
if "host" not in created_dict:
created_dict["host"] = host
created_dict["vm_hostname"] = vm_hostname
created_dict["machine_id"] = machine_id
created_dict["api_endpoint"] = api_endpoint
@ -272,7 +185,7 @@ def launch(
@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("--api-endpoint", type=str, default=None)
@cli.cmd_arg("--affiliate_amount", type=int, default=None)
@cli.cmd_arg("--affiliate_token", type=str, default=None)
def topup(
@ -297,14 +210,11 @@ def topup(
raise ValueError(message)
machine_info = get_machine_info(vm_hostname)
machine_id = machine_info["machine_id"]
# hostname of the host
host = machine_info["host"]
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
def topup_vm():
return api_client.topup(
host=host,
machine_id=machine_id,
days=days,
currency=currency,
@ -367,7 +277,7 @@ def save_machine_info(machine_info, overwrite=False):
if not os.path.exists(directory):
os.mkdir(directory)
vm_hostname = machine_info["vm_hostname"]
json_path = os.path.join(directory, "{}.json".format(vm_hostname))
json_path = os.path.join(directory, f"{vm_hostname}.json")
with open(json_path, mode) as json_file:
json.dump(machine_info, json_file)
return True
@ -379,9 +289,8 @@ def get_machine_info(vm_hostname):
"""
directory = machine_info_directory()
if not machine_exists(vm_hostname):
msg = "{} does not exist in {}".format(vm_hostname, directory)
raise ValueError(msg)
json_path = os.path.join(directory, "{}.json".format(vm_hostname))
raise ValueError(f"{vm_hostname} does not exist in {directory}")
json_path = os.path.join(directory, f"{vm_hostname}.json")
with open(json_path) as json_file:
machine_info = json.load(json_file)
if machine_info["vm_hostname"] != vm_hostname:
@ -389,16 +298,13 @@ def get_machine_info(vm_hostname):
return machine_info
def pretty_machine_info(info):
def pretty_machine_info(info) -> str:
msg = "Hostname: {}\n".format(info["vm_hostname"])
msg += "Machine ID (keep this secret!): {}\n".format(info["machine_id"])
if info["network_interfaces"][0] == {}:
msg += "SSH hostname: {}\n".format(info["sshhostname"])
else:
if "ipv6" in info["network_interfaces"][0]:
msg += "IPv6: {}\n".format(info["network_interfaces"][0]["ipv6"])
if "ipv4" in info["network_interfaces"][0]:
msg += "IPv4: {}\n".format(info["network_interfaces"][0]["ipv4"])
if "ipv6" in info["network_interfaces"][0]:
msg += "IPv6: {}\n".format(info["network_interfaces"][0]["ipv6"])
if "ipv4" in info["network_interfaces"][0]:
msg += "IPv4: {}\n".format(info["network_interfaces"][0]["ipv4"])
expiration = info["expiration"]
human_expiration = time.strftime("%Y-%m-%d %H:%M:%S %z", time.localtime(expiration))
if "running" in info:
@ -431,7 +337,6 @@ def list():
saved_vm_info = get_machine_info(vm_hostname)
try:
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"],
)
@ -498,144 +403,71 @@ def get_attribute(vm_hostname, attribute):
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("--api_endpoint", type=str, default=None)
@cli.cmd_arg("--api-endpoint", type=str, default=None)
def info(vm_hostname, api_endpoint=None):
"""
Info on the VM
"""
machine_info = get_machine_info(vm_hostname)
host = machine_info["host"]
machine_id = machine_info["machine_id"]
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
return api_client.info(host=host, machine_id=machine_id, api_endpoint=api_endpoint)
return api_client.info(machine_id=machine_id, api_endpoint=api_endpoint)
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("--api_endpoint", type=str, default=None)
@cli.cmd_arg("--api-endpoint", type=str, default=None)
def start(vm_hostname, api_endpoint=None):
"""
Boots the VM.
"""
machine_info = get_machine_info(vm_hostname)
host = machine_info["host"]
machine_id = machine_info["machine_id"]
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
return api_client.start(host=host, machine_id=machine_id, api_endpoint=api_endpoint)
return api_client.start(machine_id=machine_id, api_endpoint=api_endpoint)
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("--api_endpoint", type=str, default=None)
@cli.cmd_arg("--api-endpoint", type=str, default=None)
def stop(vm_hostname, api_endpoint=None):
"""
Immediately kills the VM.
"""
machine_info = get_machine_info(vm_hostname)
host = machine_info["host"]
machine_id = machine_info["machine_id"]
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
return api_client.stop(host=host, machine_id=machine_id, api_endpoint=api_endpoint)
return api_client.stop(machine_id=machine_id, api_endpoint=api_endpoint)
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("--api_endpoint", type=str, default=None)
@cli.cmd_arg("--api-endpoint", type=str, default=None)
def delete(vm_hostname, api_endpoint=None):
"""
Deletes the VM (most likely prematurely.
"""
machine_info = get_machine_info(vm_hostname)
host = machine_info["host"]
machine_id = machine_info["machine_id"]
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
api_client.delete(host=host, machine_id=machine_id, api_endpoint=api_endpoint)
api_client.delete(machine_id=machine_id, api_endpoint=api_endpoint)
# Also remove the .json file
remove(vm_hostname)
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("--api_endpoint", type=str, default=None)
def ipxescript(vm_hostname, ipxescript=None, api_endpoint=None):
"""
Trying to make this both useful as a CLI tool and
as a library. Not really sure how to do that best.
"""
machine_info = get_machine_info(vm_hostname)
host = machine_info["host"]
machine_id = machine_info["machine_id"]
if ipxescript is None:
# Not the safest for library use but this is generally just a CLI.
# __name__ is not __main__ here, which is weird and tricky.
ipxescript = sys.stdin.read()
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
return api_client.ipxescript(
host=host,
machine_id=machine_id,
ipxescript=ipxescript,
api_endpoint=api_endpoint,
)
@cli.cmd
@cli.cmd_arg("vm_hostname")
@cli.cmd_arg("bootorder")
@cli.cmd_arg("--api_endpoint", type=str, default=None)
def bootorder(vm_hostname, bootorder, api_endpoint=None):
machine_info = get_machine_info(vm_hostname)
host = machine_info["host"]
machine_id = machine_info["machine_id"]
if api_endpoint is None:
api_endpoint = machine_info["api_endpoint"]
return api_client.bootorder(
host=host, machine_id=machine_id, bootorder=bootorder, api_endpoint=api_endpoint
)
def api_endpoint_to_host(api_endpoint):
"""
Returns a likely workable host from just the endpoint.
This would most likely come up if building from a host directly, without
any API nodes.
Input should look like http://foo.bar or https://foo.bar. We just return
foo.bar.
"""
return api_endpoint.split("/")[2]
@cli.cmd
@cli.cmd_arg("vm_hostname")
def serialconsole(vm_hostname):
"""
ctrl + backslash to quit.
"""
machine_info = get_machine_info(vm_hostname)
host = machine_info["host"]
if host is None:
host = api_endpoint_to_host(machine_info["api_endpoint"])
machine_id = machine_info["machine_id"]
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)
@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,
@ -646,7 +478,7 @@ def settlement_token_enable(
Cents is starting balance.
"""
cents = _get_cents(dollars, cents)
cents = dollars * 100
def enable_token():
return api_client.settlement_token_enable(
@ -679,21 +511,12 @@ def settlement_token_enable(
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)
@cli.cmd_arg("--api-endpoint", type=str, default=API_ENDPOINT)
def settlement_token_add(
settlement_token,
dollars=None,
@ -706,7 +529,7 @@ def settlement_token_add(
Adds balance to an existing settlement token.
"""
cents = _get_cents(dollars, cents)
cents = dollars * 100
def add_to_token():
return api_client.settlement_token_add(
@ -741,7 +564,7 @@ def settlement_token_add(
@cli.cmd
@cli.cmd_arg("settlement_token")
@cli.cmd_arg("--api_endpoint", type=str, default=API_ENDPOINT)
@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.

9
sporestack/client_test.py

@ -0,0 +1,9 @@
from . import client
def test_random_machine_id():
assert len(client.random_machine_id()) == 64
def test_version():
assert "." in client.version()

49
sporestackv2/flavors.py → sporestack/flavors.py

@ -1,7 +1,8 @@
from typing import NamedTuple
from dataclasses import dataclass
class Flavor(NamedTuple):
@dataclass(frozen=True)
class Flavor:
# Unique string to identify the flavor that's sort of human readable.
slug: str
# Number of vCPU cores the server is given.
@ -12,9 +13,9 @@ class Flavor(NamedTuple):
disk: int
# USD cents per day
price: int
# IPv4 connectivity: "/32" or "tor". If "tor", needs to match IPv6 setting.
# IPv4 connectivity: "/32"
ipv4: str
# IPv6 connectivity: "/128" (may act as a /64, may not) or tor. If "tor", needs to match IPv4 setting.
# IPv6 connectivity: "/128"
ipv6: str
# Gigabytes of bandwidth per day
bandwidth: int
@ -172,44 +173,4 @@ all_sporestack_flavors = {
ipv6="/128",
bandwidth=438,
),
"tor-1024": Flavor(
slug="tor-1024",
cores=1,
memory=1024,
disk=8,
price=28,
ipv4="tor",
ipv6="tor",
bandwidth=20,
),
"tor-2048": Flavor(
slug="tor-2048",
cores=1,
memory=2048,
disk=16,
price=56,
ipv4="tor",
ipv6="tor",
bandwidth=40,
),
"tor-3072": Flavor(
slug="tor-3072",
cores=1,
memory=3072,
disk=24,
price=84,
ipv4="tor",
ipv6="tor",
bandwidth=60,
),
"tor-4096": Flavor(
slug="tor-4096",
cores=1,
memory=4096,
disk=32,
price=112,
ipv4="tor",
ipv6="tor",
bandwidth=80,
),
}

0
sporestackv2/utilities.py → sporestack/utilities.py

0
sporestackv2/utilities_test.py → sporestack/utilities_test.py

185
sporestackv2/validate.py → sporestack/validate.py

@ -5,11 +5,12 @@ All functions return True if valid, or raise an exception.
"""
import string
from typing import Optional
from sshpubkeys import SSHKey
def machine_id(machine_id):
def machine_id(machine_id: str) -> None:
"""
Validates the machine_id
Must be a 64 byte lowercase hex string.
@ -25,22 +26,23 @@ def machine_id(machine_id):
return True
def settlement_token(settlement_token):
def settlement_token(settlement_token: str) -> None:
"""
Validates a settlement token.
Identical format to machine IDs.
"""
return machine_id(settlement_token)
try:
machine_id(settlement_token)
except ValueError:
raise ValueError("settlement_token must be only 0-9, a-f (lowercase)")
def operating_system(operating_system):
def operating_system(operating_system: str) -> None:
"""
Validates an operating_system argument.
"""
if operating_system is None:
return True
if not isinstance(operating_system, str):
raise TypeError("operating_system must be null or a string.")
raise TypeError("operating_system must be a string.")
if len(operating_system) < 1:
raise ValueError("operating_system must have at least one letter.")
if len(operating_system) > 16:
@ -50,17 +52,13 @@ def operating_system(operating_system):
msg = "operating_system must only contain a-z, digits, -"
raise ValueError(msg)
return True
def flavor(flavor):
def flavor(flavor: str) -> None:
"""
Validates an flavor argument.
"""
if flavor is None:
return True
if not isinstance(flavor, str):
raise TypeError("flavor must be null or a string.")
raise TypeError("flavor must be a string.")
if len(flavor) < 1:
raise ValueError("flavor must have at least one letter.")
if len(flavor) > 16:
@ -70,15 +68,11 @@ def flavor(flavor):
msg = "flavor must only contain a-z, digits, -"
raise ValueError(msg)
return True
def ssh_key(ssh_key):
def ssh_key(ssh_key: str) -> None:
"""
Validates an ssh_key argument.
"""
if ssh_key is None:
return True
if not isinstance(ssh_key, str):
raise TypeError("ssh_key must be null or a string.")
@ -86,12 +80,10 @@ def ssh_key(ssh_key):
try:
ssh_key_object.parse()
except Exception as e:
raise ValueError("Invalid SSH key: {}".format(e))
return True
raise ValueError(f"Invalid SSH key: {str(e)}")
def days(days, zero_allowed=False):
def days(days: int, zero_allowed: bool = False) -> None:
"""
Makes sure our argument is valid.
0-28
@ -109,16 +101,14 @@ def days(days, zero_allowed=False):
minimum_days = 1
if days < minimum_days or days > 28:
raise ValueError("days must be {}-28".format(minimum_days))
return True
raise ValueError(f"days must be {minimum_days}-28")
def organization(organization):
def organization(organization: Optional[str]) -> None:
if organization is None:
return True
return
if not isinstance(organization, str):
raise TypeError("organization must be string.")
raise TypeError("organization must be null or a string.")
if len(organization) < 1:
raise ValueError("organization must have at least one letter.")
if len(organization) > 16:
@ -126,7 +116,6 @@ def organization(organization):
for character in organization:
if character not in string.ascii_letters:
raise ValueError("organization must only contain a-z, A-Z")
return True
def unsigned_int(variable):
@ -140,145 +129,25 @@ def unsigned_int(variable):
return True
def disk(disk):
"""
Makes sure disk is valid.
0 is valid, means no disk.
"""
if not unsigned_int(disk):
raise TypeError("disk must be an unsigned integer.")
return True
def memory(memory):
"""
Makes sure memory is valid.
"""
if not unsigned_int(memory):
raise TypeError("memory must be an unsigned integer.")
if memory == 0:
raise ValueError("0 not acceptable for memory")
return True
def expiration(expiration):
def expiration(expiration: int) -> None:
"""
Makes sure expiration is valid.
"""
if not unsigned_int(expiration):
raise TypeError("expiration must be an unsigned integer.")
return True
def cores(cores):
"""
Makes sure cores is valid.
"""
if not unsigned_int(cores):
raise TypeError("cores must be an unsigned integer.")
return True
def currency(currency):
def currency(currency: str) -> None:
"""
Makes sure currency is valid.
"""
if currency is None:
return True
if not isinstance(currency, str):
raise TypeError("currency must be None or str.")
return True
def bandwidth(bandwidth):
"""
Bandwidth is in gigabytes per day.
-1 is "unlimited".
0 means no bandwidth. Only valid if ipv4 and ipv6 are False.
"""
if isinstance(bandwidth, int):
if bandwidth >= -1:
return True
else:
raise ValueError("bandwidth can be no lower than -1.")
else:
raise TypeError("bandwidth must be integer.")
def _ip(ip, ip_type, cidr):
"""
Helper for ipv4 and ipv6
"""
# bool is int in Python 3, so have to test for bool first...
if ip is False:
return True
if not isinstance(ip, str):
raise TypeError("ipv4 must be false or string.")
elif ip == cidr:
return True
elif ip == "nat":
return True
elif ip == "tor":
return True
else:
raise ValueError("{} must be one of: False|{}".format(ip_type, cidr))
def ipv4(ipv4):
return _ip(ipv4, "ipv4", "/32")
def ipv6(ipv6):
return _ip(ipv6, "ipv6", "/128")
# further calls are for validating compatibilities between
# argument cominations.
def further_ipv4_ipv6(ipv4, ipv6):
"""
More validation with the combination of ipv4 and ipv6 options.
We don't support mixed nat/tor modes, so this handles that.
"""
message = "ipv4 and ipv6 must be the same if either is tor or nat."
if ipv4 in ["tor", "nat"] or ipv6 in ["tor", "nat"]:
if ipv4 != ipv6:
raise ValueError(message)
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
if not isinstance(ipxescript, str):
raise TypeError("ipxescript must be a string or null.")
if len(ipxescript) == 0:
raise ValueError("ipxescript must be more than zero bytes long.")
if len(ipxescript) > 4000:
raise ValueError("ipxescript must be less than 4,000 bytes long.")
for letter in ipxescript:
if letter not in string.printable:
raise ValueError("ipxescript must only contain ascii characters.")
return True
def region(region):
def region(region: Optional[str]) -> None:
if region is None:
return True
return
if not isinstance(region, str):
raise TypeError("region must be a string or null.")
if len(region) == 0:
@ -288,19 +157,17 @@ def region(region):
for letter in region:
if letter not in string.printable:
raise ValueError("region must only contain ascii characters.")
return True
def affiliate_amount(amount):
def affiliate_amount(amount: Optional[int]) -> None:
if amount is None:
return True
return
if unsigned_int(amount) is True:
if amount != 0:
return True
return
raise TypeError("affiliate_amount must be null or non-zero unsigned int.")
def cents(cents):
def cents(cents: int) -> None:
if not unsigned_int(cents):
raise TypeError("cents must be unsigned integer.")
return True

146
sporestackv2/validate_test.py → sporestack/validate_test.py

@ -9,7 +9,7 @@ invalid_id = "z1ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"
def test_machine_id():
assert validate.machine_id(valid_id) is True
validate.machine_id(valid_id)
with pytest.raises(TypeError):
validate.machine_id(1337)
with pytest.raises(ValueError):
@ -19,7 +19,7 @@ def test_machine_id():
def test_settlement_token():
assert validate.settlement_token(valid_id) is True
validate.settlement_token(valid_id)
with pytest.raises(TypeError):
validate.settlement_token(1337)
with pytest.raises(ValueError):
@ -32,11 +32,11 @@ def test_days():
with pytest.raises(ValueError):
validate.days(0)
assert validate.days(1) is True
assert validate.days(28) is True
assert validate.days(0, zero_allowed=True) is True
assert validate.days(1, zero_allowed=True) is True
assert validate.days(28, zero_allowed=True) is True
validate.days(1)
validate.days(28)
validate.days(0, zero_allowed=True)
validate.days(1, zero_allowed=True)
validate.days(28, zero_allowed=True)
with pytest.raises(ValueError):
validate.days(29)
@ -51,25 +51,6 @@ def test_days():
validate.days("one")
def test_memory():
assert validate.memory(1) is True
assert validate.memory(2) is True
with pytest.raises(TypeError):
validate.memory(-1)
with pytest.raises(ValueError):
validate.memory(0)
def test_disk():
assert validate.disk(10) is True
assert validate.disk(1) is True
assert validate.disk(0) is True
with pytest.raises(TypeError):
validate.disk(-10)
with pytest.raises(TypeError):
validate.disk("10")
def test_unsigned_int():
assert validate.unsigned_int(0) is True
assert validate.unsigned_int(1) is True
@ -80,23 +61,11 @@ def test_unsigned_int():
assert validate.unsigned_int(None) is False
def test_bandwidth():
assert validate.bandwidth(-1) is True
assert validate.bandwidth(1) is True
assert validate.bandwidth(0) is True
assert validate.bandwidth(10) is True
assert validate.bandwidth(1000000) is True
with pytest.raises(ValueError):
validate.bandwidth(-2)
with pytest.raises(TypeError):
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
validate.cents(1)
validate.cents(0)
validate.cents(10)
validate.cents(1000000)
with pytest.raises(TypeError):
validate.cents(-1)
with pytest.raises(TypeError):
@ -105,40 +74,15 @@ def test_cents():
validate.cents(None)
def test_further_ipv4_ipv6():
assert validate.further_ipv4_ipv6("tor", "tor") is True
assert validate.further_ipv4_ipv6("nat", "nat"<