summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-api
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-web/opendc-web-api')
-rw-r--r--opendc-web/opendc-web-api/opendc/api/portfolios.py14
-rw-r--r--opendc-web/opendc-web-api/opendc/api/projects.py27
-rw-r--r--opendc-web/opendc-web-api/opendc/auth.py5
-rw-r--r--opendc-web/opendc-web-api/opendc/exts.py7
-rw-r--r--opendc-web/opendc-web-api/opendc/models/portfolio.py7
-rw-r--r--opendc-web/opendc-web-api/opendc/models/scenario.py15
-rw-r--r--opendc-web/opendc-web-api/opendc/models/topology.py9
-rw-r--r--opendc-web/opendc-web-api/static/schema.yml182
-rw-r--r--opendc-web/opendc-web-api/tests/api/test_portfolios.py16
-rw-r--r--opendc-web/opendc-web-api/tests/api/test_projects.py30
10 files changed, 301 insertions, 11 deletions
diff --git a/opendc-web/opendc-web-api/opendc/api/portfolios.py b/opendc-web/opendc-web-api/opendc/api/portfolios.py
index eea82289..4d8f54fd 100644
--- a/opendc-web/opendc-web-api/opendc/api/portfolios.py
+++ b/opendc-web/opendc-web-api/opendc/api/portfolios.py
@@ -103,6 +103,20 @@ class PortfolioScenarios(Resource):
"""
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
diff --git a/opendc-web/opendc-web-api/opendc/api/projects.py b/opendc-web/opendc-web-api/opendc/api/projects.py
index 05f02a84..2b47c12e 100644
--- a/opendc-web/opendc-web-api/opendc/api/projects.py
+++ b/opendc-web/opendc-web-api/opendc/api/projects.py
@@ -132,6 +132,18 @@ class ProjectTopologies(Resource):
"""
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()
@@ -170,6 +182,18 @@ class ProjectPortfolios(Resource):
"""
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()
@@ -190,7 +214,8 @@ class ProjectPortfolios(Resource):
project.obj['portfolioIds'].append(portfolio.get_id())
project.update()
- return {'data': portfolio.obj}
+ data = PortfolioSchema().dump(portfolio.obj)
+ return {'data': data}
class PutSchema(Schema):
"""
diff --git a/opendc-web/opendc-web-api/opendc/auth.py b/opendc-web/opendc-web-api/opendc/auth.py
index 6db06fb1..d5da6ee5 100644
--- a/opendc-web/opendc-web-api/opendc/auth.py
+++ b/opendc-web/opendc-web-api/opendc/auth.py
@@ -40,10 +40,7 @@ def get_token():
parts = auth.split()
if parts[0].lower() != "bearer":
- raise AuthError({
- "code": "invalid_header",
- "description": "Authorization header must start with Bearer"
- }, 401)
+ 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:
diff --git a/opendc-web/opendc-web-api/opendc/exts.py b/opendc-web/opendc-web-api/opendc/exts.py
index 17dacd5e..3ee8babb 100644
--- a/opendc-web/opendc-web-api/opendc/exts.py
+++ b/opendc-web/opendc-web-api/opendc/exts.py
@@ -83,10 +83,9 @@ def requires_scope(required_scope):
@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)
+ 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/portfolio.py b/opendc-web/opendc-web-api/opendc/models/portfolio.py
index 1643e23e..eb016947 100644
--- a/opendc-web/opendc-web-api/opendc/models/portfolio.py
+++ b/opendc-web/opendc-web-api/opendc/models/portfolio.py
@@ -1,5 +1,7 @@
+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
@@ -38,3 +40,8 @@ class Portfolio(Model):
"""
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/scenario.py b/opendc-web/opendc-web-api/opendc/models/scenario.py
index 0fb6c453..47771e06 100644
--- a/opendc-web/opendc-web-api/opendc/models/scenario.py
+++ b/opendc-web/opendc-web-api/opendc/models/scenario.py
@@ -1,5 +1,6 @@
from datetime import datetime
+from bson import ObjectId
from marshmallow import Schema, fields
from opendc.exts import db
@@ -7,6 +8,13 @@ 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.
@@ -41,6 +49,8 @@ class ScenarioSchema(Schema):
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):
@@ -65,6 +75,11 @@ class Scenario(Model):
"""
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.
"""
diff --git a/opendc-web/opendc-web-api/opendc/models/topology.py b/opendc-web/opendc-web-api/opendc/models/topology.py
index 71d2cade..592f82c5 100644
--- a/opendc-web/opendc-web-api/opendc/models/topology.py
+++ b/opendc-web/opendc-web-api/opendc/models/topology.py
@@ -1,5 +1,7 @@
+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
@@ -73,7 +75,7 @@ class TopologySchema(Schema):
Schema representing a datacenter topology.
"""
_id = fields.String(dump_only=True)
- projectId = fields.String(dump_only=True)
+ projectId = fields.String()
name = fields.String(required=True)
rooms = fields.List(fields.Nested(RoomSchema), required=True)
@@ -93,3 +95,8 @@ class Topology(Model):
"""
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/static/schema.yml b/opendc-web/opendc-web-api/static/schema.yml
index 6a07ae52..56cf58e7 100644
--- a/opendc-web/opendc-web-api/static/schema.yml
+++ b/opendc-web/opendc-web-api/static/schema.yml
@@ -222,6 +222,49 @@ paths:
schema:
$ref: "#/components/schemas/NotFound"
"/projects/{projectId}/topologies":
+ get:
+ tags:
+ - projects
+ description: Get Project Topologies.
+ parameters:
+ - name: projectId
+ in: path
+ description: Project's ID.
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Successfully retrieved Project Topologies.
+ content:
+ "application/json":
+ schema:
+ type: object
+ required:
+ - data
+ properties:
+ data:
+ type: array
+ items:
+ $ref: "#/components/schemas/Topology"
+ "401":
+ description: Unauthorized.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/Unauthorized"
+ "403":
+ description: Forbidden from retrieving Project.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/Forbidden"
+ "404":
+ description: Project not found.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/NotFound"
post:
tags:
- projects
@@ -273,9 +316,52 @@ paths:
schema:
$ref: "#/components/schemas/NotFound"
"/projects/{projectId}/portfolios":
+ get:
+ tags:
+ - projects
+ description: Get Project Portfolios.
+ parameters:
+ - name: projectId
+ in: path
+ description: Project's ID.
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Successfully retrieved Project Portfolios.
+ content:
+ "application/json":
+ schema:
+ type: object
+ required:
+ - data
+ properties:
+ data:
+ type: array
+ items:
+ $ref: "#/components/schemas/Portfolio"
+ "401":
+ description: Unauthorized.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/Unauthorized"
+ "403":
+ description: Forbidden from retrieving Project.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/Forbidden"
+ "404":
+ description: Project not found.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/NotFound"
post:
tags:
- - portfolios
+ - projects
description: Add a Portfolio.
parameters:
- name: projectId
@@ -611,6 +697,100 @@ paths:
"application/json":
schema:
$ref: "#/components/schemas/NotFound"
+ "/portfolios/{portfolioId}/scenarios":
+ get:
+ tags:
+ - portfolios
+ description: Get Portfolio Scenarios.
+ parameters:
+ - name: portfolioId
+ in: path
+ description: Portfolio's ID.
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Successfully retrieved Portfolio Scenarios.
+ content:
+ "application/json":
+ schema:
+ type: object
+ required:
+ - data
+ properties:
+ data:
+ type: array
+ items:
+ $ref: "#/components/schemas/Scenario"
+ "401":
+ description: Unauthorized.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/Unauthorized"
+ "403":
+ description: Forbidden from retrieving Portfolio.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/Forbidden"
+ "404":
+ description: Portfolio not found.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/NotFound"
+ post:
+ tags:
+ - portfolios
+ description: Add a Scenario.
+ parameters:
+ - name: portfolioId
+ in: path
+ description: Portfolio's ID.
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ topology:
+ $ref: "#/components/schemas/Scenario"
+ description: The new Scenario.
+ required: true
+ responses:
+ "200":
+ description: Successfully added Scenario.
+ content:
+ "application/json":
+ schema:
+ type: object
+ required:
+ - data
+ properties:
+ data:
+ $ref: "#/components/schemas/Scenario"
+ "400":
+ description: Missing or incorrectly typed parameter.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/Invalid"
+ "401":
+ description: Unauthorized.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/Unauthorized"
+ "404":
+ description: Portfolio not found.
+ content:
+ "application/json":
+ schema:
+ $ref: "#/components/schemas/NotFound"
"/scenarios/{scenarioId}":
get:
tags:
diff --git a/opendc-web/opendc-web-api/tests/api/test_portfolios.py b/opendc-web/opendc-web-api/tests/api/test_portfolios.py
index da7991f6..196fcb1c 100644
--- a/opendc-web/opendc-web-api/tests/api/test_portfolios.py
+++ b/opendc-web/opendc-web-api/tests/api/test_portfolios.py
@@ -322,3 +322,19 @@ def test_add_portfolio(client, mocker):
assert 'projectId' in res.json['data']
assert 'scenarioIds' in res.json['data']
assert '200' in res.status
+
+
+def test_get_portfolio_scenarios(client, mocker):
+ mocker.patch.object(db,
+ 'fetch_one',
+ return_value={
+ 'projectId': test_id,
+ '_id': test_id,
+ 'authorizations': [{
+ 'userId': 'test',
+ 'level': 'EDIT'
+ }]
+ })
+ mocker.patch.object(db, 'fetch_all', return_value=[{'_id': test_id}])
+ res = client.get(f'/portfolios/{test_id}/scenarios')
+ assert '200' in res.status
diff --git a/opendc-web/opendc-web-api/tests/api/test_projects.py b/opendc-web/opendc-web-api/tests/api/test_projects.py
index c4c82e0d..1cfe4c52 100644
--- a/opendc-web/opendc-web-api/tests/api/test_projects.py
+++ b/opendc-web/opendc-web-api/tests/api/test_projects.py
@@ -30,6 +30,36 @@ def test_get_user_projects(client, mocker):
assert '200' in res.status
+def test_get_user_topologies(client, mocker):
+ mocker.patch.object(db,
+ 'fetch_one',
+ return_value={
+ '_id': test_id,
+ 'authorizations': [{
+ 'userId': 'test',
+ 'level': 'EDIT'
+ }]
+ })
+ mocker.patch.object(db, 'fetch_all', return_value=[{'_id': test_id}])
+ res = client.get(f'/projects/{test_id}/topologies')
+ assert '200' in res.status
+
+
+def test_get_user_portfolios(client, mocker):
+ mocker.patch.object(db,
+ 'fetch_one',
+ return_value={
+ '_id': test_id,
+ 'authorizations': [{
+ 'userId': 'test',
+ 'level': 'EDIT'
+ }]
+ })
+ mocker.patch.object(db, 'fetch_all', return_value=[{'_id': test_id}])
+ res = client.get(f'/projects/{test_id}/portfolios')
+ assert '200' in res.status
+
+
def test_add_project_missing_parameter(client):
assert '400' in client.post('/projects/').status