diff options
Diffstat (limited to 'opendc-web/opendc-web-api')
| -rw-r--r-- | opendc-web/opendc-web-api/opendc/api/portfolios.py | 14 | ||||
| -rw-r--r-- | opendc-web/opendc-web-api/opendc/api/projects.py | 27 | ||||
| -rw-r--r-- | opendc-web/opendc-web-api/opendc/auth.py | 5 | ||||
| -rw-r--r-- | opendc-web/opendc-web-api/opendc/exts.py | 7 | ||||
| -rw-r--r-- | opendc-web/opendc-web-api/opendc/models/portfolio.py | 7 | ||||
| -rw-r--r-- | opendc-web/opendc-web-api/opendc/models/scenario.py | 15 | ||||
| -rw-r--r-- | opendc-web/opendc-web-api/opendc/models/topology.py | 9 | ||||
| -rw-r--r-- | opendc-web/opendc-web-api/static/schema.yml | 182 | ||||
| -rw-r--r-- | opendc-web/opendc-web-api/tests/api/test_portfolios.py | 16 | ||||
| -rw-r--r-- | opendc-web/opendc-web-api/tests/api/test_projects.py | 30 |
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 |
