-
Notifications
You must be signed in to change notification settings - Fork 0
/
vyvyan_daemon.py
executable file
·374 lines (337 loc) · 13.9 KB
/
vyvyan_daemon.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
#!/usr/bin/python
# system imports
import os
import sys
import datetime
import bottle
import traceback
from bottle import static_file
from bottle import response
from socket import gethostname
from jinja2 import *
# vyvyan imports
from vyvyan import configure
from vyvyan.common import *
# for >=2.6 use json, >2.6 use simplejson
try:
import json as myjson
except ImportError:
import simplejson as myjson
# turn on bottle debugging
bottle.debug(True)
# instantiate a bottle
httpservice = bottle.Bottle()
# suck in our configure object
cfg = configure.VyvyanConfigureDaemon('vyvyan_daemon.yaml')
cfg.load_config()
# generic vyvyan exception type
class VyvyanDaemonError(Exception):
pass
# create a json-able dict of important info
def __generate_json_header():
jbuf = {}
jbuf['status'] = 0
jbuf['msg'] = ""
jbuf['timestamp'] = str(datetime.datetime.now())
jbuf['nodename'] = gethostname()
return jbuf
# authenticate incoming connections
def __auth_conn(jbuf, authtype):
try:
if bottle.request.auth == (cfg.api_info_user, cfg.api_info_pass) and authtype == 'info':
return (True, jbuf)
elif bottle.request.auth == (cfg.api_admin_user, cfg.api_admin_pass) and (authtype == 'admin' or authtype == 'info'):
return (True, jbuf)
elif bottle.request.cookies['apitest'] == 'ThisIsTheWorstAuthenticationMechanismInTheHistoryOfEver' and authtype == 'info':
return (True, jbuf)
elif (bottle.request.auth == (cfg.api_info_user, cfg.api_info_pass) or bottle.request.cookies['apitest'] == 'letmein') and authtype == 'admin':
cfg.log.debug("authentication failed, you do not have admin-level access")
jbuf['status'] = 1
jbuf['data'] = ""
jbuf['msg'] = "authentication failed, you do not have admin-level access"
return (False, jbuf)
else:
cfg.log.debug("authentication failed, no user/pass supplied")
jbuf['status'] = 1
jbuf['data'] = ""
jbuf['msg'] = "authentication failed, no user/pass supplied"
return (False, jbuf)
except Exception, e:
cfg.log.debug("auth request failed. error: %s" % e)
jbuf['status'] = 1
jbuf['data'] = ""
jbuf['msg'] = "auth request failed. error: %s" % e
traceback.print_exc()
return (False, jbuf)
def load_modules(auth=True):
"""
scans our main path for modules, loads valid modules
"""
jbuf = __generate_json_header()
jbuf['request'] = "/loadmodules"
# authenticate the incoming request, only if we're being called externally
if auth:
authed, jbuf = __auth_conn(jbuf, 'admin')
if not authed:
response.content_type='text/html'
raise bottle.HTTPError(401, '/')
# assuming we're authed, do stuff
response.content_type='application/json'
cfg.log.debug("loadmodules() called directly")
# clear module metadata
old_metadata = cfg.module_metadata
cfg.module_metadata = {}
cfg.module_templates = {}
# base path we're being called from, to find our modules
basepath = sys.path[0]
try:
# get a list of all subdirectories under vyvyan
# uncomment to debug
#cfg.log.debug(os.walk(basepath+'/vyvyan/').next()[1])
for i in os.walk(basepath+'/vyvyan/').next()[1]:
cfg.log.debug(os.walk(basepath+'/vyvyan/'+i).next()[0])
if 'API_' in i:
try:
# import each module in the list above, grab the metadata
# from it's main class
cfg.log.debug("importing vyvyan.%s:" % i)
if i in old_metadata.keys():
try:
cfg.log.debug("unloading module: %s" % sys.modules['vyvyan.'+i])
del sys.modules['vyvyan.'+i]
except:
pass
cfg.log.debug("module unloaded: vyvyan."+i)
mod = __import__("vyvyan."+i)
cfg.log.debug("import complete")
foo = getattr(mod, i)
bar = getattr(foo, i)
inst = bar(cfg)
cfg.module_metadata[i] = inst
cfg.module_templates[i] = {}
cfg.log.debug("loading templates for module: vyvyan.%s" % i)
# get us some template lovin
env = Environment(loader=FileSystemLoader('vyvyan/'+i))
try:
for tfile in os.listdir(basepath+'/vyvyan/'+i):
# uncomment to debug
#cfg.log.debug("tfile: %s" % tfile)
if 'template' in tfile:
cfg.log.debug("loading template: %s" % tfile)
# this won't work since template data isn't JSON serializable
#template = env.get_template(tfile)
template = open(basepath+'/vyvyan/'+i+'/'+tfile)
if template:
cfg.module_templates[i][tfile] = template.read()
except Exception, e:
# uncomment to debug
#cfg.log.debug("template file: %s" % tfile)
#cfg.log.debug("template: %s" % template)
cfg.log.debug("template load error: %s" % e)
except Exception, e:
cfg.log.debug("import error: %s" % e)
else:
cfg.log.debug("skipping non-api module: %s" % i)
jbuf['data'] = "reloaded modules:"
for k in cfg.module_metadata.keys():
jbuf['data'] += " %s" % cfg.module_metadata[k].namespace
return myjson.JSONEncoder().encode(jbuf)
except Exception, e:
jbuf['status'] = 1
jbuf['data'] = ""
jbuf['msg'] = "Exception in load_modules(). Error: %s" % e
traceback.print_exc()
return myjson.JSONEncoder().encode(jbuf)
@httpservice.route('/')
def index():
"""
main url, currently spits back info about loaded modules and such
will probably change quite a lot before the rewrite gets going
more than likely will merge with /modules route below
"""
response.content_type='application/json'
jbuf = __generate_json_header()
jbuf['request'] = '/'
# authenticate the incoming request
authed, jbuf = __auth_conn(jbuf, 'info')
if not authed:
response.content_type='text/html'
raise bottle.HTTPError(401, '/')
# assuming we're authed, do stuff
try:
jbuf['data'] = "loaded modules: "
for k in cfg.module_metadata.keys():
cfg.log.debug('route: /')
cfg.log.debug('metadata key: '+k)
try:
jbuf['data'] += " %s" % cfg.module_metadata[k].namespace
except:
continue
jbuf['data'] += ". for more info on a module, call /<modulename>"
jbuf['data'] += ". to reload modules call: /loadmodules"
jbuf['data'] += ". to get JSON list of loaded modules call: /modules"
return myjson.JSONEncoder().encode(jbuf)
except Exception, e:
jbuf['status'] = 1
jbuf['data'] = "Exception in index(). Error: %s" % e
traceback.print_exc()
return myjson.JSONEncoder().encode(jbuf)
@httpservice.route('/modules')
def loaded_modules():
"""
returns a list of currently loaded modules
"""
response.content_type='application/json'
jbuf = __generate_json_header()
jbuf['request'] = '/modules'
# authenticate the incoming request
authed, jbuf = __auth_conn(jbuf, 'info')
if not authed:
response.content_type='text/html'
raise bottle.HTTPError(401, '/modules')
# assuming we're authed, do stuff
try:
jbuf['data'] = []
for k in cfg.module_metadata.keys():
cfg.log.debug('route: /')
cfg.log.debug('metadata key: '+k)
try:
jbuf['data'].append(cfg.module_metadata[k].namespace)
# uncomment to debug
#cfg.log.debug(jbuf)
except:
continue
return myjson.JSONEncoder().encode(jbuf)
except Exception, e:
jbuf['status'] = 1
jbuf['data'] = ""
jbuf['msg'] = "Exception in loaded_modules(). Error: %s" % e
traceback.print_exc()
return myjson.JSONEncoder().encode(jbuf)
@httpservice.route("/:pname")
def namespace_path(pname):
"""
returns data about a namespace's public functions
"""
response.content_type='application/json'
jbuf = __generate_json_header()
jbuf['request'] = "/%s" % pname
# authenticate the incoming request
authed, jbuf = __auth_conn(jbuf, 'info')
if not authed:
response.content_type='text/html'
raise bottle.HTTPError(401, '/'+pname)
# assuming we're authed, do stuff
try:
jbuf['data'] = {}
jbuf['data']['callable_functions'] = []
cfg.log.debug("jbuf: %s" % jbuf)
cfg.log.debug("pname: %s" %pname)
cfg.log.debug("cfg.module_metadata[pname]: %s " %cfg.module_metadata[pname].metadata)
for meth in cfg.module_metadata[pname].metadata['methods']:
cfg.log.debug(meth)
jbuf['data']['callable_functions'].append("/%s" % meth)
return myjson.JSONEncoder().encode(jbuf)
except Exception, e:
jbuf['status'] = 1
jbuf['data'] = ""
jbuf['msg'] = "Exception in namespace_path(). Error: %s" % e
traceback.print_exc()
return myjson.JSONEncoder().encode(jbuf)
@httpservice.route("/:pname/:callpath", method=('GET', 'POST', 'PUT', 'DELETE'))
def callable_path(pname, callpath):
"""
returns data about a function call or calls the function.
"""
# set up our buffer
response.content_type='application/json'
jbuf = __generate_json_header()
jbuf['request'] = "/%s/%s" % (pname, callpath)
# authenticate the incoming request just enough to get metadata
authed, jbuf = __auth_conn(jbuf, 'info')
if not authed:
response.content_type='text/html'
raise bottle.HTTPError(401, '/'+pname+'/'+callpath)
# assuming we're authed, do stuff
try:
query = bottle.request.POST
# uncomment for debugging
#cfg.log.debug(query)
filesdata = bottle.request.files
files = []
for kkkk in filesdata.keys():
files.append(filesdata[kkkk].file)
pnameMetadata = cfg.module_metadata[pname]
pnameMetadata.metadata['templates'] = cfg.module_templates[pname]
# every API module has a 'metadata' construct
# hard wire it into callpath options
# this is an info-level request so no re-auth
if callpath == 'metadata':
# uncomment for debugging
#cfg.log.debug(myjson.JSONEncoder(indent=4).encode(pnameMetadata.metadata))
jbuf['data'] = pnameMetadata.metadata
return myjson.JSONEncoder().encode(jbuf)
else:
pnameCallpath = pnameMetadata.metadata['methods'][callpath]
# we got an actual callpath! do stuff.
# uncomment for debugging
#cfg.log.debug("method called: %s" % myjson.JSONEncoder(indent=4).encode(cfg.module_metadata[pname].metadata['methods'][callpath]))
if bottle.request.method == pnameCallpath['rest_type']:
# check to see if the function we're calling is defined as "admin-only"
# which requires a different user/pass than "info-only" requests
if pnameCallpath['admin_only']:
reauthed, jbuf = __auth_conn(jbuf, 'admin')
if not reauthed:
response.content_type='text/html'
raise bottle.HTTPError(401, '/'+pname+'/'+callpath)
# fetch the method we were asked to call and instantiate it in "buf"
# this is the magic bit of anonymous function calls that allows
# the metadata construct to define what gets used for each query
# passed by the CLI script
buf = getattr(pnameMetadata, callpath)
# this is the block that actually runs the called method
if filesdata:
jbuf['data'] = buf(query, files)
else:
jbuf['data'] = buf(query)
# uncomment for debugging
#cfg.log.debug(myjson.JSONEncoder(indent=4).encode(jbuf))
# return our buffer
return myjson.JSONEncoder().encode(jbuf)
# explode violently
else:
raise VyvyanDaemonError("request method \"%s\" does not match allowed type \"%s\" for call \"/%s/%s\"" % (bottle.request.method, pnameCallpath['rest_type'], pname, callpath))
# catch and re-raise HTTP auth errors
except bottle.HTTPError:
raise bottle.HTTPError(401, '/'+pname+'/'+callpath)
except Exception, e:
jbuf['status'] = 1
jbuf['data'] = ""
jbuf['msg'] = "Exception in callable_path(). Error: %s" % e
traceback.print_exc()
return myjson.JSONEncoder().encode(jbuf)
@httpservice.route('/favicon.ico')
def get_favicon():
"""
returns a favicon
"""
try:
return static_file('favicon.ico', root='static/')
except Exception, e:
jbuf = __generate_json_header()
jbuf['request'] = "/favicon.ico"
jbuf['status'] = 1
jbuf['data'] = ""
jbuf['msg'] = "Exception in get_favicon(). Error: %s" % e
return myjson.JSONEncoder().encode(jbuf)
if __name__ == '__main__':
# set up our logging
try:
cfg.log = VyvyanLogger(cfg)
except Exception, e:
raise VyvyanDaemonError(e)
cfg.log.debug("initializing logger in vyvyan_daemon.py")
# run our module loader once at startup
load_modules(auth=False)
# the daemon
bottle.run(httpservice, host='0.0.0.0', port=8081, reloader=False)