diff --git a/monkey_business/cc/admin/ui/css/img/datatables/sort_asc.png b/monkey_business/cc/admin/ui/css/img/datatables/sort_asc.png new file mode 100644 index 000000000..e1ba61a80 Binary files /dev/null and b/monkey_business/cc/admin/ui/css/img/datatables/sort_asc.png differ diff --git a/monkey_business/cc/admin/ui/css/img/datatables/sort_asc_disabled.png b/monkey_business/cc/admin/ui/css/img/datatables/sort_asc_disabled.png new file mode 100644 index 000000000..fb11dfe24 Binary files /dev/null and b/monkey_business/cc/admin/ui/css/img/datatables/sort_asc_disabled.png differ diff --git a/monkey_business/cc/admin/ui/css/img/datatables/sort_both.png b/monkey_business/cc/admin/ui/css/img/datatables/sort_both.png new file mode 100644 index 000000000..af5bc7c5a Binary files /dev/null and b/monkey_business/cc/admin/ui/css/img/datatables/sort_both.png differ diff --git a/monkey_business/cc/admin/ui/css/img/datatables/sort_desc.png b/monkey_business/cc/admin/ui/css/img/datatables/sort_desc.png new file mode 100644 index 000000000..0e156deb5 Binary files /dev/null and b/monkey_business/cc/admin/ui/css/img/datatables/sort_desc.png differ diff --git a/monkey_business/cc/admin/ui/css/img/datatables/sort_desc_disabled.png b/monkey_business/cc/admin/ui/css/img/datatables/sort_desc_disabled.png new file mode 100644 index 000000000..c9fdd8a15 Binary files /dev/null and b/monkey_business/cc/admin/ui/css/img/datatables/sort_desc_disabled.png differ diff --git a/monkey_business/cc/admin/ui/index.html b/monkey_business/cc/admin/ui/index.html index 44fb98e56..b938c8199 100644 --- a/monkey_business/cc/admin/ui/index.html +++ b/monkey_business/cc/admin/ui/index.html @@ -48,7 +48,7 @@
- + @@ -83,7 +83,17 @@ New Job
-
+
+
+
+ +
diff --git a/monkey_business/cc/admin/ui/js/monkeysb-admin.js b/monkey_business/cc/admin/ui/js/monkeysb-admin.js index 1dfbd3fdf..0033d6beb 100644 --- a/monkey_business/cc/admin/ui/js/monkeysb-admin.js +++ b/monkey_business/cc/admin/ui/js/monkeysb-admin.js @@ -1,18 +1,8 @@ -/*const jsonFile = "/api/jbos"; -var monkeys = null; -var generationDate = null;*/ - // The JSON must be fully loaded before onload() happens for calling draw() on 'monkeys' $.ajaxSetup({ async: false }); -// Reading the JSON file containing the monkeys' informations -/*$.getJSON(jsonFile, function(json) { - jobs = json.objects; - generationDate = json.timestamp; -});*/ - // Images/icons constants const ICONS_DIR = "./css/img/objects/"; const ICONS_EXT = ".png"; @@ -22,15 +12,27 @@ const ICONS_EXT = ".png"; var jobsTable = undefined; var vcenterCfg = undefined; +var jobCfg = undefined; JSONEditor.defaults.theme = 'bootstrap3'; - function initAdmin() { jobsTable = $("#jobs-table").DataTable({ - "ordering": false, + "ordering": true, + "order": [[1, "desc"]], }); + jobsTable.on( 'click', 'tr', function () { + if ( $(this).hasClass('selected') ) { + $(this).removeClass('selected'); + } + else { + jobsTable.$('tr.selected').removeClass('selected'); + $(this).addClass('selected'); + } + jobdata = jobsTable.row(this).data(); + createNewJob(jobdata[0], jobdata[3]); + } ); vcenterCfg = new JSONEditor(document.getElementById('vcenter-config'),{ schema: { @@ -75,7 +77,8 @@ function initAdmin() { }, datacenter_name: { title: "Datacenter (opt.)", - type: "string", }, + type: "string", + }, cluster_name: { title: "Cluster (opt.)", type: "string", @@ -93,31 +96,26 @@ function initAdmin() { }, disable_edit_json: false, disable_properties: true, - startval: $, }); - window.setTimeout(updateJobs, 10000); + window.setTimeout(updateJobs, 5000); loadVcenterConfig(); updateJobs(); } -function updateVCenterConf() { - -} - function updateJobs() { $.getJSON('/job', function(json) { jobsTable.clear(); - var jobs = json.objects; + var jobsList = json.objects; - for (var i = 0; i < jobs.length; i++) { - jobsTable.row.add([jobs[i].timestamp, jobs[i].status, JSON.stringify(jobs[i].data)]); + for (var i = 0; i < jobsList.length; i++) { + jobsTable.row.add([jobsList[i].id, jobsList[i].creation_time, jobsList[i].type,jobsList[i].execution.state, JSON.stringify(jobsList[i].properties)]); } jobsTable.draw(); + //enableJobsSelect(); }); - } function loadVcenterConfig() { @@ -152,60 +150,108 @@ function updateVcenterConfig() { } -function createNewJob() { +function createNewJob(id, state) { + if (!id) { + jobsTable.$('tr.selected').removeClass('selected'); + } + elem = document.getElementById('job-config'); elem.innerHTML = "" jobCfg = new JSONEditor(elem,{ - schema: { - type: "object", - title: "Job", - properties: { - job: { - title: "Type", - $ref: "/jobcreate", - } - }, - options: { - "collapsed": false - }, - }, - ajax: true, - disable_edit_json: false, - disable_collapse: true, - disable_properties: true, - }); + schema: { + type: "object", + title: "Job", + properties: { + job: { + title: "Type", + $ref: "/jobcreate" + ((id)?"?id="+id:""), + } + }, + options: { + "collapsed": false + }, + }, + ajax: true, + disable_edit_json: false, + disable_collapse: true, + disable_properties: true, + no_additional_properties: true + }); + + jobCfg.on('ready',function() { + if (id && state != "pending") { + jobCfg.disable(); + document.getElementById("btnSendJob").style.visibility = "hidden"; + document.getElementById("btnDeleteJob").style.visibility = "hidden"; + } + else { + jobCfg.enable(); + document.getElementById("btnSendJob").style.visibility = "visible"; + if (id) { + document.getElementById("btnDeleteJob").style.visibility = "visible"; + } + else { + document.getElementById("btnDeleteJob").style.visibility = "hidden"; + } + } + }); +} + +function sendJob() { + var job_config = jobCfg.getValue() + + $.ajax({ + headers : { + 'Accept' : 'application/json', + 'Content-Type' : 'application/json' + }, + url : '/jobcreate', + type : 'POST', + data : JSON.stringify(job_config.job), + success : function(response, textStatus, jqXhr) { + console.log("Job successfully updated!"); + updateJobs(); + }, + error : function(jqXHR, textStatus, errorThrown) { + // log the error to the console + console.log("The following error occured: " + textStatus, errorThrown); + }, + complete : function() { + console.log("Sending job config..."); + } + }); +} + +function deleteJob() { + var job_config = jobCfg.getValue(); + if (job_config.job.id) { + $.ajax({ + headers : { + 'Accept' : 'application/json', + 'Content-Type' : 'application/json' + }, + url : '/jobcreate', + type : 'GET', + data : "action=delete&id=" + job_config.job.id, + success : function(response, textStatus, jqXhr) { + console.log("Job successfully updated!"); + updateJobs(); + }, + error : function(jqXHR, textStatus, errorThrown) { + // log the error to the console + console.log("The following error occured: " + textStatus, errorThrown); + }, + complete : function() { + console.log("Sending job config..."); + } + }); + } } function configSched() { } -/** - * Manage the event when an object is selected - */ -function onSelect(properties) { - - /*if (properties.nodes.length > 0) { - onNodeSelect(properties.nodes); - } - else - { - var content = "No selection" - $("#selectionInfo").html(content); - $('#monkey-config').hide() - $('#btnConfigLoad, #btnConfigUpdate').hide(); - telemTable.clear(); - telemTable.draw(); - }*/ - - /*if (properties.edges.length > 0) { - onEdgeSelect(properties.edges); - }*/ - -} - - - /** * Clears the value in the local storage */ diff --git a/monkey_business/cc/connectors/__init__.py b/monkey_business/cc/connectors/__init__.py index 4eeedfc9e..71520bc0c 100644 --- a/monkey_business/cc/connectors/__init__.py +++ b/monkey_business/cc/connectors/__init__.py @@ -1,17 +1,17 @@ +def _load_prop_dict(self, target, prop): + for property in prop: + if not target.has_key(property): + continue + if type(prop[property]) is dict: + _load_prop_dict(self, target[property], prop[property]) + else: + target[property] = prop[property] + class NetControllerConnector(object): def __init__(self): self._properties = {} - def _load_prop_dict(self, target, prop): - for property in prop: - if not target.has_key(property): - continue - if type(prop[property]) is dict: - self._load_prop_dict(target[property], prop[property]) - else: - target[property] = prop[property] - def is_connected(self): return False @@ -22,7 +22,7 @@ class NetControllerConnector(object): return self._properties def load_properties(self, properties): - self._load_prop_dict(self._properties, properties) + _load_prop_dict(self, self._properties, properties) def get_vlans_list(self): raise NotImplementedError() @@ -38,17 +38,27 @@ class NetControllerConnector(object): class NetControllerJob(object): connector = NetControllerConnector + _properties = { + # property: value + } + + _enumerations = { + + } def __init__(self): - self._properties = { - # property: [value, enumerating_function] - } + pass def get_job_properties(self): return self._properties - def set_job_properties(self, properties): - return {} + def load_job_properties(self, properties): + _load_prop_dict(self, self._properties, properties) + + def get_property_function(self, property): + if property in self._enumerations.keys(): + return self._enumerations[property] + return None def run(self): raise NotImplementedError() \ No newline at end of file diff --git a/monkey_business/cc/connectors/demo.py b/monkey_business/cc/connectors/demo.py index 15a61e0ab..a294b6526 100644 --- a/monkey_business/cc/connectors/demo.py +++ b/monkey_business/cc/connectors/demo.py @@ -36,8 +36,9 @@ class DemoConnector(NetControllerConnector): class DemoJob(NetControllerJob): connector = DemoConnector - - def __init__(self): - self._properties = { - "vlan": [0, "get_vlans_list"], - } + _properties = { + "vlan": 0, + } + _enumerations = { + "vlan": "get_vlans_list", + } \ No newline at end of file diff --git a/monkey_business/cc/connectors/vcenter.py b/monkey_business/cc/connectors/vcenter.py index 30cfc122b..61e913100 100644 --- a/monkey_business/cc/connectors/vcenter.py +++ b/monkey_business/cc/connectors/vcenter.py @@ -63,7 +63,7 @@ class VCenterConnector(NetControllerConnector): def get_entities_on_vlan(self, vlanid): return [] - def deploy_monkey(self, vlanid): + def deploy_monkey(self, vlanid, vm_name): if not self._properties["monkey_template_name"]: raise Exception("Monkey template not configured") @@ -72,7 +72,7 @@ class VCenterConnector(NetControllerConnector): if not monkey_template: raise Exception("Monkey template not found") - task = self._clone_vm(vcontent, monkey_template) + task = self._clone_vm(vcontent, monkey_template, vm_name) if not task: raise Exception("Error deploying monkey VM") @@ -86,7 +86,7 @@ class VCenterConnector(NetControllerConnector): if self._service_instance: self.disconnect() - def _clone_vm(self, vcontent, vm): + def _clone_vm(self, vcontent, vm, name): # get vm target folder if self._properties["monkey_vm_info"]["vm_folder"]: @@ -116,7 +116,7 @@ class VCenterConnector(NetControllerConnector): clonespec = vim.vm.CloneSpec() clonespec.location = relospec - task = vm.Clone(folder=destfolder, name=self._properties["monkey_vm_info"]["name"], spec=clonespec) + task = vm.Clone(folder=destfolder, name=name, spec=clonespec) return self._wait_for_task(task) @@ -154,9 +154,11 @@ class VCenterConnector(NetControllerConnector): class VCenterJob(NetControllerJob): connector = VCenterConnector - - def __init__(self): - self._properties = { - "vlan": [0, "get_vlans_list"], - } + _properties = { + "vlan": 0, + "vm_name": "", + } + _enumerations = { + "vlan": "get_vlans_list", + } diff --git a/monkey_business/cc/dbconfig.py b/monkey_business/cc/dbconfig.py new file mode 100644 index 000000000..ec88943b6 --- /dev/null +++ b/monkey_business/cc/dbconfig.py @@ -0,0 +1,10 @@ +SRV_ADDRESS = 'localhost:27017' + +BROKER_URL = 'mongodb://%(srv)s/monkeybusiness' % {'srv': SRV_ADDRESS} +MONGO_URI = BROKER_URL +CELERY_RESULT_BACKEND = 'mongodb://%(srv)s/' % {'srv': SRV_ADDRESS} +CELERY_MONGODB_BACKEND_SETTINGS = { + 'database': 'monkeybusiness', + 'taskmeta_collection': 'celery_taskmeta', +} +#CELERYD_LOG_FILE="../celery.log" \ No newline at end of file diff --git a/monkey_business/cc/main.py b/monkey_business/cc/main.py index a7aa41d4b..a1982c1cd 100644 --- a/monkey_business/cc/main.py +++ b/monkey_business/cc/main.py @@ -10,13 +10,10 @@ from datetime import datetime import dateutil.parser from connectors.vcenter import VCenterJob, VCenterConnector from connectors.demo import DemoJob, DemoConnector - -MONGO_URL = os.environ.get('MONGO_URL') -if not MONGO_URL: - MONGO_URL = "mongodb://localhost:27017/monkeybusiness" +import tasks_manager app = Flask(__name__) -app.config['MONGO_URI'] = MONGO_URL +app.config.from_object('dbconfig') mongo = PyMongo(app) available_jobs = [VCenterJob, DemoJob] @@ -35,16 +32,14 @@ class Job(restful.Resource): def get(self, **kw): id = kw.get('id') timestamp = request.args.get('timestamp') + result = {} if (id): - return mongo.db.job.find_one_or_404({"id": id}) + return mongo.db.job.find_one_or_404({"_id": id}) else: - result = {'timestamp': datetime.now().isoformat()} + result['timestamp'] = datetime.now().isoformat() - find_filter = {} - if None != timestamp: - find_filter['modifytime'] = {'$gt': dateutil.parser.parse(timestamp)} - result['objects'] = [x for x in mongo.db.job.find(find_filter)] + result['objects'] = [x for x in mongo.db.job.find().sort("creation_time", -1)] return result def post(self, **kw): @@ -67,10 +62,11 @@ class Job(restful.Resource): {"$set": job_json}, upsert=True) + class Connector(restful.Resource): def get(self, **kw): type = request.args.get('type') - if (type == 'VCenterConnector'): + if type == 'VCenterConnector': vcenter = VCenterConnector() properties = mongo.db.connector.find_one({"type": 'VCenterConnector'}) if properties: @@ -82,7 +78,7 @@ class Connector(restful.Resource): def post(self, **kw): settings_json = json.loads(request.data) - if (settings_json.get("type") == 'VCenterConnector'): + if settings_json.get("type") == 'VCenterConnector': # preserve password properties = mongo.db.connector.find_one({"type": 'VCenterConnector'}) @@ -93,10 +89,19 @@ class Connector(restful.Resource): {"$set": settings_json}, upsert=True) + +def get_jobclass_by_name(name): + for jobclass in available_jobs: + if jobclass.__name__ == name: + return jobclass() + + class JobCreation(restful.Resource): def get(self, **kw): jobtype = request.args.get('type') - if not jobtype: + action = request.args.get('action') + jobid = request.args.get('id') + if not (jobtype or jobid): res = [] update_connectors() for con in available_jobs: @@ -105,24 +110,49 @@ class JobCreation(restful.Resource): return {"oneOf": res} job = None - for jobclass in available_jobs: - if jobclass.__name__ == jobtype: - job = jobclass() + if not jobid: + job = get_jobclass_by_name(jobtype) + else: + loaded_job = mongo.db.job.find_one({"_id": bson.ObjectId(jobid)}) + if loaded_job: + job = get_jobclass_by_name(loaded_job.get("type")) + job.load_job_properties(loaded_job.get("properties")) + + if action == "delete": + if loaded_job.get("execution")["state"] == "pending": + mongo.db.job.remove({"_id": bson.ObjectId(jobid)}) + return {'status': 'ok'} + else: + return {'status': 'bad state'} if job and job.connector.__name__ in active_connectors.keys(): - properties = dict() - job_prop = job.get_job_properties() + properties = { + "type": { + "type": "enum", + "enum": [job.__class__.__name__], + "options": {"hidden": True} + } + } + if (jobid): + properties["_id"] = { + "type": "enum", + "enum": [jobid], + "name": "ID", + } + job_prop = job.get_job_properties() for prop in job_prop: properties[prop] = dict({}) - if type(job_prop[prop][0]) is int: + properties[prop]["default"] = job_prop[prop] + if type(job_prop[prop]) is int: properties[prop]["type"] = "number" - elif type(job_prop[prop][0]) is bool: + elif type(job_prop[prop]) is bool: properties[prop]["type"] = "boolean" else: properties[prop]["type"] = "string" - if job_prop[prop][1]: - properties[prop]["enum"] = list(active_connectors[job.connector.__name__].__getattribute__(job_prop[prop][1])()) + enum = job.get_property_function(prop) + if enum: + properties[prop]["enum"] = list(active_connectors[job.connector.__name__].__getattribute__(enum)()) res = dict({ "title": "%s Job" % jobtype, @@ -137,6 +167,45 @@ class JobCreation(restful.Resource): return {} + def post(self, **kw): + settings_json = json.loads(request.data) + jobtype = settings_json.get("type") + jobid = settings_json.get("id") + job = None + for jobclass in available_jobs: + if jobclass.__name__ == jobtype: + job = jobclass() + if not job: + return {'status': 'bad type'} + + # params validation + job.load_job_properties(settings_json) + parsed_prop = job.get_job_properties() + if jobid: + res = mongo.db.job.update({"_id": bson.ObjectId(jobid)}, + {"$set": {"properties": parsed_prop}}) + if res and (res["ok"] == 1): + return {'status': 'ok', 'updated': res["nModified"]} + else: + return {'status': 'failed'} + + else: + execution_state = {"taskid": "", + "state" : "pending"} + new_job = { + "creation_time": datetime.now(), + "type": jobtype, + "properties": parsed_prop, + "execution": execution_state, + } + jobid = mongo.db.job.insert(new_job) + async = tasks_manager.run_task.delay(jobid) + execution_state["taskid"] = async.id + mongo.db.job.update({"_id": jobid}, + {"$set": {"execution": execution_state}}) + + return {'status': 'created'} + def normalize_obj(obj): if obj.has_key('_id') and not obj.has_key('id'): @@ -185,7 +254,9 @@ def update_connectors(): active_connectors.pop(connector_name) app.logger.info("Error activating connector: %s, reason: %s" % (connector_name, e)) - +@app.before_first_request +def init(): + update_connectors() @app.route('/admin/') def send_admin(path): diff --git a/monkey_business/cc/tasks_manager.py b/monkey_business/cc/tasks_manager.py new file mode 100644 index 000000000..953a8859d --- /dev/null +++ b/monkey_business/cc/tasks_manager.py @@ -0,0 +1,45 @@ +import os +import time +from flask import Flask +from flask.ext.pymongo import PyMongo +from celery import Celery + +def make_celery(app): + celery = Celery(main='MONKEY_TASKS', backend=app.config['CELERY_RESULT_BACKEND'], + broker=app.config['BROKER_URL']) + celery.conf.update(app.config) + TaskBase = celery.Task + class ContextTask(TaskBase): + abstract = True + def __call__(self, *args, **kwargs): + with app.app_context(): + return TaskBase.__call__(self, *args, **kwargs) + celery.Task = ContextTask + return celery + +fapp = Flask(__name__) +fapp.config.from_object('dbconfig') +celery = make_celery(fapp) +mongo = PyMongo(fapp) + +@celery.task +def run_task(jobid): + task_id = run_task.request.id + print "searching for ", jobid + job = mongo.db.job.find_one({"_id": jobid}) + if not job: + return False + job["execution"]["state"] = "processing" + mongo.db.job.update({"_id": jobid}, job) + + time.sleep(30) + + job["execution"]["state"] = "done" + mongo.db.job.update({"_id": jobid}, job) + return "task: " + task_id + + +@celery.task +def update_cache(connector): + time.sleep(30) + return "job: " + repr(job) diff --git a/monkey_business/readme.txt b/monkey_business/readme.txt new file mode 100644 index 000000000..6f456df9e --- /dev/null +++ b/monkey_business/readme.txt @@ -0,0 +1,4 @@ +dependencies: +sudo pip install pyVmomi +sudo pip install celery +sudo pip install -U celery[mongodb] \ No newline at end of file
TimeStatusData
IdTimeTypeStatusProperties