169 lines
6.1 KiB
Python
169 lines
6.1 KiB
Python
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)
|
|
CONFPATH = os.path.join(CONFDIR, "conf.json") # ~/Library/Application Support/npcli/conf.json
|
|
|
|
|
|
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))
|
|
|
|
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"])
|
|
|
|
spr_action = parser.add_subparsers(dest="action", help="action to take")
|
|
spr_action.add_parser("classlist", help="show list of classes")
|
|
spr_action.add_parser("nodelist", help="show list of nodes")
|
|
|
|
spr_new = spr_action.add_parser("new", help="create a node")
|
|
spr_new.add_argument("--parents", nargs="+", help="parent nodes")
|
|
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")
|
|
spr_addc.add_argument("-r", "--rename", help="rename class")
|
|
|
|
spr_delc = spr_action.add_parser("delclass", help="delete a class")
|
|
spr_delc.add_argument("cls", help="name of class to delete")
|
|
|
|
spr_dump = spr_action.add_parser("dump", help="dump the database")
|
|
|
|
spr_import = spr_action.add_parser("import", help="import a database dump")
|
|
spr_import.add_argument("fname", help="db dump yaml file to import")
|
|
|
|
args = parser.parse_args()
|
|
r = requests.session()
|
|
|
|
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')
|
|
|
|
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":
|
|
putnode(args.node, yaml.dump({"body": {},
|
|
"classes": {},
|
|
"parents": args.parents or []})).raise_for_status()
|
|
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")
|
|
|
|
elif args.action == "nodelist":
|
|
print(r.get(args.host.rstrip("/") + "/api/node").text)
|
|
|
|
elif args.action == "classlist":
|
|
print(r.get(args.host.rstrip("/") + "/api/class").text)
|
|
|
|
elif args.action == "addclass":
|
|
r.put(args.host.rstrip("/") + "/api/class/" + args.cls,
|
|
params={"rename": args.rename} if args.rename else None).raise_for_status()
|
|
|
|
elif args.action == "delclass":
|
|
r.delete(args.host.rstrip("/") + "/api/class/" + args.cls).raise_for_status()
|
|
|
|
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))
|
|
|
|
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()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|