Python 3 library and CLI application for SporeStack
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

785 lines
23 KiB

  1. #!/usr/bin/python3
  2. """
  3. Cleaner interface into api_client, for the most part.
  4. """
  5. import json
  6. import sys
  7. import secrets
  8. import os
  9. import logging
  10. import time
  11. from argparse import SUPPRESS
  12. import aaargh
  13. import pyqrcode
  14. from walkingliberty import WalkingLiberty
  15. from . import api_client
  16. from . import validate
  17. from .flavors import all_sporestack_flavors
  18. from .version import __version__
  19. cli = aaargh.App()
  20. logging.basicConfig(level=logging.INFO)
  21. CLEARNET_ENDPOINT = "https://api.sporestack.com"
  22. TOR_ENDPOINT = (
  23. "http://spore64" "i5sofqlfz5gq2ju4msgzojjwifls7rok2cti624zyq3fcelad.onion"
  24. )
  25. API_ENDPOINT = CLEARNET_ENDPOINT
  26. DEFAULT_FLAVOR = "vps-1vcpu-1gb"
  27. WAITING_PAYMENT_TO_PROCESS = "Waiting for payment to process..."
  28. def random_machine_id():
  29. """
  30. Makes a random Machine ID.
  31. These can also be deterministic, but then it wouldn't be called "random".
  32. """
  33. return secrets.token_hex(32)
  34. def make_payment(currency, uri, usd=None, walkingliberty_wallet=None):
  35. if walkingliberty_wallet is not None:
  36. walkingliberty = WalkingLiberty(currency)
  37. txid = walkingliberty.pay(private_key=walkingliberty_wallet, uri=uri)
  38. logging.debug("WalkingLiberty txid: {}".format(txid))
  39. else:
  40. premessage = """Payment URI: {}
  41. Pay *exactly* the specified amount. No more, no less. Pay within
  42. one hour at the very most.
  43. Resize your terminal and try again if QR code above is not readable.
  44. Press ctrl+c to abort."""
  45. message = premessage.format(uri)
  46. qr = pyqrcode.create(uri)
  47. print(qr.terminal(module_color="black", background="white", quiet_zone=1))
  48. print(message)
  49. if usd is not None:
  50. print("Approximate price in USD: {}".format(usd))
  51. input("[Press enter once you have made payment.]")
  52. # FIXME: ordering...
  53. @cli.cmd
  54. @cli.cmd_arg("vm_hostname")
  55. @cli.cmd_arg("--host", type=str, default=None)
  56. @cli.cmd_arg("--save", type=bool, default=True)
  57. @cli.cmd_arg("--region", type=str, default=None)
  58. @cli.cmd_arg("--currency", type=str, default=None)
  59. @cli.cmd_arg("--settlement_token", type=str, default=None)
  60. @cli.cmd_arg("--flavor", type=str, default=DEFAULT_FLAVOR)
  61. @cli.cmd_arg("--cores", type=int, help=SUPPRESS, default=None)
  62. @cli.cmd_arg("--memory", type=int, help=SUPPRESS, default=None)
  63. @cli.cmd_arg("--ipv4", help=SUPPRESS, default=None)
  64. @cli.cmd_arg("--ipv6", help=SUPPRESS, default=None)
  65. @cli.cmd_arg("--disk", type=int, default=None)
  66. @cli.cmd_arg("--days", type=int, required=True)
  67. @cli.cmd_arg("--walkingliberty_wallet", type=str, default=None)
  68. @cli.cmd_arg("--api_endpoint", type=str, default=API_ENDPOINT)
  69. @cli.cmd_arg("--want_topup", type=bool, default=False, help=SUPPRESS)
  70. @cli.cmd_arg("--organization", type=str, default=None)
  71. @cli.cmd_arg("--ipxescript", type=str, default=None)
  72. @cli.cmd_arg("--ipxescript_stdin", type=bool, default=False)
  73. @cli.cmd_arg("--ipxescript_file", type=str, default=None)
  74. @cli.cmd_arg("--operating_system", type=str, default=None)
  75. @cli.cmd_arg("--ssh_key", type=str, default=None)
  76. @cli.cmd_arg("--ssh_key_file", type=str, default=None)
  77. @cli.cmd_arg("--affiliate_amount", type=int, default=None)
  78. @cli.cmd_arg("--affiliate_token", type=str, default=None)
  79. def launch(
  80. vm_hostname,
  81. days,
  82. flavor=None,
  83. disk=None,
  84. memory=None,
  85. ipv4=None,
  86. ipv6=None,
  87. host=None,
  88. api_endpoint=API_ENDPOINT,
  89. cores=None,
  90. currency=None,
  91. region=None,
  92. organization=None,
  93. settlement_token=None,
  94. ipxescript=None,
  95. ipxescript_stdin=False,
  96. ipxescript_file=None,
  97. operating_system=None,
  98. ssh_key=None,
  99. ssh_key_file=None,
  100. walkingliberty_wallet=None,
  101. want_topup=False,
  102. save=True,
  103. affiliate_amount=None,
  104. affiliate_token=None,
  105. ):
  106. """
  107. Attempts to launch a server.
  108. Flavor overrides cores, memory, etc settings.
  109. Flavor is highly preferred, core/memory/disk/ipv4/ipv6 are deprecated.
  110. """
  111. if memory is not None:
  112. logging.warning("--memory is deprecated, please use --flavor instead.")
  113. if cores is not None:
  114. logging.warning("--cores is deprecated, please use --flavor instead.")
  115. if disk is not None:
  116. logging.warning("--disk is deprecated, please use --flavor instead.")
  117. if want_topup is True:
  118. # want_topup is ignored, always true basically now.
  119. logging.warning("--want_topup is deprecated, please use --flavor instead.")
  120. ipv4 = api_client.normalize_argument(ipv4)
  121. ipv6 = api_client.normalize_argument(ipv6)
  122. if ipv4 is not None:
  123. logging.warning("--ipv4 is deprecated, please use --flavor instead.")
  124. if ipv6 is not None:
  125. logging.warning("--ipv6 is deprecated, please use --flavor instead.")
  126. ipxescript_stdin = api_client.normalize_argument(ipxescript_stdin)
  127. if settlement_token is not None:
  128. if currency is None:
  129. currency = "settlement"
  130. if machine_exists(vm_hostname):
  131. message = "{} already created.".format(vm_hostname)
  132. raise ValueError(message)
  133. if host is None and api_endpoint is None:
  134. raise ValueError("host and/or api_endpoint must be set.")
  135. if ssh_key is not None and ssh_key_file is not None:
  136. raise ValueError("Only ssh_key or ssh_key_file can be set.")
  137. if ssh_key_file is not None:
  138. with open(ssh_key_file) as fp:
  139. ssh_key = fp.read()
  140. ipxe_not_none_or_false = 0
  141. for ipxe_option in [ipxescript, ipxescript_stdin, ipxescript_file]:
  142. if ipxe_option not in [False, None]:
  143. ipxe_not_none_or_false = ipxe_not_none_or_false + 1
  144. msg = "Only set one of ipxescript, ipxescript_stdin, ipxescript_file"
  145. if ipxe_not_none_or_false > 1:
  146. raise ValueError(msg)
  147. if ipxescript_stdin is True:
  148. ipxescript = sys.stdin.read()
  149. elif ipxescript_file is not None:
  150. with open(ipxescript_file) as fp:
  151. ipxescript = fp.read()
  152. machine_id = random_machine_id()
  153. def create_vm(host):
  154. create = api_client.launch
  155. return create(
  156. host=host,
  157. machine_id=machine_id,
  158. days=days,
  159. flavor=flavor,
  160. disk=disk,
  161. memory=memory,
  162. ipxescript=ipxescript,
  163. operating_system=operating_system,
  164. ssh_key=ssh_key,
  165. cores=cores,
  166. ipv4=ipv4,
  167. ipv6=ipv6,
  168. currency=currency,
  169. region=region,
  170. organization=organization,
  171. settlement_token=settlement_token,
  172. api_endpoint=api_endpoint,
  173. affiliate_amount=affiliate_amount,
  174. affiliate_token=affiliate_token,
  175. retry=True,
  176. )
  177. created_dict = create_vm(host)
  178. logging.debug(created_dict)
  179. if api_endpoint is not None:
  180. # Adjust host to whatever it gives us.
  181. host = created_dict["host"]
  182. # This will be false at least the first time if paying with BTC or BCH.
  183. if created_dict["paid"] is False:
  184. uri = created_dict["payment"]["uri"]
  185. if "usd" in created_dict["payment"]:
  186. usd = created_dict["payment"]["usd"]
  187. else:
  188. usd = None
  189. make_payment(
  190. currency=currency,
  191. uri=uri,
  192. usd=usd,
  193. walkingliberty_wallet=walkingliberty_wallet,
  194. )
  195. tries = 360
  196. while tries > 0:
  197. tries = tries - 1
  198. logging.info(WAITING_PAYMENT_TO_PROCESS)
  199. # FIXME: Wait one hour in a smarter way.
  200. # Waiting for payment to set in.
  201. time.sleep(10)
  202. created_dict = create_vm(host)
  203. logging.debug(created_dict)
  204. if created_dict["paid"] is True:
  205. break
  206. if created_dict["created"] is False:
  207. tries = 360
  208. while tries > 0:
  209. logging.info("Waiting for server to build...")
  210. tries = tries + 1
  211. # Waiting for server to spin up.
  212. time.sleep(10)
  213. created_dict = create_vm(host)
  214. logging.debug(created_dict)
  215. if created_dict["created"] is True:
  216. break
  217. if created_dict["created"] is False:
  218. logging.warning(created_dict)
  219. # FIXME: Bad exception type.
  220. raise ValueError("Server creation failed, tries exceeded.")
  221. if "host" not in created_dict:
  222. created_dict["host"] = host
  223. created_dict["vm_hostname"] = vm_hostname
  224. created_dict["machine_id"] = machine_id
  225. created_dict["api_endpoint"] = api_endpoint
  226. save_machine_info(created_dict)
  227. print(pretty_machine_info(created_dict), file=sys.stderr)
  228. return created_dict
  229. @cli.cmd
  230. @cli.cmd_arg("vm_hostname")
  231. @cli.cmd_arg("--currency", type=str, default=None)
  232. @cli.cmd_arg("--settlement_token", type=str, default=None)
  233. @cli.cmd_arg("--days", type=int, required=True)
  234. @cli.cmd_arg("--walkingliberty_wallet", type=str, default=None)
  235. @cli.cmd_arg("--api_endpoint", type=str, default=None)
  236. @cli.cmd_arg("--affiliate_amount", type=int, default=None)
  237. @cli.cmd_arg("--affiliate_token", type=str, default=None)
  238. def topup(
  239. vm_hostname,
  240. days,
  241. currency,
  242. settlement_token=None,
  243. walkingliberty_wallet=None,
  244. api_endpoint=None,
  245. affiliate_amount=None,
  246. affiliate_token=None,
  247. ):
  248. """
  249. tops up an existing vm.
  250. """
  251. if settlement_token is not None:
  252. if currency is None:
  253. currency = "settlement"
  254. if not machine_exists(vm_hostname):
  255. message = "{} does not exist.".format(vm_hostname)
  256. raise ValueError(message)
  257. machine_info = get_machine_info(vm_hostname)
  258. machine_id = machine_info["machine_id"]
  259. # hostname of the host
  260. host = machine_info["host"]
  261. if api_endpoint is None:
  262. api_endpoint = machine_info["api_endpoint"]
  263. def topup_vm():
  264. return api_client.topup(
  265. host=host,
  266. machine_id=machine_id,
  267. days=days,
  268. currency=currency,
  269. api_endpoint=api_endpoint,
  270. settlement_token=settlement_token,
  271. affiliate_amount=affiliate_amount,
  272. affiliate_token=affiliate_token,
  273. retry=True,
  274. )
  275. topped_dict = topup_vm()
  276. # This will be false at least the first time if paying with anything
  277. # but settlement.
  278. if topped_dict["paid"] is False:
  279. uri = topped_dict["payment"]["uri"]
  280. if "usd" in topped_dict["payment"]:
  281. usd = topped_dict["payment"]["usd"]
  282. else:
  283. usd = None
  284. make_payment(
  285. currency=currency,
  286. uri=uri,
  287. usd=usd,
  288. walkingliberty_wallet=walkingliberty_wallet,
  289. )
  290. tries = 360
  291. while tries > 0:
  292. logging.info(WAITING_PAYMENT_TO_PROCESS)
  293. tries = tries - 1
  294. # FIXME: Wait one hour in a smarter way.
  295. # Waiting for payment to set in.
  296. time.sleep(10)
  297. topped_dict = topup_vm()
  298. if topped_dict["paid"] is True:
  299. break
  300. machine_info["expiration"] = topped_dict["expiration"]
  301. save_machine_info(machine_info, overwrite=True)
  302. return machine_info["expiration"]
  303. def machine_info_directory():
  304. directory = os.path.join(os.getenv("HOME"), ".sporestackv2")
  305. return directory
  306. def save_machine_info(machine_info, overwrite=False):
  307. """
  308. Save info to disk.
  309. """
  310. if overwrite is True:
  311. mode = "w"
  312. else:
  313. mode = "x"
  314. os.umask(0o0077)
  315. directory = machine_info_directory()
  316. if not os.path.exists(directory):
  317. os.mkdir(directory)
  318. vm_hostname = machine_info["vm_hostname"]
  319. json_path = os.path.join(directory, "{}.json".format(vm_hostname))
  320. with open(json_path, mode) as json_file:
  321. json.dump(machine_info, json_file)
  322. return True
  323. def get_machine_info(vm_hostname):
  324. """
  325. Get info from disk.
  326. """
  327. directory = machine_info_directory()
  328. if not machine_exists(vm_hostname):
  329. msg = "{} does not exist in {}".format(vm_hostname, directory)
  330. raise ValueError(msg)
  331. json_path = os.path.join(directory, "{}.json".format(vm_hostname))
  332. with open(json_path) as json_file:
  333. machine_info = json.load(json_file)
  334. if machine_info["vm_hostname"] != vm_hostname:
  335. raise ValueError("vm_hostname does not match filename.")
  336. return machine_info
  337. def pretty_machine_info(info):
  338. msg = "Hostname: {}\n".format(info["vm_hostname"])
  339. msg += "Machine ID (keep this secret!): {}\n".format(info["machine_id"])
  340. if info["network_interfaces"][0] == {}:
  341. msg += "SSH hostname: {}\n".format(info["sshhostname"])
  342. else:
  343. if "ipv6" in info["network_interfaces"][0]:
  344. msg += "IPv6: {}\n".format(info["network_interfaces"][0]["ipv6"])
  345. if "ipv4" in info["network_interfaces"][0]:
  346. msg += "IPv4: {}\n".format(info["network_interfaces"][0]["ipv4"])
  347. expiration = info["expiration"]
  348. human_expiration = time.strftime("%Y-%m-%d %H:%M:%S %z", time.localtime(expiration))
  349. if "running" in info:
  350. msg += "Running: {}\n".format(info["running"])
  351. msg += "Expiration: {} ({})\n".format(expiration, human_expiration)
  352. time_to_live = expiration - int(time.time())
  353. hours = time_to_live // 3600
  354. msg += "Server will be deleted in {} hours.".format(hours)
  355. return msg
  356. @cli.cmd
  357. def flavors():
  358. """
  359. List all available flavors.
  360. """
  361. for flavor in all_sporestack_flavors:
  362. print(all_sporestack_flavors[flavor])
  363. @cli.cmd
  364. def list():
  365. """
  366. List all locally known servers.
  367. """
  368. directory = machine_info_directory()
  369. infos = []
  370. for vm_hostname_json in os.listdir(directory):
  371. vm_hostname = vm_hostname_json.split(".")[0]
  372. saved_vm_info = get_machine_info(vm_hostname)
  373. try:
  374. upstream_vm_info = api_client.info(
  375. host=saved_vm_info["host"],
  376. machine_id=saved_vm_info["machine_id"],
  377. api_endpoint=saved_vm_info["api_endpoint"],
  378. )
  379. saved_vm_info["expiration"] = upstream_vm_info["expiration"]
  380. saved_vm_info["running"] = upstream_vm_info["running"]
  381. infos.append(saved_vm_info)
  382. except ValueError as e:
  383. expiration = saved_vm_info["expiration"]
  384. human_expiration = time.strftime(
  385. "%Y-%m-%d %H:%M:%S %z", time.localtime(expiration)
  386. )
  387. msg = vm_hostname
  388. msg += " expired ({} {}): ".format(expiration, human_expiration)
  389. msg += str(e)
  390. print(msg)
  391. for info in infos:
  392. print()
  393. print(pretty_machine_info(info))
  394. print()
  395. return None
  396. def remove(vm_hostname):
  397. """
  398. Removes a server's .json file.
  399. """
  400. os.remove(machine_info_directory() + "/" + vm_hostname + ".json")
  401. @cli.cmd(name="remove")
  402. @cli.cmd_arg("vm_hostname")
  403. def remove_cli(vm_hostname):
  404. info = get_machine_info(vm_hostname)
  405. print(info)
  406. print(pretty_machine_info(info))
  407. remove(vm_hostname)
  408. print("{} removed.".format(vm_hostname))
  409. def machine_exists(vm_hostname):
  410. """
  411. Check if the VM's JSON exists locally.
  412. """
  413. directory = machine_info_directory()
  414. file_path = os.path.join(directory, "{}.json".format(vm_hostname))
  415. if os.path.exists(file_path):
  416. return True
  417. else:
  418. return False
  419. @cli.cmd
  420. @cli.cmd_arg("vm_hostname")
  421. @cli.cmd_arg("attribute")
  422. def get_attribute(vm_hostname, attribute):
  423. """
  424. Returns an attribute about the VM.
  425. """
  426. machine_info = get_machine_info(vm_hostname)
  427. return machine_info[attribute]
  428. @cli.cmd
  429. @cli.cmd_arg("vm_hostname")
  430. @cli.cmd_arg("--api_endpoint", type=str, default=None)
  431. def info(vm_hostname, api_endpoint=None):
  432. """
  433. Info on the VM
  434. """
  435. machine_info = get_machine_info(vm_hostname)
  436. host = machine_info["host"]
  437. machine_id = machine_info["machine_id"]
  438. if api_endpoint is None:
  439. api_endpoint = machine_info["api_endpoint"]
  440. return api_client.info(host=host, machine_id=machine_id, api_endpoint=api_endpoint)
  441. @cli.cmd
  442. @cli.cmd_arg("vm_hostname")
  443. @cli.cmd_arg("--api_endpoint", type=str, default=None)
  444. def start(vm_hostname, api_endpoint=None):
  445. """
  446. Boots the VM.
  447. """
  448. machine_info = get_machine_info(vm_hostname)
  449. host = machine_info["host"]
  450. machine_id = machine_info["machine_id"]
  451. if api_endpoint is None:
  452. api_endpoint = machine_info["api_endpoint"]
  453. return api_client.start(host=host, machine_id=machine_id, api_endpoint=api_endpoint)
  454. @cli.cmd
  455. @cli.cmd_arg("vm_hostname")
  456. @cli.cmd_arg("--api_endpoint", type=str, default=None)
  457. def stop(vm_hostname, api_endpoint=None):
  458. """
  459. Immediately kills the VM.
  460. """
  461. machine_info = get_machine_info(vm_hostname)
  462. host = machine_info["host"]
  463. machine_id = machine_info["machine_id"]
  464. if api_endpoint is None:
  465. api_endpoint = machine_info["api_endpoint"]
  466. return api_client.stop(host=host, machine_id=machine_id, api_endpoint=api_endpoint)
  467. @cli.cmd
  468. @cli.cmd_arg("vm_hostname")
  469. @cli.cmd_arg("--api_endpoint", type=str, default=None)
  470. def delete(vm_hostname, api_endpoint=None):
  471. """
  472. Deletes the VM (most likely prematurely.
  473. """
  474. machine_info = get_machine_info(vm_hostname)
  475. host = machine_info["host"]
  476. machine_id = machine_info["machine_id"]
  477. if api_endpoint is None:
  478. api_endpoint = machine_info["api_endpoint"]
  479. api_client.delete(host=host, machine_id=machine_id, api_endpoint=api_endpoint)
  480. # Also remove the .json file
  481. remove(vm_hostname)
  482. @cli.cmd
  483. @cli.cmd_arg("vm_hostname")
  484. @cli.cmd_arg("--api_endpoint", type=str, default=None)
  485. def ipxescript(vm_hostname, ipxescript=None, api_endpoint=None):
  486. """
  487. Trying to make this both useful as a CLI tool and
  488. as a library. Not really sure how to do that best.
  489. """
  490. machine_info = get_machine_info(vm_hostname)
  491. host = machine_info["host"]
  492. machine_id = machine_info["machine_id"]
  493. if ipxescript is None:
  494. # Not the safest for library use but this is generally just a CLI.
  495. # __name__ is not __main__ here, which is weird and tricky.
  496. ipxescript = sys.stdin.read()
  497. if api_endpoint is None:
  498. api_endpoint = machine_info["api_endpoint"]
  499. return api_client.ipxescript(
  500. host=host,
  501. machine_id=machine_id,
  502. ipxescript=ipxescript,
  503. api_endpoint=api_endpoint,
  504. )
  505. @cli.cmd
  506. @cli.cmd_arg("vm_hostname")
  507. @cli.cmd_arg("bootorder")
  508. @cli.cmd_arg("--api_endpoint", type=str, default=None)
  509. def bootorder(vm_hostname, bootorder, api_endpoint=None):
  510. machine_info = get_machine_info(vm_hostname)
  511. host = machine_info["host"]
  512. machine_id = machine_info["machine_id"]
  513. if api_endpoint is None:
  514. api_endpoint = machine_info["api_endpoint"]
  515. return api_client.bootorder(
  516. host=host, machine_id=machine_id, bootorder=bootorder, api_endpoint=api_endpoint
  517. )
  518. def api_endpoint_to_host(api_endpoint):
  519. """
  520. Returns a likely workable host from just the endpoint.
  521. This would most likely come up if building from a host directly, without
  522. any API nodes.
  523. Input should look like http://foo.bar or https://foo.bar. We just return
  524. foo.bar.
  525. """
  526. return api_endpoint.split("/")[2]
  527. @cli.cmd
  528. @cli.cmd_arg("vm_hostname")
  529. def serialconsole(vm_hostname):
  530. """
  531. ctrl + backslash to quit.
  532. """
  533. machine_info = get_machine_info(vm_hostname)
  534. host = machine_info["host"]
  535. if host is None:
  536. host = api_endpoint_to_host(machine_info["api_endpoint"])
  537. machine_id = machine_info["machine_id"]
  538. return api_client.serialconsole(host, machine_id)
  539. @cli.cmd
  540. @cli.cmd_arg("settlement_token")
  541. @cli.cmd_arg("--dollars", type=int, default=None)
  542. @cli.cmd_arg("--cents", type=int, default=None)
  543. @cli.cmd_arg("--currency", type=str, default=None, required=True)
  544. @cli.cmd_arg("--walkingliberty_wallet", type=str, default=None)
  545. @cli.cmd_arg("--api_endpoint", type=str, default=API_ENDPOINT)
  546. def settlement_token_enable(
  547. settlement_token,
  548. dollars=None,
  549. cents=None,
  550. currency=None,
  551. walkingliberty_wallet=None,
  552. api_endpoint=None,
  553. ):
  554. """
  555. Enables a new settlement token.
  556. Cents is starting balance.
  557. """
  558. cents = _get_cents(dollars, cents)
  559. def enable_token():
  560. return api_client.settlement_token_enable(
  561. settlement_token=settlement_token,
  562. cents=cents,
  563. currency=currency,
  564. api_endpoint=api_endpoint,
  565. retry=True,
  566. )
  567. enable_dict = enable_token()
  568. uri = enable_dict["payment_uri"]
  569. usd = enable_dict["usd"]
  570. make_payment(
  571. currency=currency, uri=uri, usd=usd, walkingliberty_wallet=walkingliberty_wallet
  572. )
  573. tries = 360
  574. while tries > 0:
  575. logging.info(WAITING_PAYMENT_TO_PROCESS)
  576. tries = tries - 1
  577. # FIXME: Wait one hour in a smarter way.
  578. # Waiting for payment to set in.
  579. time.sleep(10)
  580. enable_dict = enable_token()
  581. if enable_dict["paid"] is True:
  582. break
  583. return True
  584. def _get_cents(dollars, cents):
  585. validate._further_dollars_cents(dollars, cents)
  586. if dollars is not None:
  587. validate.cents(dollars)
  588. cents = dollars * 100
  589. return cents
  590. @cli.cmd
  591. @cli.cmd_arg("settlement_token")
  592. @cli.cmd_arg("--dollars", type=int, default=None)
  593. @cli.cmd_arg("--cents", type=int, default=None)
  594. @cli.cmd_arg("--currency", type=str, default=None, required=True)
  595. @cli.cmd_arg("--walkingliberty_wallet", type=str, default=None)
  596. @cli.cmd_arg("--api_endpoint", type=str, default=API_ENDPOINT)
  597. def settlement_token_add(
  598. settlement_token,
  599. dollars=None,
  600. cents=None,
  601. currency=None,
  602. walkingliberty_wallet=None,
  603. api_endpoint=None,
  604. ):
  605. """
  606. Adds balance to an existing settlement token.
  607. """
  608. cents = _get_cents(dollars, cents)
  609. def add_to_token():
  610. return api_client.settlement_token_add(
  611. settlement_token,
  612. cents,
  613. currency=currency,
  614. api_endpoint=api_endpoint,
  615. retry=True,
  616. )
  617. add_dict = add_to_token()
  618. uri = add_dict["payment_uri"]
  619. usd = add_dict["usd"]
  620. make_payment(
  621. currency=currency, uri=uri, usd=usd, walkingliberty_wallet=walkingliberty_wallet
  622. )
  623. tries = 360
  624. while tries > 0:
  625. logging.info(WAITING_PAYMENT_TO_PROCESS)
  626. tries = tries - 1
  627. # FIXME: Wait one hour in a smarter way.
  628. # Waiting for payment to set in.
  629. time.sleep(10)
  630. add_dict = add_to_token()
  631. if add_dict["paid"] is True:
  632. break
  633. return True
  634. @cli.cmd
  635. @cli.cmd_arg("settlement_token")
  636. @cli.cmd_arg("--api_endpoint", type=str, default=API_ENDPOINT)
  637. def settlement_token_balance(settlement_token, api_endpoint=None):
  638. """
  639. Gets balance for a settlement token.
  640. """
  641. return api_client.settlement_token_balance(
  642. settlement_token=settlement_token, api_endpoint=api_endpoint
  643. )
  644. @cli.cmd
  645. def settlement_token_generate():
  646. """
  647. Generates a settlement token that can be enabled.
  648. """
  649. return random_machine_id()
  650. @cli.cmd
  651. def version():
  652. """
  653. Returns the installed version.
  654. """
  655. return __version__
  656. def main():
  657. output = cli.run()
  658. if output is True:
  659. exit(0)
  660. elif output is False:
  661. exit(1)
  662. elif output is None:
  663. exit(0)
  664. else:
  665. print(output)
  666. if __name__ == "__main__":
  667. main()