docker-nodepupper/nodepupper/cli.py

169 lines
6.1 KiB
Python
Raw Permalink Normal View History

2019-01-26 15:10:57 -08:00
from appdirs import user_config_dir
import os
import json
import yaml
import argparse
import requests
import tempfile
import subprocess
APPNAME = "npcli"
CONFDIR = user_config_dir(APPNAME)
2019-04-16 20:50:06 -07:00
CONFPATH = os.path.join(CONFDIR, "conf.json") # ~/Library/Application Support/npcli/conf.json
2019-01-26 15:10:57 -08:00
def editorloop(fpath, validator):
"""
Open the editor until the user provides valid yaml.
Raises if we fail to edit the file.
"""
content = ""
while True:
subprocess.check_call([os.environ["EDITOR"], fpath]) # XXX commented for testing
with open(fpath) as f:
content = f.read()
try:
yaml.load(content)
break
except Exception as e:
print(e)
if input("Reopen editor? Y/n: ").lower() in ["", "y"]:
continue
else:
raise
return content
def main():
conf = {"host": "", "username": "", "password": ""}
if os.path.exists(CONFPATH):
with open(CONFPATH) as cf:
conf = json.load(cf)
else:
os.makedirs(CONFDIR, exist_ok=True)
with open(CONFPATH, "w") as cf:
json.dump(conf, cf)
parser = argparse.ArgumentParser(description="Nodepupper cli",
epilog="host/username/password will be saved to {} "
"after first use.".format(CONFPATH))
2019-04-16 20:50:06 -07:00
parser.add_argument("--host", help="http/s host to connect to",
default=os.environ.get('NPCLI_HOST', None) or conf["host"])
parser.add_argument("-u", "--username", help="username",
default=os.environ.get('NPCLI_USERNAME', None) or conf["username"])
parser.add_argument("-p", "--password", help="password",
default=os.environ.get('NPCLI_PASSWORD', None) or conf["password"])
2019-01-26 15:10:57 -08:00
spr_action = parser.add_subparsers(dest="action", help="action to take")
spr_action.add_parser("classlist", help="show list of classes")
2019-04-09 21:45:19 -07:00
spr_action.add_parser("nodelist", help="show list of nodes")
2019-01-26 15:10:57 -08:00
spr_new = spr_action.add_parser("new", help="create a node")
2019-04-27 15:52:27 -07:00
spr_new.add_argument("--parents", nargs="+", help="parent nodes")
2019-01-26 15:10:57 -08:00
spr_new.add_argument("node", help="name of node to create")
spr_edit = spr_action.add_parser("edit", help="edit a node")
spr_edit.add_argument("node", help="name of node to edit")
spr_del = spr_action.add_parser("del", help="delete a node")
spr_del.add_argument("node", help="name of node to delete")
spr_addc = spr_action.add_parser("addclass", help="add a class")
spr_addc.add_argument("cls", help="name of class to add")
2019-05-08 22:33:04 -07:00
spr_addc.add_argument("-r", "--rename", help="rename class")
2019-01-26 15:10:57 -08:00
spr_delc = spr_action.add_parser("delclass", help="delete a class")
spr_delc.add_argument("cls", help="name of class to delete")
2019-04-14 17:20:27 -07:00
spr_dump = spr_action.add_parser("dump", help="dump the database")
2019-04-15 21:36:44 -07:00
spr_import = spr_action.add_parser("import", help="import a database dump")
spr_import.add_argument("fname", help="db dump yaml file to import")
2019-01-26 15:10:57 -08:00
args = parser.parse_args()
r = requests.session()
2019-04-16 20:50:06 -07:00
if args.username and args.password:
r.auth = (args.username, args.password)
if not args.host:
parser.error('--host, $NPCLI_HOST, or config file is required')
2019-01-26 15:10:57 -08:00
def getnode(nodename):
req = r.get(args.host.rstrip("/") + "/api/node/" + nodename)
req.raise_for_status()
return req.text
def putnode(nodename, body):
return r.put(args.host.rstrip("/") + "/api/node/" + nodename, data=body)
if args.action == "new":
2019-04-27 15:52:27 -07:00
putnode(args.node, yaml.dump({"body": {},
"classes": {},
"parents": args.parents or []})).raise_for_status()
2019-01-26 15:10:57 -08:00
elif args.action == "del":
r.delete(args.host.rstrip("/") + "/api/node/" + args.node).raise_for_status()
elif args.action == "edit":
# TODO refuse if editor is unset
body = getnode(args.node)
newbody = None
with tempfile.TemporaryDirectory() as d:
tmppath = os.path.join(d, args.node)
with open(tmppath, "w") as f:
f.write(body)
newbody = editorloop(tmppath, lambda content: yaml.load(content))
if newbody != body:
try:
putnode(args.node, newbody).raise_for_status()
except Exception:
print("Your edits:\n")
print(newbody, "\n\n")
raise
else:
print("No changes, exiting")
2019-04-09 21:45:19 -07:00
elif args.action == "nodelist":
print(r.get(args.host.rstrip("/") + "/api/node").text)
2019-01-26 15:10:57 -08:00
elif args.action == "classlist":
print(r.get(args.host.rstrip("/") + "/api/class").text)
elif args.action == "addclass":
2019-05-08 22:33:04 -07:00
r.put(args.host.rstrip("/") + "/api/class/" + args.cls,
params={"rename": args.rename} if args.rename else None).raise_for_status()
2019-01-26 15:10:57 -08:00
elif args.action == "delclass":
r.delete(args.host.rstrip("/") + "/api/class/" + args.cls).raise_for_status()
2019-04-14 17:20:27 -07:00
elif args.action == "dump":
nodes = yaml.load(r.get(args.host.rstrip("/") + "/api/node").text)["nodes"]
dump = {"classes": yaml.load(r.get(args.host.rstrip("/") + "/api/class").text)["classes"],
"nodes": {}}
for nodename in nodes:
dump["nodes"][nodename] = yaml.load(getnode(nodename))
print(yaml.dump(dump, default_flow_style=False))
2019-04-15 21:36:44 -07:00
elif args.action == "import":
with open(args.fname) as f:
dump = yaml.load(f)
for clsname in dump["classes"]:
r.put(args.host.rstrip("/") + "/api/class/" + clsname).raise_for_status()
# just make the nodes first
for nodename, nodebody in dump["nodes"].items():
putnode(nodename, yaml.dump({"body": {}, "classes": {}, "parents": []})).raise_for_status()
# then fill out node bodies to avoid missing parent ordering issues
for nodename, nodebody in dump["nodes"].items():
putnode(nodename, yaml.dump(nodebody)).raise_for_status()
2019-01-26 15:10:57 -08:00
if __name__ == "__main__":
main()