Compare commits
1 Commits
9b1c0d1785
...
63a837c2ee
Author | SHA1 | Date |
---|---|---|
dave | 63a837c2ee |
|
@ -0,0 +1,119 @@
|
|||
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")
|
||||
|
||||
|
||||
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", default=conf["host"], help="http/s host to connect to")
|
||||
# parser.add_argument("-u", "--username", help="username")
|
||||
# parser.add_argument("-p", "--password", help="password")
|
||||
|
||||
spr_action = parser.add_subparsers(dest="action", help="action to take")
|
||||
spr_action.add_parser("classlist", help="show list of classes")
|
||||
|
||||
spr_new = spr_action.add_parser("new", help="create a node")
|
||||
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_delc = spr_action.add_parser("delclass", help="delete a class")
|
||||
spr_delc.add_argument("cls", help="name of class to delete")
|
||||
|
||||
args = parser.parse_args()
|
||||
r = requests.session()
|
||||
|
||||
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": []})).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 == "classlist":
|
||||
print(r.get(args.host.rstrip("/") + "/api/class").text)
|
||||
|
||||
elif args.action == "addclass":
|
||||
r.put(args.host.rstrip("/") + "/api/class/" + args.cls).raise_for_status()
|
||||
|
||||
elif args.action == "delclass":
|
||||
r.delete(args.host.rstrip("/") + "/api/class/" + args.cls).raise_for_status()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -53,6 +53,10 @@ def recurse_classes(node):
|
|||
return classes
|
||||
|
||||
|
||||
def yamldump(data):
|
||||
return yaml.dump(data, default_flow_style=False)
|
||||
|
||||
|
||||
class AppWeb(object):
|
||||
def __init__(self, nodedb, template_dir):
|
||||
self.nodes = nodedb
|
||||
|
@ -118,7 +122,7 @@ class AppWeb(object):
|
|||
if preview:
|
||||
yield "<plaintext>"
|
||||
yield "---\n"
|
||||
yield yaml.dump(doc, default_flow_style=False)
|
||||
yield yamldump(doc)
|
||||
|
||||
@cherrypy.expose
|
||||
def login(self):
|
||||
|
@ -145,6 +149,61 @@ class AppWeb(object):
|
|||
yield self.render("error.html", status=status, message=message, traceback=traceback)
|
||||
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.popargs("node")
|
||||
class NodesApi(object):
|
||||
def __init__(self, nodedb):
|
||||
self.nodes = nodedb
|
||||
|
||||
def GET(self, node):
|
||||
with self.nodes.db.transaction() as c:
|
||||
node = c.root.nodes[node]
|
||||
output = {
|
||||
"body": yaml.load(node.body),
|
||||
"parents": node.parent_names(),
|
||||
"classes": {clsname: yaml.load(clsa.conf) for clsname, clsa in node.classes.items()}
|
||||
}
|
||||
yield yamldump(output)
|
||||
|
||||
def PUT(self, node):
|
||||
nodeyaml = yaml.load(cherrypy.request.body.read().decode('utf-8'))
|
||||
with self.nodes.db.transaction() as c:
|
||||
newnode = NObject(node, yamldump(nodeyaml["body"]))
|
||||
for clsname, clsbody in nodeyaml["classes"].items():
|
||||
newnode.classes[clsname] = NClassAttachment(c.root.classes[clsname], yamldump(clsbody))
|
||||
for parent in nodeyaml["parents"]:
|
||||
newnode.parents.append(c.root.nodes[parent])
|
||||
c.root.nodes[node] = newnode
|
||||
|
||||
def DELETE(self, node):
|
||||
with self.nodes.db.transaction() as c:
|
||||
del c.root.nodes[node]
|
||||
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.popargs("cls")
|
||||
class ClassesApi(object):
|
||||
def __init__(self, nodedb):
|
||||
self.nodes = nodedb
|
||||
|
||||
def GET(self, cls=None):
|
||||
with self.nodes.db.transaction() as c:
|
||||
clslist = list(c.root.classes.keys())
|
||||
clslist.sort()
|
||||
yield yamldump({"classes": clslist})
|
||||
|
||||
def PUT(self, cls):
|
||||
with self.nodes.db.transaction() as c:
|
||||
c.root.classes[cls] = NClass(cls)
|
||||
|
||||
def DELETE(self, cls):
|
||||
with self.nodes.db.transaction() as c:
|
||||
for node in c.root.nodes.values():
|
||||
if cls in node.class_names():
|
||||
raise Exception("Class is in use by '{}'".format(node.fqdn))
|
||||
del c.root.classes[cls]
|
||||
|
||||
|
||||
@cherrypy.popargs("node")
|
||||
class NodesWeb(object):
|
||||
def __init__(self, root):
|
||||
|
@ -220,10 +279,11 @@ def main():
|
|||
tpl_dir = os.path.join(APPROOT, "templates") if not args.debug else "templates"
|
||||
|
||||
web = AppWeb(library, tpl_dir)
|
||||
napi = NodesApi(library)
|
||||
capi = ClassesApi(library)
|
||||
|
||||
def validate_password(realm, username, password):
|
||||
s = library.session()
|
||||
if s.query(User).filter(User.name == username, User.password == pwhash(password)).first():
|
||||
if 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -236,6 +296,8 @@ def main():
|
|||
'/login': {'tools.auth_basic.on': True,
|
||||
'tools.auth_basic.realm': 'webapp',
|
||||
'tools.auth_basic.checkpassword': validate_password}})
|
||||
cherrypy.tree.mount(napi, '/api/node', {'/': {'request.dispatch': cherrypy.dispatch.MethodDispatcher()}})
|
||||
cherrypy.tree.mount(capi, '/api/class', {'/': {'request.dispatch': cherrypy.dispatch.MethodDispatcher()}})
|
||||
|
||||
cherrypy.config.update({
|
||||
'tools.sessions.on': True,
|
||||
|
|
|
@ -24,6 +24,9 @@ class NObject(persistent.Persistent):
|
|||
def parent_names(self):
|
||||
return [n.fqdn for n in self.parents]
|
||||
|
||||
def class_names(self):
|
||||
return list(self.classes.keys())
|
||||
|
||||
|
||||
class NClass(persistent.Persistent):
|
||||
def __init__(self, name):
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
appdirs==1.4.3
|
||||
backports.functools-lru-cache==1.5
|
||||
BTrees==4.5.1
|
||||
certifi==2018.11.29
|
||||
chardet==3.0.4
|
||||
cheroot==6.5.2
|
||||
CherryPy==18.0.1
|
||||
idna==2.8
|
||||
jaraco.functools==1.20
|
||||
Jinja2==2.10
|
||||
MarkupSafe==1.0
|
||||
|
@ -10,9 +14,11 @@ persistent==4.4.2
|
|||
portend==2.3
|
||||
pytz==2018.5
|
||||
PyYAML==3.13
|
||||
requests==2.21.0
|
||||
six==1.11.0
|
||||
tempora==1.13
|
||||
transaction==2.2.1
|
||||
urllib3==1.24.1
|
||||
zc.lockfile==1.3.0
|
||||
ZConfig==3.3.0
|
||||
ZODB==5.4.0
|
||||
|
|
Loading…
Reference in New Issue