summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-api/opendc
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-web/opendc-web-api/opendc')
-rw-r--r--opendc-web/opendc-web-api/opendc/__init__.py0
-rw-r--r--opendc-web/opendc-web-api/opendc/api/__init__.py0
-rw-r--r--opendc-web/opendc-web-api/opendc/api/jobs.py105
-rw-r--r--opendc-web/opendc-web-api/opendc/api/portfolios.py153
-rw-r--r--opendc-web/opendc-web-api/opendc/api/prefabs.py123
-rw-r--r--opendc-web/opendc-web-api/opendc/api/projects.py224
-rw-r--r--opendc-web/opendc-web-api/opendc/api/scenarios.py86
-rw-r--r--opendc-web/opendc-web-api/opendc/api/schedulers.py46
-rw-r--r--opendc-web/opendc-web-api/opendc/api/topologies.py97
-rw-r--r--opendc-web/opendc-web-api/opendc/api/traces.py51
-rw-r--r--opendc-web/opendc-web-api/opendc/auth.py236
-rw-r--r--opendc-web/opendc-web-api/opendc/database.py97
-rw-r--r--opendc-web/opendc-web-api/opendc/exts.py91
-rw-r--r--opendc-web/opendc-web-api/opendc/models/__init__.py0
-rw-r--r--opendc-web/opendc-web-api/opendc/models/model.py61
-rw-r--r--opendc-web/opendc-web-api/opendc/models/portfolio.py47
-rw-r--r--opendc-web/opendc-web-api/opendc/models/prefab.py31
-rw-r--r--opendc-web/opendc-web-api/opendc/models/project.py48
-rw-r--r--opendc-web/opendc-web-api/opendc/models/scenario.py93
-rw-r--r--opendc-web/opendc-web-api/opendc/models/topology.py108
-rw-r--r--opendc-web/opendc-web-api/opendc/models/trace.py16
-rw-r--r--opendc-web/opendc-web-api/opendc/util.py32
22 files changed, 0 insertions, 1745 deletions
diff --git a/opendc-web/opendc-web-api/opendc/__init__.py b/opendc-web/opendc-web-api/opendc/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/opendc-web/opendc-web-api/opendc/__init__.py
+++ /dev/null
diff --git a/opendc-web/opendc-web-api/opendc/api/__init__.py b/opendc-web/opendc-web-api/opendc/api/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/opendc-web/opendc-web-api/opendc/api/__init__.py
+++ /dev/null
diff --git a/opendc-web/opendc-web-api/opendc/api/jobs.py b/opendc-web/opendc-web-api/opendc/api/jobs.py
deleted file mode 100644
index 6fb0522b..00000000
--- a/opendc-web/opendc-web-api/opendc/api/jobs.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# 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
deleted file mode 100644
index 4d8f54fd..00000000
--- a/opendc-web/opendc-web-api/opendc/api/portfolios.py
+++ /dev/null
@@ -1,153 +0,0 @@
-# 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 Schema, fields
-
-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
-from opendc.models.topology import Topology
-
-
-class Portfolio(Resource):
- """
- Resource representing a portfolio.
- """
- method_decorators = [requires_auth]
-
- def get(self, portfolio_id):
- """
- Get a portfolio by identifier.
- """
- portfolio = PortfolioModel.from_id(portfolio_id)
-
- portfolio.check_exists()
-
- # 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):
- """
- Replace the portfolio.
- """
- schema = Portfolio.PutSchema()
- result = schema.load(request.json)
-
- portfolio = PortfolioModel.from_id(portfolio_id)
- portfolio.check_exists()
- portfolio.check_user_access(current_user['sub'], True)
-
- portfolio.set_property('name', result['portfolio']['name'])
- portfolio.set_property('targets.enabledMetrics', result['portfolio']['targets']['enabledMetrics'])
- portfolio.set_property('targets.repeatsPerScenario', result['portfolio']['targets']['repeatsPerScenario'])
-
- portfolio.update()
- data = PortfolioSchema().dump(portfolio.obj)
- return {'data': data}
-
- def delete(self, portfolio_id):
- """
- Delete a portfolio.
- """
- portfolio = PortfolioModel.from_id(portfolio_id)
-
- portfolio.check_exists()
- portfolio.check_user_access(current_user['sub'], True)
-
- portfolio_id = portfolio.get_id()
-
- project = Project.from_id(portfolio.obj['projectId'])
- project.check_exists()
- if portfolio_id in project.obj['portfolioIds']:
- project.obj['portfolioIds'].remove(portfolio_id)
- project.update()
-
- old_object = portfolio.delete()
- data = PortfolioSchema().dump(old_object)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a portfolio.
- """
- portfolio = fields.Nested(PortfolioSchema, required=True)
-
-
-class PortfolioScenarios(Resource):
- """
- Resource representing the scenarios of a portfolio.
- """
- method_decorators = [requires_auth]
-
- def get(self, portfolio_id):
- """
- Get all scenarios belonging to a portfolio.
- """
- portfolio = PortfolioModel.from_id(portfolio_id)
-
- portfolio.check_exists()
- portfolio.check_user_access(current_user['sub'], True)
-
- scenarios = Scenario.get_for_portfolio(portfolio_id)
-
- data = ScenarioSchema().dump(scenarios, many=True)
- return {'data': data}
-
- def post(self, portfolio_id):
- """
- Add a new scenario to this portfolio
- """
- schema = PortfolioScenarios.PostSchema()
- result = schema.load(request.json)
-
- portfolio = PortfolioModel.from_id(portfolio_id)
-
- portfolio.check_exists()
- portfolio.check_user_access(current_user['sub'], True)
-
- scenario = Scenario(result['scenario'])
-
- topology = Topology.from_id(scenario.obj['topology']['topologyId'])
- topology.check_exists()
- topology.check_user_access(current_user['sub'], True)
-
- scenario.set_property('portfolioId', portfolio.get_id())
- scenario.set_property('simulation', {'state': 'QUEUED'})
- scenario.set_property('topology.topologyId', topology.get_id())
-
- scenario.insert()
-
- portfolio.obj['scenarioIds'].append(scenario.get_id())
- portfolio.update()
- data = ScenarioSchema().dump(scenario.obj)
- return {'data': data}
-
- class PostSchema(Schema):
- """
- Schema for the POST operation on a portfolio's scenarios.
- """
- scenario = fields.Nested(ScenarioSchema, required=True)
diff --git a/opendc-web/opendc-web-api/opendc/api/prefabs.py b/opendc-web/opendc-web-api/opendc/api/prefabs.py
deleted file mode 100644
index 730546ba..00000000
--- a/opendc-web/opendc-web-api/opendc/api/prefabs.py
+++ /dev/null
@@ -1,123 +0,0 @@
-# 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 datetime import datetime
-from flask import request
-from flask_restful import Resource
-from marshmallow import Schema, fields
-
-from opendc.models.prefab import Prefab as PrefabModel, PrefabSchema
-from opendc.exts import current_user, requires_auth, db
-
-
-class PrefabList(Resource):
- """
- Resource for the list of prefabs available to the user.
- """
- method_decorators = [requires_auth]
-
- def get(self):
- """
- Get the available prefabs for a user.
- """
- user_id = current_user['sub']
-
- own_prefabs = db.fetch_all({'authorId': user_id}, PrefabModel.collection_name)
- public_prefabs = db.fetch_all({'visibility': 'public'}, PrefabModel.collection_name)
-
- authorizations = {"authorizations": []}
- authorizations["authorizations"].append(own_prefabs)
- authorizations["authorizations"].append(public_prefabs)
- return {'data': authorizations}
-
- def post(self):
- """
- Create a new prefab.
- """
- schema = PrefabList.PostSchema()
- result = schema.load(request.json)
-
- prefab = PrefabModel(result['prefab'])
- 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()
- data = PrefabSchema().dump(prefab.obj)
- return {'data': data}
-
- class PostSchema(Schema):
- """
- Schema for the POST operation on the prefab list.
- """
- prefab = fields.Nested(PrefabSchema, required=True)
-
-
-class Prefab(Resource):
- """
- Resource representing a single prefab.
- """
- method_decorators = [requires_auth]
-
- def get(self, prefab_id):
- """Get this Prefab."""
- prefab = PrefabModel.from_id(prefab_id)
- prefab.check_exists()
- prefab.check_user_access(current_user['sub'])
- data = PrefabSchema().dump(prefab.obj)
- return {'data': data}
-
- def put(self, prefab_id):
- """Update a prefab's name and/or contents."""
-
- schema = Prefab.PutSchema()
- result = schema.load(request.json)
-
- prefab = PrefabModel.from_id(prefab_id)
- prefab.check_exists()
- prefab.check_user_access(current_user['sub'])
-
- prefab.set_property('name', result['prefab']['name'])
- prefab.set_property('rack', result['prefab']['rack'])
- prefab.set_property('datetimeLastEdited', datetime.now())
- prefab.update()
-
- data = PrefabSchema().dump(prefab.obj)
- return {'data': data}
-
- def delete(self, prefab_id):
- """Delete this Prefab."""
- prefab = PrefabModel.from_id(prefab_id)
-
- prefab.check_exists()
- prefab.check_user_access(current_user['sub'])
-
- old_object = prefab.delete()
-
- data = PrefabSchema().dump(old_object)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a prefab.
- """
- prefab = fields.Nested(PrefabSchema, required=True)
diff --git a/opendc-web/opendc-web-api/opendc/api/projects.py b/opendc-web/opendc-web-api/opendc/api/projects.py
deleted file mode 100644
index 2b47c12e..00000000
--- a/opendc-web/opendc-web-api/opendc/api/projects.py
+++ /dev/null
@@ -1,224 +0,0 @@
-# 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 datetime import datetime
-from flask import request
-from flask_restful import Resource
-from marshmallow import Schema, fields
-
-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
-
-
-class ProjectList(Resource):
- """
- Resource representing the list of projects available to a user.
- """
- method_decorators = [requires_auth]
-
- def get(self):
- """Get the authorized projects of the user"""
- user_id = current_user['sub']
- projects = ProjectModel.get_for_user(user_id)
- data = ProjectSchema().dump(projects, many=True)
- return {'data': data}
-
- def post(self):
- """Create a new project, and return that new project."""
- user_id = current_user['sub']
-
- schema = Project.PutSchema()
- result = schema.load(request.json)
-
- topology = Topology({'name': 'Default topology', 'rooms': []})
- topology.insert()
-
- project = ProjectModel(result['project'])
- 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'}])
- project.insert()
-
- topology.set_property('projectId', project.get_id())
- topology.update()
-
- data = ProjectSchema().dump(project.obj)
- return {'data': data}
-
-
-class Project(Resource):
- """
- Resource representing a single project.
- """
- method_decorators = [requires_auth]
-
- def get(self, project_id):
- """Get this Project."""
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], False)
-
- data = ProjectSchema().dump(project.obj)
- return {'data': data}
-
- def put(self, project_id):
- """Update a project's name."""
- schema = Project.PutSchema()
- result = schema.load(request.json)
-
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], True)
-
- project.set_property('name', result['project']['name'])
- project.set_property('datetimeLastEdited', datetime.now())
- project.update()
-
- data = ProjectSchema().dump(project.obj)
- return {'data': data}
-
- def delete(self, project_id):
- """Delete this Project."""
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], True)
-
- for topology_id in project.obj['topologyIds']:
- topology = Topology.from_id(topology_id)
- topology.delete()
-
- for portfolio_id in project.obj['portfolioIds']:
- portfolio = Portfolio.from_id(portfolio_id)
- portfolio.delete()
-
- old_object = project.delete()
- data = ProjectSchema().dump(old_object)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a project.
- """
- project = fields.Nested(ProjectSchema, required=True)
-
-
-class ProjectTopologies(Resource):
- """
- Resource representing the topologies of a project.
- """
- method_decorators = [requires_auth]
-
- def get(self, project_id):
- """Get all topologies belonging to the project."""
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], True)
-
- topologies = Topology.get_for_project(project_id)
- data = TopologySchema().dump(topologies, many=True)
-
- return {'data': data}
-
- def post(self, project_id):
- """Add a new Topology to the specified project and return it"""
- schema = ProjectTopologies.PutSchema()
- result = schema.load(request.json)
-
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], True)
-
- topology = Topology({
- 'projectId': project.get_id(),
- 'name': result['topology']['name'],
- 'rooms': result['topology']['rooms'],
- })
-
- topology.insert()
-
- project.obj['topologyIds'].append(topology.get_id())
- project.set_property('datetimeLastEdited', datetime.now())
- project.update()
-
- data = TopologySchema().dump(topology.obj)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a project topology.
- """
- topology = fields.Nested(TopologySchema, required=True)
-
-
-class ProjectPortfolios(Resource):
- """
- Resource representing the portfolios of a project.
- """
- method_decorators = [requires_auth]
-
- def get(self, project_id):
- """Get all portfolios belonging to the project."""
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], True)
-
- portfolios = Portfolio.get_for_project(project_id)
- data = PortfolioSchema().dump(portfolios, many=True)
-
- return {'data': data}
-
- def post(self, project_id):
- """Add a new Portfolio for this Project."""
- schema = ProjectPortfolios.PutSchema()
- result = schema.load(request.json)
-
- project = ProjectModel.from_id(project_id)
-
- project.check_exists()
- project.check_user_access(current_user['sub'], True)
-
- portfolio = Portfolio(result['portfolio'])
-
- portfolio.set_property('projectId', project.get_id())
- portfolio.set_property('scenarioIds', [])
-
- portfolio.insert()
-
- project.obj['portfolioIds'].append(portfolio.get_id())
- project.update()
-
- data = PortfolioSchema().dump(portfolio.obj)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a project portfolio.
- """
- portfolio = fields.Nested(PortfolioSchema, required=True)
diff --git a/opendc-web/opendc-web-api/opendc/api/scenarios.py b/opendc-web/opendc-web-api/opendc/api/scenarios.py
deleted file mode 100644
index eacb0b49..00000000
--- a/opendc-web/opendc-web-api/opendc/api/scenarios.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# 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 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, has_scope
-
-
-class Scenario(Resource):
- """
- A Scenario resource.
- """
- method_decorators = [requires_auth]
-
- def get(self, scenario_id):
- """Get scenario by identifier."""
- scenario = ScenarioModel.from_id(scenario_id)
- scenario.check_exists()
-
- # 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):
- """Update this Scenarios name."""
- schema = Scenario.PutSchema()
- result = schema.load(request.json)
-
- scenario = ScenarioModel.from_id(scenario_id)
-
- scenario.check_exists()
- scenario.check_user_access(current_user['sub'], True)
-
- scenario.set_property('name', result['scenario']['name'])
-
- scenario.update()
- data = ScenarioSchema().dump(scenario.obj)
- return {'data': data}
-
- def delete(self, scenario_id):
- """Delete this Scenario."""
- scenario = ScenarioModel.from_id(scenario_id)
- scenario.check_exists()
- scenario.check_user_access(current_user['sub'], True)
-
- scenario_id = scenario.get_id()
-
- portfolio = Portfolio.from_id(scenario.obj['portfolioId'])
- portfolio.check_exists()
- if scenario_id in portfolio.obj['scenarioIds']:
- portfolio.obj['scenarioIds'].remove(scenario_id)
- portfolio.update()
-
- old_object = scenario.delete()
- data = ScenarioSchema().dump(old_object)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the put operation.
- """
- scenario = fields.Nested(ScenarioSchema, required=True)
diff --git a/opendc-web/opendc-web-api/opendc/api/schedulers.py b/opendc-web/opendc-web-api/opendc/api/schedulers.py
deleted file mode 100644
index b00d8c31..00000000
--- a/opendc-web/opendc-web-api/opendc/api/schedulers.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# 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_restful import Resource
-from opendc.exts import requires_auth
-
-SCHEDULERS = [
- 'mem',
- 'mem-inv',
- 'core-mem',
- 'core-mem-inv',
- 'active-servers',
- 'active-servers-inv',
- 'provisioned-cores',
- 'provisioned-cores-inv',
- 'random'
-]
-
-
-class SchedulerList(Resource):
- """
- Resource for the list of schedulers to pick from.
- """
- method_decorators = [requires_auth]
-
- def get(self):
- """Get all available Traces."""
- return {'data': [{'name': name} for name in SCHEDULERS]}
diff --git a/opendc-web/opendc-web-api/opendc/api/topologies.py b/opendc-web/opendc-web-api/opendc/api/topologies.py
deleted file mode 100644
index c0b2e7ee..00000000
--- a/opendc-web/opendc-web-api/opendc/api/topologies.py
+++ /dev/null
@@ -1,97 +0,0 @@
-# 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 datetime import datetime
-
-from flask import request
-from flask_restful import Resource
-from marshmallow import Schema, fields
-
-from opendc.models.project import Project
-from opendc.models.topology import Topology as TopologyModel, TopologySchema
-from opendc.exts import current_user, requires_auth, has_scope
-
-
-class Topology(Resource):
- """
- Resource representing a single topology.
- """
- method_decorators = [requires_auth]
-
- def get(self, topology_id):
- """
- Get a single topology.
- """
- topology = TopologyModel.from_id(topology_id)
- topology.check_exists()
-
- # 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):
- """
- Replace the topology.
- """
- topology = TopologyModel.from_id(topology_id)
-
- schema = Topology.PutSchema()
- result = schema.load(request.json)
-
- topology.check_exists()
- topology.check_user_access(current_user['sub'], True)
-
- topology.set_property('name', result['topology']['name'])
- topology.set_property('rooms', result['topology']['rooms'])
- topology.set_property('datetimeLastEdited', datetime.now())
-
- topology.update()
- data = TopologySchema().dump(topology.obj)
- return {'data': data}
-
- def delete(self, topology_id):
- """
- Delete a topology.
- """
- topology = TopologyModel.from_id(topology_id)
-
- topology.check_exists()
- topology.check_user_access(current_user['sub'], True)
-
- topology_id = topology.get_id()
-
- project = Project.from_id(topology.obj['projectId'])
- project.check_exists()
- if topology_id in project.obj['topologyIds']:
- project.obj['topologyIds'].remove(topology_id)
- project.update()
-
- old_object = topology.delete()
- data = TopologySchema().dump(old_object)
- return {'data': data}
-
- class PutSchema(Schema):
- """
- Schema for the PUT operation on a topology.
- """
- topology = fields.Nested(TopologySchema, required=True)
diff --git a/opendc-web/opendc-web-api/opendc/api/traces.py b/opendc-web/opendc-web-api/opendc/api/traces.py
deleted file mode 100644
index 6be8c5e5..00000000
--- a/opendc-web/opendc-web-api/opendc/api/traces.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# 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_restful import Resource
-
-from opendc.exts import requires_auth
-from opendc.models.trace import Trace as TraceModel, TraceSchema
-
-
-class TraceList(Resource):
- """
- Resource for the list of traces to pick from.
- """
- method_decorators = [requires_auth]
-
- def get(self):
- """Get all available Traces."""
- traces = TraceModel.get_all()
- data = TraceSchema().dump(traces.obj, many=True)
- return {'data': data}
-
-
-class Trace(Resource):
- """
- Resource representing a single trace.
- """
- method_decorators = [requires_auth]
-
- def get(self, trace_id):
- """Get trace information by identifier."""
- trace = TraceModel.from_id(trace_id)
- trace.check_exists()
- data = TraceSchema().dump(trace.obj)
- return {'data': data}
diff --git a/opendc-web/opendc-web-api/opendc/auth.py b/opendc-web/opendc-web-api/opendc/auth.py
deleted file mode 100644
index d5da6ee5..00000000
--- a/opendc-web/opendc-web-api/opendc/auth.py
+++ /dev/null
@@ -1,236 +0,0 @@
-# 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.
-
-import json
-import time
-
-import urllib3
-from flask import request
-from jose import jwt, JWTError
-
-
-def get_token():
- """
- Obtain the Access Token from the Authorization Header
- """
- auth = request.headers.get("Authorization", None)
- if not auth:
- raise AuthError({
- "code": "authorization_header_missing",
- "description": "Authorization header is expected"
- }, 401)
-
- parts = auth.split()
-
- if parts[0].lower() != "bearer":
- raise AuthError({"code": "invalid_header", "description": "Authorization header must start with Bearer"}, 401)
- if len(parts) == 1:
- raise AuthError({"code": "invalid_header", "description": "Token not found"}, 401)
- if len(parts) > 2:
- raise AuthError({"code": "invalid_header", "description": "Authorization header must be" " Bearer token"}, 401)
-
- token = parts[1]
- return token
-
-
-class AuthError(Exception):
- """
- This error is thrown when the request failed to authorize.
- """
- def __init__(self, error, status_code):
- Exception.__init__(self, error)
- self.error = error
- self.status_code = status_code
-
-
-class AuthContext:
- """
- This class handles the authorization of requests.
- """
- def __init__(self, alg, issuer, audience):
- self._alg = alg
- self._issuer = issuer
- self._audience = audience
-
- def validate(self, token):
- """
- Validate the specified JWT token.
- :param token: The authorization token specified by the user.
- :return: The token payload on success, otherwise `AuthError`.
- """
- try:
- header = jwt.get_unverified_header(token)
- except JWTError as e:
- raise AuthError({"code": "invalid_token", "message": str(e)}, 401)
-
- alg = header.get('alg', None)
- if alg != self._alg.algorithm:
- raise AuthError(
- {
- "code":
- "invalid_header",
- "message":
- f"Signature algorithm of {alg} is not supported. Expected the ID token "
- f"to be signed with {self._alg.algorithm}"
- }, 401)
-
- kid = header.get('kid', None)
- try:
- secret_or_certificate = self._alg.get_key(key_id=kid)
- except TokenValidationError as e:
- raise AuthError({"code": "invalid_header", "message": str(e)}, 401)
- try:
- payload = jwt.decode(token,
- key=secret_or_certificate,
- algorithms=[self._alg.algorithm],
- audience=self._audience,
- issuer=self._issuer)
- return payload
- except jwt.ExpiredSignatureError:
- raise AuthError({"code": "token_expired", "message": "Token is expired"}, 401)
- except jwt.JWTClaimsError:
- raise AuthError(
- {
- "code": "invalid_claims",
- "message": "Incorrect claims, please check the audience and issuer"
- }, 401)
- except Exception as e:
- print(e)
- raise AuthError({"code": "invalid_header", "message": "Unable to parse authentication token."}, 401)
-
-
-class SymmetricJwtAlgorithm:
- """Verifier for HMAC signatures, which rely on shared secrets.
- Args:
- shared_secret (str): The shared secret used to decode the token.
- algorithm (str, optional): The expected signing algorithm. Defaults to "HS256".
- """
- def __init__(self, shared_secret, algorithm="HS256"):
- self.algorithm = algorithm
- self._shared_secret = shared_secret
-
- # pylint: disable=W0613
- def get_key(self, key_id=None):
- """
- Obtain the key for this algorithm.
- :param key_id: The identifier of the key.
- :return: The JWK key.
- """
- return self._shared_secret
-
-
-class AsymmetricJwtAlgorithm:
- """Verifier for RSA signatures, which rely on public key certificates.
- Args:
- jwks_url (str): The url where the JWK set is located.
- algorithm (str, optional): The expected signing algorithm. Defaults to "RS256".
- """
- def __init__(self, jwks_url, algorithm="RS256"):
- self.algorithm = algorithm
- self._fetcher = JwksFetcher(jwks_url)
-
- def get_key(self, key_id=None):
- """
- Obtain the key for this algorithm.
- :param key_id: The identifier of the key.
- :return: The JWK key.
- """
- return self._fetcher.get_key(key_id)
-
-
-class TokenValidationError(Exception):
- """
- Error thrown when the token cannot be validated
- """
-
-
-class JwksFetcher:
- """Class that fetches and holds a JSON web key set.
- This class makes use of an in-memory cache. For it to work properly, define this instance once and re-use it.
- Args:
- jwks_url (str): The url where the JWK set is located.
- cache_ttl (str, optional): The lifetime of the JWK set cache in seconds. Defaults to 600 seconds.
- """
- CACHE_TTL = 600 # 10 min cache lifetime
-
- def __init__(self, jwks_url, cache_ttl=CACHE_TTL):
- self._jwks_url = jwks_url
- self._http = urllib3.PoolManager()
- self._cache_value = {}
- self._cache_date = 0
- self._cache_ttl = cache_ttl
- self._cache_is_fresh = False
-
- def _fetch_jwks(self, force=False):
- """Attempts to obtain the JWK set from the cache, as long as it's still valid.
- When not, it will perform a network request to the jwks_url to obtain a fresh result
- and update the cache value with it.
- Args:
- force (bool, optional): whether to ignore the cache and force a network request or not. Defaults to False.
- """
- has_expired = self._cache_date + self._cache_ttl < time.time()
-
- if not force and not has_expired:
- # Return from cache
- self._cache_is_fresh = False
- return self._cache_value
-
- # Invalidate cache and fetch fresh data
- self._cache_value = {}
- response = self._http.request('GET', self._jwks_url)
-
- if response.status == 200:
- # Update cache
- jwks = json.loads(response.data.decode('utf-8'))
- self._cache_value = self._parse_jwks(jwks)
- self._cache_is_fresh = True
- self._cache_date = time.time()
- return self._cache_value
-
- @staticmethod
- def _parse_jwks(jwks):
- """Converts a JWK string representation into a binary certificate in PEM format.
- """
- keys = {}
-
- for key in jwks['keys']:
- keys[key["kid"]] = key
- return keys
-
- def get_key(self, key_id):
- """Obtains the JWK associated with the given key id.
- Args:
- key_id (str): The id of the key to fetch.
- Returns:
- the JWK associated with the given key id.
-
- Raises:
- TokenValidationError: when a key with that id cannot be found
- """
- keys = self._fetch_jwks()
-
- if keys and key_id in keys:
- return keys[key_id]
-
- if not self._cache_is_fresh:
- keys = self._fetch_jwks(force=True)
- if keys and key_id in keys:
- return keys[key_id]
- raise TokenValidationError(f"RSA Public Key with ID {key_id} was not found.")
diff --git a/opendc-web/opendc-web-api/opendc/database.py b/opendc-web/opendc-web-api/opendc/database.py
deleted file mode 100644
index dd6367f2..00000000
--- a/opendc-web/opendc-web-api/opendc/database.py
+++ /dev/null
@@ -1,97 +0,0 @@
-# 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.
-
-import urllib.parse
-
-from pymongo import MongoClient, ReturnDocument
-
-DATETIME_STRING_FORMAT = '%Y-%m-%dT%H:%M:%S'
-CONNECTION_POOL = None
-
-
-class Database:
- """Object holding functionality for database access."""
- def __init__(self, db=None):
- """Initializes the database connection."""
- self.opendc_db = db
-
- @classmethod
- def from_credentials(cls, user, password, database, host):
- """
- Construct a database instance from the specified credentials.
- :param user: The username to connect with.
- :param password: The password to connect with.
- :param database: The database name to connect to.
- :param host: The host to connect to.
- :return: The database instance.
- """
- user = urllib.parse.quote_plus(user)
- password = urllib.parse.quote_plus(password)
- database = urllib.parse.quote_plus(database)
- host = urllib.parse.quote_plus(host)
-
- client = MongoClient('mongodb://%s:%s@%s/default_db?authSource=%s' % (user, password, host, database))
- return cls(client.opendc)
-
- def fetch_one(self, query, collection):
- """Uses existing mongo connection to return a single (the first) document in a collection matching the given
- query as a JSON object.
-
- The query needs to be in json format, i.e.: `{'name': prefab_name}`.
- """
- return getattr(self.opendc_db, collection).find_one(query)
-
- def fetch_all(self, query, collection):
- """Uses existing mongo connection to return all documents matching a given query, as a list of JSON objects.
-
- The query needs to be in json format, i.e.: `{'name': prefab_name}`.
- """
- cursor = getattr(self.opendc_db, collection).find(query)
- return list(cursor)
-
- def insert(self, obj, collection):
- """Updates an existing object."""
- bson = getattr(self.opendc_db, collection).insert(obj)
-
- return bson
-
- def update(self, _id, obj, collection):
- """Updates an existing object."""
- return getattr(self.opendc_db, collection).update({'_id': _id}, obj)
-
- def fetch_and_update(self, query, update, collection):
- """Updates an existing object."""
- return getattr(self.opendc_db, collection).find_one_and_update(query,
- update,
- return_document=ReturnDocument.AFTER)
-
- def delete_one(self, query, collection):
- """Deletes one object matching the given query.
-
- The query needs to be in json format, i.e.: `{'name': prefab_name}`.
- """
- getattr(self.opendc_db, collection).delete_one(query)
-
- def delete_all(self, query, collection):
- """Deletes all objects matching the given query.
-
- The query needs to be in json format, i.e.: `{'name': prefab_name}`.
- """
- getattr(self.opendc_db, collection).delete_many(query)
diff --git a/opendc-web/opendc-web-api/opendc/exts.py b/opendc-web/opendc-web-api/opendc/exts.py
deleted file mode 100644
index 3ee8babb..00000000
--- a/opendc-web/opendc-web-api/opendc/exts.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import os
-from functools import wraps
-
-from flask import g, _request_ctx_stack
-from jose import jwt
-from werkzeug.local import LocalProxy
-
-from opendc.database import Database
-from opendc.auth import AuthContext, AsymmetricJwtAlgorithm, get_token, AuthError
-
-
-def get_db():
- """
- Return the configured database instance for the application.
- """
- _db = getattr(g, 'db', None)
- if _db is None:
- _db = Database.from_credentials(user=os.environ['OPENDC_DB_USERNAME'],
- password=os.environ['OPENDC_DB_PASSWORD'],
- database=os.environ['OPENDC_DB'],
- host=os.environ.get('OPENDC_DB_HOST', 'localhost'))
- g.db = _db
- return _db
-
-
-db = LocalProxy(get_db)
-
-
-def get_auth_context():
- """
- Return the configured auth context for the application.
- """
- _auth_context = getattr(g, 'auth_context', None)
- if _auth_context is None:
- _auth_context = AuthContext(
- alg=AsymmetricJwtAlgorithm(jwks_url=f"https://{os.environ['AUTH0_DOMAIN']}/.well-known/jwks.json"),
- issuer=f"https://{os.environ['AUTH0_DOMAIN']}/",
- audience=os.environ['AUTH0_AUDIENCE'])
- g.auth_context = _auth_context
- return _auth_context
-
-
-auth_context = LocalProxy(get_auth_context)
-
-
-def requires_auth(f):
- """Decorator to determine if the Access Token is valid.
- """
- @wraps(f)
- def decorated(*args, **kwargs):
- token = get_token()
- payload = auth_context.validate(token)
- _request_ctx_stack.top.current_user = payload
- return f(*args, **kwargs)
-
- return decorated
-
-
-current_user = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_user', None))
-
-
-def has_scope(required_scope):
- """Determines if the required scope is present in the Access Token
- Args:
- required_scope (str): The scope required to access the resource
- """
- token = get_token()
- unverified_claims = jwt.get_unverified_claims(token)
- if unverified_claims.get("scope"):
- token_scopes = unverified_claims["scope"].split()
- for token_scope in token_scopes:
- if token_scope == required_scope:
- return True
- return False
-
-
-def requires_scope(required_scope):
- """Determines if the required scope is present in the Access Token
- Args:
- required_scope (str): The scope required to access the resource
- """
- def decorator(f):
- @wraps(f)
- def decorated(*args, **kwargs):
- if not has_scope(required_scope):
- raise AuthError({"code": "Unauthorized", "description": "You don't have access to this resource"}, 403)
- return f(*args, **kwargs)
-
- return decorated
-
- return decorator
diff --git a/opendc-web/opendc-web-api/opendc/models/__init__.py b/opendc-web/opendc-web-api/opendc/models/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/opendc-web/opendc-web-api/opendc/models/__init__.py
+++ /dev/null
diff --git a/opendc-web/opendc-web-api/opendc/models/model.py b/opendc-web/opendc-web-api/opendc/models/model.py
deleted file mode 100644
index 28299453..00000000
--- a/opendc-web/opendc-web-api/opendc/models/model.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from bson.objectid import ObjectId
-from werkzeug.exceptions import NotFound
-
-from opendc.exts import db
-
-
-class Model:
- """Base class for all models."""
-
- collection_name = '<specified in subclasses>'
-
- @classmethod
- def from_id(cls, _id):
- """Fetches the document with given ID from the collection."""
- if isinstance(_id, str) and len(_id) == 24:
- _id = ObjectId(_id)
-
- return cls(db.fetch_one({'_id': _id}, cls.collection_name))
-
- @classmethod
- def get_all(cls):
- """Fetches all documents from the collection."""
- return cls(db.fetch_all({}, cls.collection_name))
-
- def __init__(self, obj):
- self.obj = obj
-
- def get_id(self):
- """Returns the ID of the enclosed object."""
- return self.obj['_id']
-
- def check_exists(self):
- """Raises an error if the enclosed object does not exist."""
- if self.obj is None:
- raise NotFound('Entity not found.')
-
- def set_property(self, key, value):
- """Sets the given property on the enclosed object, with support for simple nested access."""
- if '.' in key:
- keys = key.split('.')
- self.obj[keys[0]][keys[1]] = value
- else:
- self.obj[key] = value
-
- def insert(self):
- """Inserts the enclosed object and generates a UUID for it."""
- self.obj['_id'] = ObjectId()
- db.insert(self.obj, self.collection_name)
-
- def update(self):
- """Updates the enclosed object and updates the internal reference to the newly inserted object."""
- db.update(self.get_id(), self.obj, self.collection_name)
-
- def delete(self):
- """Deletes the enclosed object in the database, if it existed."""
- if self.obj is None:
- return None
-
- old_object = self.obj.copy()
- db.delete_one({'_id': self.get_id()}, self.collection_name)
- return old_object
diff --git a/opendc-web/opendc-web-api/opendc/models/portfolio.py b/opendc-web/opendc-web-api/opendc/models/portfolio.py
deleted file mode 100644
index eb016947..00000000
--- a/opendc-web/opendc-web-api/opendc/models/portfolio.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from bson import ObjectId
-from marshmallow import Schema, fields
-
-from opendc.exts import db
-from opendc.models.project import Project
-from opendc.models.model import Model
-
-
-class TargetSchema(Schema):
- """
- Schema representing a target.
- """
- enabledMetrics = fields.List(fields.String())
- repeatsPerScenario = fields.Integer(required=True)
-
-
-class PortfolioSchema(Schema):
- """
- Schema representing a portfolio.
- """
- _id = fields.String(dump_only=True)
- projectId = fields.String()
- name = fields.String(required=True)
- scenarioIds = fields.List(fields.String())
- targets = fields.Nested(TargetSchema)
-
-
-class Portfolio(Model):
- """Model representing a Portfolio."""
-
- collection_name = 'portfolios'
-
- def check_user_access(self, user_id, edit_access):
- """Raises an error if the user with given [user_id] has insufficient access.
-
- Checks access on the parent project.
-
- :param user_id: The User ID of the user.
- :param edit_access: True when edit access should be checked, otherwise view access.
- """
- project = Project.from_id(self.obj['projectId'])
- project.check_user_access(user_id, edit_access)
-
- @classmethod
- def get_for_project(cls, project_id):
- """Get all portfolios for the specified project id."""
- return db.fetch_all({'projectId': ObjectId(project_id)}, cls.collection_name)
diff --git a/opendc-web/opendc-web-api/opendc/models/prefab.py b/opendc-web/opendc-web-api/opendc/models/prefab.py
deleted file mode 100644
index 5e4b81dc..00000000
--- a/opendc-web/opendc-web-api/opendc/models/prefab.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from marshmallow import Schema, fields
-from werkzeug.exceptions import Forbidden
-
-from opendc.models.topology import ObjectSchema
-from opendc.models.model import Model
-
-
-class PrefabSchema(Schema):
- """
- Schema for a Prefab.
- """
- _id = fields.String(dump_only=True)
- authorId = fields.String(dump_only=True)
- name = fields.String(required=True)
- datetimeCreated = fields.DateTime()
- datetimeLastEdited = fields.DateTime()
- rack = fields.Nested(ObjectSchema)
-
-
-class Prefab(Model):
- """Model representing a Prefab."""
-
- collection_name = 'prefabs'
-
- def check_user_access(self, user_id):
- """Raises an error if the user with given [user_id] has insufficient access to view this prefab.
-
- :param user_id: The user ID of the user.
- """
- if self.obj['authorId'] != user_id and self.obj['visibility'] == "private":
- raise Forbidden("Forbidden from retrieving prefab.")
diff --git a/opendc-web/opendc-web-api/opendc/models/project.py b/opendc-web/opendc-web-api/opendc/models/project.py
deleted file mode 100644
index f2b3b564..00000000
--- a/opendc-web/opendc-web-api/opendc/models/project.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from marshmallow import Schema, fields, validate
-from werkzeug.exceptions import Forbidden
-
-from opendc.models.model import Model
-from opendc.exts import db
-
-
-class ProjectAuthorizations(Schema):
- """
- Schema representing a project authorization.
- """
- userId = fields.String(required=True)
- level = fields.String(required=True, validate=validate.OneOf(["VIEW", "EDIT", "OWN"]))
-
-
-class ProjectSchema(Schema):
- """
- Schema representing a Project.
- """
- _id = fields.String(dump_only=True)
- name = fields.String(required=True)
- datetimeCreated = fields.DateTime()
- datetimeLastEdited = fields.DateTime()
- topologyIds = fields.List(fields.String())
- portfolioIds = fields.List(fields.String())
- authorizations = fields.List(fields.Nested(ProjectAuthorizations))
-
-
-class Project(Model):
- """Model representing a Project."""
-
- collection_name = 'projects'
-
- def check_user_access(self, user_id, edit_access):
- """Raises an error if the user with given [user_id] has insufficient access.
-
- :param user_id: The User ID of the user.
- :param edit_access: True when edit access should be checked, otherwise view access.
- """
- for authorization in self.obj['authorizations']:
- if user_id == authorization['userId'] and authorization['level'] != 'VIEW' or not edit_access:
- return
- raise Forbidden("Forbidden from retrieving project.")
-
- @classmethod
- def get_for_user(cls, user_id):
- """Get all projects for the specified user id."""
- return db.fetch_all({'authorizations.userId': user_id}, Project.collection_name)
diff --git a/opendc-web/opendc-web-api/opendc/models/scenario.py b/opendc-web/opendc-web-api/opendc/models/scenario.py
deleted file mode 100644
index 47771e06..00000000
--- a/opendc-web/opendc-web-api/opendc/models/scenario.py
+++ /dev/null
@@ -1,93 +0,0 @@
-from datetime import datetime
-
-from bson import ObjectId
-from marshmallow import Schema, fields
-
-from opendc.exts import db
-from opendc.models.model import Model
-from opendc.models.portfolio import Portfolio
-
-
-class SimulationSchema(Schema):
- """
- Simulation details.
- """
- state = fields.String()
-
-
-class TraceSchema(Schema):
- """
- Schema for specifying the trace of a scenario.
- """
- traceId = fields.String()
- loadSamplingFraction = fields.Float()
-
-
-class TopologySchema(Schema):
- """
- Schema for topology specification for a scenario.
- """
- topologyId = fields.String()
-
-
-class OperationalSchema(Schema):
- """
- Schema for the operational phenomena for a scenario.
- """
- failuresEnabled = fields.Boolean()
- performanceInterferenceEnabled = fields.Boolean()
- schedulerName = fields.String()
-
-
-class ScenarioSchema(Schema):
- """
- Schema representing a scenario.
- """
- _id = fields.String(dump_only=True)
- portfolioId = fields.String()
- name = fields.String(required=True)
- trace = fields.Nested(TraceSchema)
- topology = fields.Nested(TopologySchema)
- operational = fields.Nested(OperationalSchema)
- simulation = fields.Nested(SimulationSchema, dump_only=True)
- results = fields.Dict(dump_only=True)
-
-
-class Scenario(Model):
- """Model representing a Scenario."""
-
- collection_name = 'scenarios'
-
- def check_user_access(self, user_id, edit_access):
- """Raises an error if the user with given [user_id] has insufficient access.
-
- Checks access on the parent project.
-
- :param user_id: The User ID of the user.
- :param edit_access: True when edit access should be checked, otherwise view access.
- """
- portfolio = Portfolio.from_id(self.obj['portfolioId'])
- portfolio.check_user_access(user_id, edit_access)
-
- @classmethod
- def get_jobs(cls):
- """Obtain the scenarios that have been queued.
- """
- return cls(db.fetch_all({'simulation.state': 'QUEUED'}, cls.collection_name))
-
- @classmethod
- def get_for_portfolio(cls, portfolio_id):
- """Get all scenarios for the specified portfolio id."""
- return db.fetch_all({'portfolioId': ObjectId(portfolio_id)}, cls.collection_name)
-
- def update_state(self, new_state, results=None):
- """Atomically update the state of the Scenario.
- """
- update = {'$set': {'simulation.state': new_state, 'simulation.heartbeat': datetime.now()}}
- if results:
- update['$set']['results'] = results
- return db.fetch_and_update(
- query={'_id': self.obj['_id'], 'simulation.state': self.obj['simulation']['state']},
- update=update,
- collection=self.collection_name
- )
diff --git a/opendc-web/opendc-web-api/opendc/models/topology.py b/opendc-web/opendc-web-api/opendc/models/topology.py
deleted file mode 100644
index 44994818..00000000
--- a/opendc-web/opendc-web-api/opendc/models/topology.py
+++ /dev/null
@@ -1,108 +0,0 @@
-from bson import ObjectId
-from marshmallow import Schema, fields
-
-from opendc.exts import db
-from opendc.models.project import Project
-from opendc.models.model import Model
-
-
-class MemorySchema(Schema):
- """
- Schema representing a memory unit.
- """
- _id = fields.String()
- name = fields.String()
- speedMbPerS = fields.Integer()
- sizeMb = fields.Integer()
- energyConsumptionW = fields.Integer()
-
-
-class PuSchema(Schema):
- """
- Schema representing a processing unit.
- """
- _id = fields.String()
- name = fields.String()
- clockRateMhz = fields.Integer()
- numberOfCores = fields.Integer()
- energyConsumptionW = fields.Integer()
-
-
-class MachineSchema(Schema):
- """
- Schema representing a machine.
- """
- _id = fields.String()
- position = fields.Integer()
- cpus = fields.List(fields.Nested(PuSchema))
- gpus = fields.List(fields.Nested(PuSchema))
- memories = fields.List(fields.Nested(MemorySchema))
- storages = fields.List(fields.Nested(MemorySchema))
- rackId = fields.String()
-
-
-class ObjectSchema(Schema):
- """
- Schema representing a room object.
- """
- _id = fields.String()
- name = fields.String()
- capacity = fields.Integer()
- powerCapacityW = fields.Integer()
- machines = fields.List(fields.Nested(MachineSchema))
- tileId = fields.String()
-
-
-class TileSchema(Schema):
- """
- Schema representing a room tile.
- """
- _id = fields.String()
- topologyId = fields.String()
- positionX = fields.Integer()
- positionY = fields.Integer()
- rack = fields.Nested(ObjectSchema)
- roomId = fields.String()
-
-
-class RoomSchema(Schema):
- """
- Schema representing a room.
- """
- _id = fields.String()
- name = fields.String(required=True)
- topologyId = fields.String()
- tiles = fields.List(fields.Nested(TileSchema), required=True)
-
-
-class TopologySchema(Schema):
- """
- Schema representing a datacenter topology.
- """
- _id = fields.String(dump_only=True)
- projectId = fields.String()
- name = fields.String(required=True)
- rooms = fields.List(fields.Nested(RoomSchema), required=True)
- datetimeLastEdited = fields.DateTime()
-
-
-class Topology(Model):
- """Model representing a Project."""
-
- collection_name = 'topologies'
-
- def check_user_access(self, user_id, edit_access):
- """Raises an error if the user with given [user_id] has insufficient access.
-
- Checks access on the parent project.
-
- :param user_id: The User ID of the user.
- :param edit_access: True when edit access should be checked, otherwise view access.
- """
- project = Project.from_id(self.obj['projectId'])
- project.check_user_access(user_id, edit_access)
-
- @classmethod
- def get_for_project(cls, project_id):
- """Get all topologies for the specified project id."""
- return db.fetch_all({'projectId': ObjectId(project_id)}, cls.collection_name)
diff --git a/opendc-web/opendc-web-api/opendc/models/trace.py b/opendc-web/opendc-web-api/opendc/models/trace.py
deleted file mode 100644
index 69287f29..00000000
--- a/opendc-web/opendc-web-api/opendc/models/trace.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from marshmallow import Schema, fields
-
-from opendc.models.model import Model
-
-
-class TraceSchema(Schema):
- """Schema for a Trace."""
- _id = fields.String(dump_only=True)
- name = fields.String()
- type = fields.String()
-
-
-class Trace(Model):
- """Model representing a Trace."""
-
- collection_name = 'traces'
diff --git a/opendc-web/opendc-web-api/opendc/util.py b/opendc-web/opendc-web-api/opendc/util.py
deleted file mode 100644
index e7dc07a4..00000000
--- a/opendc-web/opendc-web-api/opendc/util.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# 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.
-
-import flask
-from bson.objectid import ObjectId
-
-
-class JSONEncoder(flask.json.JSONEncoder):
- """
- A customized JSON encoder to handle unsupported types.
- """
- def default(self, o):
- if isinstance(o, ObjectId):
- return str(o)
- return flask.json.JSONEncoder.default(self, o)