Add basic cli
This commit is contained in:
parent
3980153489
commit
da64324005
|
@ -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
|
return classes
|
||||||
|
|
||||||
|
|
||||||
|
def yamldump(data):
|
||||||
|
return yaml.dump(data, default_flow_style=False)
|
||||||
|
|
||||||
|
|
||||||
class AppWeb(object):
|
class AppWeb(object):
|
||||||
def __init__(self, nodedb, template_dir):
|
def __init__(self, nodedb, template_dir):
|
||||||
self.nodes = nodedb
|
self.nodes = nodedb
|
||||||
|
@ -118,7 +122,7 @@ class AppWeb(object):
|
||||||
if preview:
|
if preview:
|
||||||
yield "<plaintext>"
|
yield "<plaintext>"
|
||||||
yield "---\n"
|
yield "---\n"
|
||||||
yield yaml.dump(doc, default_flow_style=False)
|
yield yamldump(doc)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def login(self):
|
def login(self):
|
||||||
|
@ -145,6 +149,61 @@ class AppWeb(object):
|
||||||
yield self.render("error.html", status=status, message=message, traceback=traceback)
|
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")
|
@cherrypy.popargs("node")
|
||||||
class NodesWeb(object):
|
class NodesWeb(object):
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
|
@ -220,10 +279,11 @@ def main():
|
||||||
tpl_dir = os.path.join(APPROOT, "templates") if not args.debug else "templates"
|
tpl_dir = os.path.join(APPROOT, "templates") if not args.debug else "templates"
|
||||||
|
|
||||||
web = AppWeb(library, tpl_dir)
|
web = AppWeb(library, tpl_dir)
|
||||||
|
napi = NodesApi(library)
|
||||||
|
capi = ClassesApi(library)
|
||||||
|
|
||||||
def validate_password(realm, username, password):
|
def validate_password(realm, username, password):
|
||||||
s = library.session()
|
if 1:
|
||||||
if s.query(User).filter(User.name == username, User.password == pwhash(password)).first():
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -236,6 +296,8 @@ def main():
|
||||||
'/login': {'tools.auth_basic.on': True,
|
'/login': {'tools.auth_basic.on': True,
|
||||||
'tools.auth_basic.realm': 'webapp',
|
'tools.auth_basic.realm': 'webapp',
|
||||||
'tools.auth_basic.checkpassword': validate_password}})
|
'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({
|
cherrypy.config.update({
|
||||||
'tools.sessions.on': True,
|
'tools.sessions.on': True,
|
||||||
|
|
|
@ -24,6 +24,9 @@ class NObject(persistent.Persistent):
|
||||||
def parent_names(self):
|
def parent_names(self):
|
||||||
return [n.fqdn for n in self.parents]
|
return [n.fqdn for n in self.parents]
|
||||||
|
|
||||||
|
def class_names(self):
|
||||||
|
return list(self.classes.keys())
|
||||||
|
|
||||||
|
|
||||||
class NClass(persistent.Persistent):
|
class NClass(persistent.Persistent):
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
appdirs==1.4.3
|
||||||
backports.functools-lru-cache==1.5
|
backports.functools-lru-cache==1.5
|
||||||
BTrees==4.5.1
|
BTrees==4.5.1
|
||||||
|
certifi==2018.11.29
|
||||||
|
chardet==3.0.4
|
||||||
cheroot==6.5.2
|
cheroot==6.5.2
|
||||||
CherryPy==18.0.1
|
CherryPy==18.0.1
|
||||||
|
idna==2.8
|
||||||
jaraco.functools==1.20
|
jaraco.functools==1.20
|
||||||
Jinja2==2.10
|
Jinja2==2.10
|
||||||
MarkupSafe==1.0
|
MarkupSafe==1.0
|
||||||
|
@ -10,9 +14,11 @@ persistent==4.4.2
|
||||||
portend==2.3
|
portend==2.3
|
||||||
pytz==2018.5
|
pytz==2018.5
|
||||||
PyYAML==3.13
|
PyYAML==3.13
|
||||||
|
requests==2.21.0
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
tempora==1.13
|
tempora==1.13
|
||||||
transaction==2.2.1
|
transaction==2.2.1
|
||||||
|
urllib3==1.24.1
|
||||||
zc.lockfile==1.3.0
|
zc.lockfile==1.3.0
|
||||||
ZConfig==3.3.0
|
ZConfig==3.3.0
|
||||||
ZODB==5.4.0
|
ZODB==5.4.0
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -16,7 +16,8 @@ setup(name='nodepupper',
|
||||||
install_requires=[],
|
install_requires=[],
|
||||||
entry_points={
|
entry_points={
|
||||||
"console_scripts": [
|
"console_scripts": [
|
||||||
"nodepupperd = nodepupper.daemon:main"
|
"nodepupperd = nodepupper.daemon:main",
|
||||||
|
"npcli = nodepupper.cli:main"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
|
Loading…
Reference in New Issue