summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-api/opendc/api
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-web/opendc-web-api/opendc/api')
-rw-r--r--opendc-web/opendc-web-api/opendc/api/jobs.py105
-rw-r--r--opendc-web/opendc-web-api/opendc/api/portfolios.py16
-rw-r--r--opendc-web/opendc-web-api/opendc/api/prefabs.py19
-rw-r--r--opendc-web/opendc-web-api/opendc/api/projects.py28
-rw-r--r--opendc-web/opendc-web-api/opendc/api/scenarios.py15
-rw-r--r--opendc-web/opendc-web-api/opendc/api/topologies.py18
-rw-r--r--opendc-web/opendc-web-api/opendc/api/traces.py6
7 files changed, 166 insertions, 41 deletions
diff --git a/opendc-web/opendc-web-api/opendc/api/jobs.py b/opendc-web/opendc-web-api/opendc/api/jobs.py
new file mode 100644
index 00000000..6fb0522b
--- /dev/null
+++ b/opendc-web/opendc-web-api/opendc/api/jobs.py
@@ -0,0 +1,105 @@
+# Copyright (c) 2021 AtLarge Research
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+from flask import request
+from flask_restful import Resource
+from marshmallow import fields, Schema, validate
+from werkzeug.exceptions import BadRequest, Conflict
+
+from opendc.exts import requires_auth, requires_scope
+from opendc.models.scenario import Scenario
+
+
+def convert_to_job(scenario):
+ """Convert a scenario to a job.
+ """
+ return JobSchema().dump({
+ '_id': scenario['_id'],
+ 'scenarioId': scenario['_id'],
+ 'state': scenario['simulation']['state'],
+ 'heartbeat': scenario['simulation'].get('heartbeat', None),
+ 'results': scenario.get('results', {})
+ })
+
+
+class JobSchema(Schema):
+ """
+ Schema representing a simulation job.
+ """
+ _id = fields.String(dump_only=True)
+ scenarioId = fields.String(dump_only=True)
+ state = fields.String(required=True,
+ validate=validate.OneOf(["QUEUED", "CLAIMED", "RUNNING", "FINISHED", "FAILED"]))
+ heartbeat = fields.DateTime()
+ results = fields.Dict()
+
+
+class JobList(Resource):
+ """
+ Resource representing the list of available jobs.
+ """
+ method_decorators = [requires_auth, requires_scope('runner')]
+
+ def get(self):
+ """Get all available jobs."""
+ jobs = Scenario.get_jobs()
+ data = list(map(convert_to_job, jobs.obj))
+ return {'data': data}
+
+
+class Job(Resource):
+ """
+ Resource representing a single job.
+ """
+ method_decorators = [requires_auth, requires_scope('runner')]
+
+ def get(self, job_id):
+ """Get the details of a single job."""
+ job = Scenario.from_id(job_id)
+ job.check_exists()
+ data = convert_to_job(job.obj)
+ return {'data': data}
+
+ def post(self, job_id):
+ """Update the details of a single job."""
+ action = JobSchema(only=('state', 'results')).load(request.json)
+
+ job = Scenario.from_id(job_id)
+ job.check_exists()
+
+ old_state = job.obj['simulation']['state']
+ new_state = action['state']
+
+ if old_state == new_state:
+ data = job.update_state(new_state)
+ elif (old_state, new_state) == ('QUEUED', 'CLAIMED'):
+ data = job.update_state('CLAIMED')
+ elif (old_state, new_state) == ('CLAIMED', 'RUNNING'):
+ data = job.update_state('RUNNING')
+ elif (old_state, new_state) == ('RUNNING', 'FINISHED'):
+ data = job.update_state('FINISHED', results=action.get('results', None))
+ elif old_state in ('CLAIMED', 'RUNNING') and new_state == 'FAILED':
+ data = job.update_state('FAILED')
+ else:
+ raise BadRequest('Invalid state transition')
+
+ if not data:
+ raise Conflict('State conflict')
+
+ return {'data': convert_to_job(data)}
diff --git a/opendc-web/opendc-web-api/opendc/api/portfolios.py b/opendc-web/opendc-web-api/opendc/api/portfolios.py
index b07e9da5..eea82289 100644
--- a/opendc-web/opendc-web-api/opendc/api/portfolios.py
+++ b/opendc-web/opendc-web-api/opendc/api/portfolios.py
@@ -22,7 +22,7 @@ from flask import request
from flask_restful import Resource
from marshmallow import Schema, fields
-from opendc.exts import requires_auth, current_user
+from opendc.exts import requires_auth, current_user, has_scope
from opendc.models.portfolio import Portfolio as PortfolioModel, PortfolioSchema
from opendc.models.project import Project
from opendc.models.scenario import ScenarioSchema, Scenario
@@ -42,9 +42,12 @@ class Portfolio(Resource):
portfolio = PortfolioModel.from_id(portfolio_id)
portfolio.check_exists()
- portfolio.check_user_access(current_user['sub'], False)
- data = portfolio.obj
+ # Users with scope runner can access all portfolios
+ if not has_scope('runner'):
+ portfolio.check_user_access(current_user['sub'], False)
+
+ data = PortfolioSchema().dump(portfolio.obj)
return {'data': data}
def put(self, portfolio_id):
@@ -63,7 +66,7 @@ class Portfolio(Resource):
portfolio.set_property('targets.repeatsPerScenario', result['portfolio']['targets']['repeatsPerScenario'])
portfolio.update()
- data = portfolio.obj
+ data = PortfolioSchema().dump(portfolio.obj)
return {'data': data}
def delete(self, portfolio_id):
@@ -84,7 +87,8 @@ class Portfolio(Resource):
project.update()
old_object = portfolio.delete()
- return {'data': old_object}
+ data = PortfolioSchema().dump(old_object)
+ return {'data': data}
class PutSchema(Schema):
"""
@@ -125,7 +129,7 @@ class PortfolioScenarios(Resource):
portfolio.obj['scenarioIds'].append(scenario.get_id())
portfolio.update()
- data = scenario.obj
+ data = ScenarioSchema().dump(scenario.obj)
return {'data': data}
class PostSchema(Schema):
diff --git a/opendc-web/opendc-web-api/opendc/api/prefabs.py b/opendc-web/opendc-web-api/opendc/api/prefabs.py
index 7bb17e7d..730546ba 100644
--- a/opendc-web/opendc-web-api/opendc/api/prefabs.py
+++ b/opendc-web/opendc-web-api/opendc/api/prefabs.py
@@ -24,7 +24,6 @@ from flask_restful import Resource
from marshmallow import Schema, fields
from opendc.models.prefab import Prefab as PrefabModel, PrefabSchema
-from opendc.database import Database
from opendc.exts import current_user, requires_auth, db
@@ -56,14 +55,15 @@ class PrefabList(Resource):
result = schema.load(request.json)
prefab = PrefabModel(result['prefab'])
- prefab.set_property('datetimeCreated', Database.datetime_to_string(datetime.now()))
- prefab.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now()))
+ prefab.set_property('datetimeCreated', datetime.now())
+ prefab.set_property('datetimeLastEdited', datetime.now())
user_id = current_user['sub']
prefab.set_property('authorId', user_id)
prefab.insert()
- return {'data': prefab.obj}
+ data = PrefabSchema().dump(prefab.obj)
+ return {'data': data}
class PostSchema(Schema):
"""
@@ -83,7 +83,8 @@ class Prefab(Resource):
prefab = PrefabModel.from_id(prefab_id)
prefab.check_exists()
prefab.check_user_access(current_user['sub'])
- return {'data': prefab.obj}
+ data = PrefabSchema().dump(prefab.obj)
+ return {'data': data}
def put(self, prefab_id):
"""Update a prefab's name and/or contents."""
@@ -97,10 +98,11 @@ class Prefab(Resource):
prefab.set_property('name', result['prefab']['name'])
prefab.set_property('rack', result['prefab']['rack'])
- prefab.set_property('datetime_last_edited', Database.datetime_to_string(datetime.now()))
+ prefab.set_property('datetimeLastEdited', datetime.now())
prefab.update()
- return {'data': prefab.obj}
+ data = PrefabSchema().dump(prefab.obj)
+ return {'data': data}
def delete(self, prefab_id):
"""Delete this Prefab."""
@@ -111,7 +113,8 @@ class Prefab(Resource):
old_object = prefab.delete()
- return {'data': old_object}
+ data = PrefabSchema().dump(old_object)
+ return {'data': data}
class PutSchema(Schema):
"""
diff --git a/opendc-web/opendc-web-api/opendc/api/projects.py b/opendc-web/opendc-web-api/opendc/api/projects.py
index 8c44b680..05f02a84 100644
--- a/opendc-web/opendc-web-api/opendc/api/projects.py
+++ b/opendc-web/opendc-web-api/opendc/api/projects.py
@@ -27,7 +27,6 @@ from opendc.models.portfolio import Portfolio, PortfolioSchema
from opendc.models.topology import Topology, TopologySchema
from opendc.models.project import Project as ProjectModel, ProjectSchema
from opendc.exts import current_user, requires_auth
-from opendc.database import Database
class ProjectList(Resource):
@@ -40,7 +39,8 @@ class ProjectList(Resource):
"""Get the authorized projects of the user"""
user_id = current_user['sub']
projects = ProjectModel.get_for_user(user_id)
- return {'data': projects}
+ data = ProjectSchema().dump(projects, many=True)
+ return {'data': data}
def post(self):
"""Create a new project, and return that new project."""
@@ -53,8 +53,8 @@ class ProjectList(Resource):
topology.insert()
project = ProjectModel(result['project'])
- project.set_property('datetimeCreated', Database.datetime_to_string(datetime.now()))
- project.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now()))
+ project.set_property('datetimeCreated', datetime.now())
+ project.set_property('datetimeLastEdited', datetime.now())
project.set_property('topologyIds', [topology.get_id()])
project.set_property('portfolioIds', [])
project.set_property('authorizations', [{'userId': user_id, 'level': 'OWN'}])
@@ -63,7 +63,8 @@ class ProjectList(Resource):
topology.set_property('projectId', project.get_id())
topology.update()
- return {'data': project.obj}
+ data = ProjectSchema().dump(project.obj)
+ return {'data': data}
class Project(Resource):
@@ -79,7 +80,8 @@ class Project(Resource):
project.check_exists()
project.check_user_access(current_user['sub'], False)
- return {'data': project.obj}
+ data = ProjectSchema().dump(project.obj)
+ return {'data': data}
def put(self, project_id):
"""Update a project's name."""
@@ -92,10 +94,11 @@ class Project(Resource):
project.check_user_access(current_user['sub'], True)
project.set_property('name', result['project']['name'])
- project.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now()))
+ project.set_property('datetimeLastEdited', datetime.now())
project.update()
- return {'data': project.obj}
+ data = ProjectSchema().dump(project.obj)
+ return {'data': data}
def delete(self, project_id):
"""Delete this Project."""
@@ -113,8 +116,8 @@ class Project(Resource):
portfolio.delete()
old_object = project.delete()
-
- return {'data': old_object}
+ data = ProjectSchema().dump(old_object)
+ return {'data': data}
class PutSchema(Schema):
"""
@@ -148,10 +151,11 @@ class ProjectTopologies(Resource):
topology.insert()
project.obj['topologyIds'].append(topology.get_id())
- project.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now()))
+ project.set_property('datetimeLastEdited', datetime.now())
project.update()
- return {'data': topology.obj}
+ data = TopologySchema().dump(topology.obj)
+ return {'data': data}
class PutSchema(Schema):
"""
diff --git a/opendc-web/opendc-web-api/opendc/api/scenarios.py b/opendc-web/opendc-web-api/opendc/api/scenarios.py
index b566950a..eacb0b49 100644
--- a/opendc-web/opendc-web-api/opendc/api/scenarios.py
+++ b/opendc-web/opendc-web-api/opendc/api/scenarios.py
@@ -24,7 +24,7 @@ from marshmallow import Schema, fields
from opendc.models.scenario import Scenario as ScenarioModel, ScenarioSchema
from opendc.models.portfolio import Portfolio
-from opendc.exts import current_user, requires_auth
+from opendc.exts import current_user, requires_auth, has_scope
class Scenario(Resource):
@@ -37,8 +37,12 @@ class Scenario(Resource):
"""Get scenario by identifier."""
scenario = ScenarioModel.from_id(scenario_id)
scenario.check_exists()
- scenario.check_user_access(current_user['sub'], False)
- data = scenario.obj
+
+ # Users with scope runner can access all scenarios
+ if not has_scope('runner'):
+ scenario.check_user_access(current_user['sub'], False)
+
+ data = ScenarioSchema().dump(scenario.obj)
return {'data': data}
def put(self, scenario_id):
@@ -54,7 +58,7 @@ class Scenario(Resource):
scenario.set_property('name', result['scenario']['name'])
scenario.update()
- data = scenario.obj
+ data = ScenarioSchema().dump(scenario.obj)
return {'data': data}
def delete(self, scenario_id):
@@ -72,7 +76,8 @@ class Scenario(Resource):
portfolio.update()
old_object = scenario.delete()
- return {'data': old_object}
+ data = ScenarioSchema().dump(old_object)
+ return {'data': data}
class PutSchema(Schema):
"""
diff --git a/opendc-web/opendc-web-api/opendc/api/topologies.py b/opendc-web/opendc-web-api/opendc/api/topologies.py
index eedf049d..c0b2e7ee 100644
--- a/opendc-web/opendc-web-api/opendc/api/topologies.py
+++ b/opendc-web/opendc-web-api/opendc/api/topologies.py
@@ -24,10 +24,9 @@ from flask import request
from flask_restful import Resource
from marshmallow import Schema, fields
-from opendc.database import Database
from opendc.models.project import Project
from opendc.models.topology import Topology as TopologyModel, TopologySchema
-from opendc.exts import current_user, requires_auth
+from opendc.exts import current_user, requires_auth, has_scope
class Topology(Resource):
@@ -42,8 +41,12 @@ class Topology(Resource):
"""
topology = TopologyModel.from_id(topology_id)
topology.check_exists()
- topology.check_user_access(current_user['sub'], False)
- data = topology.obj
+
+ # Users with scope runner can access all topologies
+ if not has_scope('runner'):
+ topology.check_user_access(current_user['sub'], False)
+
+ data = TopologySchema().dump(topology.obj)
return {'data': data}
def put(self, topology_id):
@@ -60,10 +63,10 @@ class Topology(Resource):
topology.set_property('name', result['topology']['name'])
topology.set_property('rooms', result['topology']['rooms'])
- topology.set_property('datetimeLastEdited', Database.datetime_to_string(datetime.now()))
+ topology.set_property('datetimeLastEdited', datetime.now())
topology.update()
- data = topology.obj
+ data = TopologySchema().dump(topology.obj)
return {'data': data}
def delete(self, topology_id):
@@ -84,7 +87,8 @@ class Topology(Resource):
project.update()
old_object = topology.delete()
- return {'data': old_object}
+ data = TopologySchema().dump(old_object)
+ return {'data': data}
class PutSchema(Schema):
"""
diff --git a/opendc-web/opendc-web-api/opendc/api/traces.py b/opendc-web/opendc-web-api/opendc/api/traces.py
index f685f00c..6be8c5e5 100644
--- a/opendc-web/opendc-web-api/opendc/api/traces.py
+++ b/opendc-web/opendc-web-api/opendc/api/traces.py
@@ -21,7 +21,7 @@
from flask_restful import Resource
from opendc.exts import requires_auth
-from opendc.models.trace import Trace as TraceModel
+from opendc.models.trace import Trace as TraceModel, TraceSchema
class TraceList(Resource):
@@ -33,7 +33,7 @@ class TraceList(Resource):
def get(self):
"""Get all available Traces."""
traces = TraceModel.get_all()
- data = traces.obj
+ data = TraceSchema().dump(traces.obj, many=True)
return {'data': data}
@@ -47,5 +47,5 @@ class Trace(Resource):
"""Get trace information by identifier."""
trace = TraceModel.from_id(trace_id)
trace.check_exists()
- data = trace.obj
+ data = TraceSchema().dump(trace.obj)
return {'data': data}