diff options
Diffstat (limited to 'opendc-web/opendc-web-api')
96 files changed, 4522 insertions, 5581 deletions
diff --git a/opendc-web/opendc-web-api/.coveragerc b/opendc-web/opendc-web-api/.coveragerc deleted file mode 100644 index 55d99c2e..00000000 --- a/opendc-web/opendc-web-api/.coveragerc +++ /dev/null @@ -1,5 +0,0 @@ -[run] -source = . -omit = - tests/* - conftest.py diff --git a/opendc-web/opendc-web-api/.dockerignore b/opendc-web/opendc-web-api/.dockerignore deleted file mode 100644 index 06d67de9..00000000 --- a/opendc-web/opendc-web-api/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -Dockerfile - -.idea/ -**/out -*.iml -.idea_modules/ - -.pytest_cache diff --git a/opendc-web/opendc-web-api/.gitignore b/opendc-web/opendc-web-api/.gitignore deleted file mode 100644 index 9f8dfc5c..00000000 --- a/opendc-web/opendc-web-api/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -.DS_Store -*.pyc -*.pyo -venv -venv* -dist -build -*.egg -*.egg-info -_mailinglist -.tox -.cache/ -.idea/ -config.json -test.json -.env* -.coverage -coverage.xml -junit-report.xml diff --git a/opendc-web/opendc-web-api/.pylintrc b/opendc-web/opendc-web-api/.pylintrc deleted file mode 100644 index 4dbb0b50..00000000 --- a/opendc-web/opendc-web-api/.pylintrc +++ /dev/null @@ -1,523 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Specify a score threshold to be exceeded before program exits with error. -fail-under=10 - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=duplicate-code, - missing-module-docstring, - invalid-name, - bare-except, - too-few-public-methods, - fixme, - no-self-use - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'error', 'warning', 'refactor', and 'convention' -# which contain the number of messages in each category, as well as 'statement' -# which is the total number of statements analyzed. This score is used by the -# global evaluation report (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it work, -# install the python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -#notes-rgx= - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )?<?https?://\S+>?$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=120 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=12 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/opendc-web/opendc-web-api/.style.yapf b/opendc-web/opendc-web-api/.style.yapf deleted file mode 100644 index f5c26c57..00000000 --- a/opendc-web/opendc-web-api/.style.yapf +++ /dev/null @@ -1,3 +0,0 @@ -[style] -based_on_style = pep8 -column_limit=120 diff --git a/opendc-web/opendc-web-api/Dockerfile b/opendc-web/opendc-web-api/Dockerfile deleted file mode 100644 index 505a69de..00000000 --- a/opendc-web/opendc-web-api/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM python:3.9-slim -MAINTAINER OpenDC Maintainers <opendc@atlarge-research.com> - -# Ensure the STDOUT is not buffered by Python so that our logs become visible -# See https://stackoverflow.com/q/29663459/10213073 -ENV PYTHONUNBUFFERED 1 - -# Copy OpenDC directory -COPY ./ /opendc - -# Fetch web server dependencies -RUN pip install -r /opendc/requirements.txt && pip install pyuwsgi - -# Create opendc user -RUN groupadd --gid 1000 opendc \ - && useradd --uid 1000 --gid opendc --shell /bin/bash --create-home opendc -RUN chown -R opendc:opendc /opendc -USER opendc - -# Set working directory -WORKDIR /opendc - -CMD uwsgi -M --socket 0.0.0.0:80 --protocol=http --wsgi-file app.py --enable-threads --processes 2 --lazy-app diff --git a/opendc-web/opendc-web-api/README.md b/opendc-web/opendc-web-api/README.md deleted file mode 100644 index d1c469c1..00000000 --- a/opendc-web/opendc-web-api/README.md +++ /dev/null @@ -1,122 +0,0 @@ -<h1 align="center"> - <img src="../../docs/images/logo.png" width="100" alt="OpenDC"> - <br> - OpenDC Web Server -</h1> -<p align="center"> - Collaborative Datacenter Simulation and Exploration for Everybody -</p> - -<br> - -The OpenDC web server is the bridge between OpenDC's frontend and database. It is built with Flask/SocketIO in Python -and implements the OpenAPI-compliant [OpenDC API specification](../../opendc-api-spec.yml). - -This document explains a high-level view of the web server architecture ([jump](#architecture)), and describes how to -set up the web server for local development ([jump](#setup-for-local-development)). - -## Architecture - -The following diagram shows a high-level view of the architecture of the OpenDC web server. Squared-off colored boxes -indicate packages (colors become more saturated as packages are nested); rounded-off boxes indicate individual -components; dotted lines indicate control flow; and solid lines indicate data flow. - - - -The OpenDC API is implemented by the `Main Server Loop`, which is the only component in the base package. - -### Util Package - -The `Util` package handles several miscellaneous tasks: - -* `Database API`: Wraps database access functionality used by `Models` to read themselves from/write themselves into the - database. -* `Exceptions`: Holds definitions for exceptions used throughout the web server. -* `Parameter Checker`: Recursively checks whether required `Request` parameters are present and correctly typed. -* `REST`: Parses HTTP messages into `Request` objects, and calls the appropriate `API` endpoint to get a `Response` - object to return to the `Main Server Loop`. - -### API Package - -The `API` package contains the logic for the HTTP methods in each API endpoint. Packages are structured to mirror the -API: the code for the endpoint `GET api/projects`, for example, would be located at the `endpoint.py` inside -the `projects` package (so at `api/projects/endpoint.py`). - -An `endpoint.py` file contains methods for each HTTP method it supports, which takes a request as input (such -as `def GET(request):`). Typically, such a method checks whether the parameters were passed correctly (using -the `Parameter Checker`); fetches some model from the database; checks whether the data exists and is accessible by the -user who made the request; possibly modifies this data and writes it back to the database; and returns a JSON -representation of the model. - -The `REST` component dynamically imports the appropriate method from the appropriate `endpoint`, according to request it -receives, and executes it. - -### Models Package - -The `models` package contains the logic for mapping Python objects to their database representations. This involves an -abstract `model` which has generic CRUD operations. Extensions of `model`, such as a `User` or `Project`, specify some -more specific operations and their collection metadata. - -`Endpoint`s import these `models` and use them to execute requests. - -## Setup for Local Development - -The following steps will guide you through setting up the OpenDC web server locally for development. - -### Local Setup - -#### Install requirements - -Make sure you have Python 3.7+ installed (if not, get it [here](https://www.python.org/)), as well as pip (if not, get -it [here](https://pip.pypa.io/en/stable/installing/)). Then run the following to install the requirements. - -```bash -pip install -r requirements.txt -``` - -The web server also requires a running MongoDB instance. We recommend setting this up through docker, by -running `docker-compose build` and `docker-compose up` in the [`mongodb` directory](../../database) of the main OpenDC -repository. - -#### Get and configure the code - -Clone OpenDC and follow the [instructions from the deployment guide](../../docs/deploy.md) to set up an [Auth0](https://auth0.com) -application and environment variables. - -**Important:** Be sure to set up environment variables according to those instructions, in a `.env` file. - -#### Set up the database - -You can selectively run only the database services from the standard OpenDC `docker-compose` setup (in the root -directory): - -```bash -docker-compose build mongo mongo-express -docker-compose up mongo mongo-express -``` - -This will set you up with a running MongoDB instance and a visual inspection tool running -on [localhost:8082](http://localhost:8082), with which you can view and manipulate the database. Add the simulator -images to the command lists above if you want to test simulation capabilities, as well. - -### Local Development - -Run the server. - -```bash -python3 -m flask run --port 8081 -``` - -When editing the web server code, restart the server (`CTRL` + `c` followed by `python app.py` in the console running -the server) to see the result of your changes. - -#### Code Style - -To format all files, run `format.sh` in this directory. The script uses `yapf` internally to format everything -automatically. - -To check if code style is up to modern standards, run `check.sh` in this directory. The script uses `pylint` internally. - -#### Testing - -Run `pytest opendc` in this directory to run all tests. diff --git a/opendc-web/opendc-web-api/app.py b/opendc-web/opendc-web-api/app.py deleted file mode 100755 index 36c80b7a..00000000 --- a/opendc-web/opendc-web-api/app.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -import mimetypes -import os - -from dotenv import load_dotenv -from flask import Flask, jsonify, redirect -from flask_compress import Compress -from flask_cors import CORS -from flask_restful import Api -from flask_swagger_ui import get_swaggerui_blueprint -from marshmallow import ValidationError - -from opendc.api.jobs import JobList, Job -from opendc.api.portfolios import Portfolio, PortfolioScenarios -from opendc.api.prefabs import Prefab, PrefabList -from opendc.api.projects import ProjectList, Project, ProjectTopologies, ProjectPortfolios -from opendc.api.scenarios import Scenario -from opendc.api.schedulers import SchedulerList -from opendc.api.topologies import Topology -from opendc.api.traces import TraceList, Trace -from opendc.auth import AuthError -from opendc.util import JSONEncoder - - -# Load environmental variables from dotenv file -load_dotenv() - - -def setup_sentry(): - """ - Setup the Sentry integration for Flask if a DSN is supplied via the environmental variables. - """ - if 'SENTRY_DSN' not in os.environ: - return - - import sentry_sdk - from sentry_sdk.integrations.flask import FlaskIntegration - - sentry_sdk.init( - integrations=[FlaskIntegration()], - traces_sample_rate=0.1 - ) - - -def setup_api(app): - """ - Setup the API interface. - """ - api = Api(app) - # Map to ('string', 'ObjectId') passing type and format - api.add_resource(ProjectList, '/projects/') - api.add_resource(Project, '/projects/<string:project_id>') - api.add_resource(ProjectTopologies, '/projects/<string:project_id>/topologies') - api.add_resource(ProjectPortfolios, '/projects/<string:project_id>/portfolios') - api.add_resource(Topology, '/topologies/<string:topology_id>') - api.add_resource(PrefabList, '/prefabs/') - api.add_resource(Prefab, '/prefabs/<string:prefab_id>') - api.add_resource(Portfolio, '/portfolios/<string:portfolio_id>') - api.add_resource(PortfolioScenarios, '/portfolios/<string:portfolio_id>/scenarios') - api.add_resource(Scenario, '/scenarios/<string:scenario_id>') - api.add_resource(TraceList, '/traces/') - api.add_resource(Trace, '/traces/<string:trace_id>') - api.add_resource(SchedulerList, '/schedulers/') - api.add_resource(JobList, '/jobs/') - api.add_resource(Job, '/jobs/<string:job_id>') - - @app.errorhandler(AuthError) - def handle_auth_error(ex): - response = jsonify(ex.error) - response.status_code = ex.status_code - return response - - @app.errorhandler(ValidationError) - def handle_validation_error(ex): - return {'message': 'Input validation failed', 'errors': ex.messages}, 400 - - return api - - -def setup_swagger(app): - """ - Setup Swagger UI - """ - SWAGGER_URL = '/docs' - API_URL = '../schema.yml' - - swaggerui_blueprint = get_swaggerui_blueprint( - SWAGGER_URL, - API_URL, - config={ - 'app_name': "OpenDC API v2" - }, - oauth_config={ - 'clientId': os.environ.get("AUTH0_DOCS_CLIENT_ID", ""), - 'additionalQueryStringParams': {'audience': os.environ.get("AUTH0_AUDIENCE", "https://api.opendc.org/v2/")}, - } - ) - app.register_blueprint(swaggerui_blueprint) - - -def create_app(testing=False): - app = Flask(__name__, static_url_path='/') - app.config['TESTING'] = testing - app.config['SECRET_KEY'] = os.environ['OPENDC_FLASK_SECRET'] - app.config['RESTFUL_JSON'] = {'cls': JSONEncoder} - app.json_encoder = JSONEncoder - - # Define YAML content type - mimetypes.add_type('text/yaml', '.yml') - - # Setup Sentry if DSN is specified - setup_sentry() - - # Set up CORS support - CORS(app) - - # Setup compression - compress = Compress() - compress.init_app(app) - - setup_api(app) - setup_swagger(app) - - @app.route('/') - def index(): - """ - Redirect the user to the API documentation if it accesses the API root. - """ - return redirect('docs/') - - return app - - -application = create_app(testing="OPENDC_FLASK_TESTING" in os.environ) - -if __name__ == '__main__': - application.run() diff --git a/opendc-web/opendc-web-api/build.gradle.kts b/opendc-web/opendc-web-api/build.gradle.kts index 7edfd134..9889b832 100644 --- a/opendc-web/opendc-web-api/build.gradle.kts +++ b/opendc-web/opendc-web-api/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 AtLarge Research + * Copyright (c) 2020 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 @@ -19,3 +19,75 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ + +description = "REST API for the OpenDC website" + +/* Build configuration */ +plugins { + `kotlin-conventions` + kotlin("plugin.allopen") + kotlin("plugin.jpa") + `testing-conventions` + `jacoco-conventions` + id("io.quarkus") +} + +dependencies { + implementation(enforcedPlatform(libs.quarkus.bom)) + + implementation(projects.opendcWeb.opendcWebProto) + + implementation(libs.quarkus.kotlin) + implementation(libs.quarkus.resteasy.core) + implementation(libs.quarkus.resteasy.jackson) + implementation(libs.jackson.module.kotlin) + implementation(libs.quarkus.smallrye.openapi) + + implementation(libs.quarkus.security) + implementation(libs.quarkus.oidc) + + implementation(libs.quarkus.hibernate.orm) + implementation(libs.quarkus.hibernate.validator) + implementation(libs.quarkus.hibernate.types) + implementation(libs.quarkus.jdbc.postgresql) + quarkusDev(libs.quarkus.jdbc.h2) + + testImplementation(libs.quarkus.junit5.core) + testImplementation(libs.quarkus.junit5.mockk) + testImplementation(libs.restassured.core) + testImplementation(libs.restassured.kotlin) + testImplementation(libs.quarkus.test.security) + testImplementation(libs.quarkus.jdbc.h2) +} + +allOpen { + annotation("javax.ws.rs.Path") + annotation("javax.enterprise.context.ApplicationScoped") + annotation("io.quarkus.test.junit.QuarkusTest") + annotation("javax.persistence.Entity") +} + +tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { + kotlinOptions.javaParameters = true +} + +tasks.quarkusDev { + workingDir = rootProject.projectDir.toString() +} + +tasks.test { + extensions.configure(JacocoTaskExtension::class) { + excludeClassLoaders = listOf("*QuarkusClassLoader") + destinationFile = layout.buildDirectory.file("jacoco-quarkus.exec").get().asFile + } +} + +/* Fix for Quarkus/ktlint-gradle incompatibilities */ +tasks.named("runKtlintCheckOverMainSourceSet").configure { + mustRunAfter(tasks.quarkusGenerateCode) + mustRunAfter(tasks.quarkusGenerateCodeDev) +} + +tasks.named("runKtlintCheckOverTestSourceSet").configure { + mustRunAfter(tasks.quarkusGenerateCodeTests) +} diff --git a/opendc-web/opendc-web-api/check.sh b/opendc-web/opendc-web-api/check.sh deleted file mode 100755 index abe2c596..00000000 --- a/opendc-web/opendc-web-api/check.sh +++ /dev/null @@ -1 +0,0 @@ -pylint opendc --ignore-patterns=test_.*?py diff --git a/opendc-web/opendc-web-api/conftest.py b/opendc-web/opendc-web-api/conftest.py deleted file mode 100644 index 958a5894..00000000 --- a/opendc-web/opendc-web-api/conftest.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Configuration file for all unit tests. -""" - -from functools import wraps -import pytest -from flask import _request_ctx_stack, g -from opendc.database import Database - - -def requires_auth_mock(f): - @wraps(f) - def decorated_function(*args, **kwargs): - _request_ctx_stack.top.current_user = {'sub': 'test'} - return f(*args, **kwargs) - return decorated_function - - -def requires_scope_mock(required_scope): - def decorator(f): - @wraps(f) - def decorated_function(*args, **kwargs): - return f(*args, **kwargs) - return decorated_function - return decorator - - -@pytest.fixture -def client(): - """Returns a Flask API client to interact with.""" - - # Disable authorization for test API endpoints - from opendc import exts - exts.requires_auth = requires_auth_mock - exts.requires_scope = requires_scope_mock - exts.has_scope = lambda x: False - - from app import create_app - - app = create_app(testing=True) - - with app.app_context(): - g.db = Database() - with app.test_client() as client: - yield client diff --git a/opendc-web/opendc-web-api/docs/component-diagram.png b/opendc-web/opendc-web-api/docs/component-diagram.png Binary files differdeleted file mode 100644 index 91b26006..00000000 --- a/opendc-web/opendc-web-api/docs/component-diagram.png +++ /dev/null diff --git a/opendc-web/opendc-web-api/format.sh b/opendc-web/opendc-web-api/format.sh deleted file mode 100755 index 18cba452..00000000 --- a/opendc-web/opendc-web-api/format.sh +++ /dev/null @@ -1 +0,0 @@ -yapf **/*.py -i 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) diff --git a/opendc-web/opendc-web-api/pytest.ini b/opendc-web/opendc-web-api/pytest.ini deleted file mode 100644 index 8e7964ba..00000000 --- a/opendc-web/opendc-web-api/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -env = - OPENDC_FLASK_TESTING=True - OPENDC_FLASK_SECRET=Secret -junit_family = xunit2 diff --git a/opendc-web/opendc-web-api/requirements.txt b/opendc-web/opendc-web-api/requirements.txt deleted file mode 100644 index 6f3b42aa..00000000 --- a/opendc-web/opendc-web-api/requirements.txt +++ /dev/null @@ -1,47 +0,0 @@ -astroid==2.4.2 -attrs==20.3.0 -blinker==1.4 -Brotli==1.0.9 -certifi==2020.11.8 -click==7.1.2 -eventlet==0.31.0 -Flask==1.1.2 -Flask-Compress==1.5.0 -Flask-Cors==3.0.9 -Flask-SocketIO==4.3.1 -flask-swagger-ui==3.36.0 -Flask-Restful==0.3.8 -httplib2==0.19.0 -isort==4.3.21 -itsdangerous==1.1.0 -Jinja2==2.11.3 -lazy-object-proxy==1.4.3 -MarkupSafe==1.1.1 -marshmallow==3.12.1 -mccabe==0.6.1 -monotonic==1.5 -more-itertools==8.6.0 -oauth2client==4.1.3 -packaging==20.4 -pluggy==0.13.1 -py==1.10.0 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -pylint==2.5.3 -pymongo==3.10.1 -pyparsing==2.4.7 -pytest==5.4.3 -pytest-cov==2.11.1 -pytest-env==0.6.2 -pytest-mock==3.2.0 -python-dotenv==0.14.0 -python-jose==3.2.0 -rsa==4.7 -sentry-sdk==0.19.2 -six==1.15.0 -toml==0.10.2 -urllib3==1.26.5 -wcwidth==0.2.5 -Werkzeug==1.0.1 -wrapt==1.12.1 -yapf==0.30.0 diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/OpenDCApplication.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/OpenDCApplication.kt new file mode 100644 index 00000000..ddbd5390 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/OpenDCApplication.kt @@ -0,0 +1,30 @@ +/* + * 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. + */ + +package org.opendc.web.api + +import javax.ws.rs.core.Application + +/** + * [Application] definition for the OpenDC web API. + */ +class OpenDCApplication : Application() diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt new file mode 100644 index 00000000..23838e34 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.model + +import io.quarkiverse.hibernate.types.json.JsonBinaryType +import io.quarkiverse.hibernate.types.json.JsonTypes +import org.hibernate.annotations.Type +import org.hibernate.annotations.TypeDef +import org.opendc.web.proto.JobState +import java.time.Instant +import javax.persistence.* + +/** + * A simulation job to be run by the simulator. + */ +@TypeDef(name = JsonTypes.JSON_BIN, typeClass = JsonBinaryType::class) +@Entity +@Table(name = "jobs") +@NamedQueries( + value = [ + NamedQuery( + name = "Job.findAll", + query = "SELECT j FROM Job j WHERE j.state = :state" + ), + NamedQuery( + name = "Job.updateOne", + query = """ + UPDATE Job j + SET j.state = :newState, j.updatedAt = :updatedAt, j.results = :results + WHERE j.id = :id AND j.state = :oldState + """ + ) + ] +) +class Job( + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + val id: Long, + + @OneToOne(optional = false, mappedBy = "job", fetch = FetchType.EAGER) + @JoinColumn(name = "scenario_id", nullable = false) + val scenario: Scenario, + + @Column(name = "created_at", nullable = false, updatable = false) + val createdAt: Instant, + + /** + * The number of simulation runs to perform. + */ + @Column(nullable = false, updatable = false) + val repeats: Int +) { + /** + * The instant at which the job was updated. + */ + @Column(name = "updated_at", nullable = false) + var updatedAt: Instant = createdAt + + /** + * The state of the job. + */ + @Column(nullable = false) + var state: JobState = JobState.PENDING + + /** + * Experiment results in JSON + */ + @Type(type = JsonTypes.JSON_BIN) + @Column(columnDefinition = JsonTypes.JSON_BIN) + var results: Map<String, Any>? = null + + /** + * Return a string representation of this job. + */ + override fun toString(): String = "Job[id=$id,scenario=${scenario.id},state=$state]" +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Portfolio.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Portfolio.kt new file mode 100644 index 00000000..c8b94daf --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Portfolio.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.model + +import org.hibernate.annotations.Type +import org.hibernate.annotations.TypeDef +import org.opendc.web.api.util.hibernate.json.JsonType +import org.opendc.web.proto.Targets +import javax.persistence.* + +/** + * A portfolio is the composition of multiple scenarios. + */ +@TypeDef(name = "json", typeClass = JsonType::class) +@Entity +@Table( + name = "portfolios", + uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])], + indexes = [Index(name = "fn_portfolios_number", columnList = "project_id, number")] +) +@NamedQueries( + value = [ + NamedQuery( + name = "Portfolio.findAll", + query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId" + ), + NamedQuery( + name = "Portfolio.findOne", + query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId AND p.number = :number" + ) + ] +) +class Portfolio( + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + val id: Long, + + /** + * Unique number of the portfolio for the project. + */ + @Column(nullable = false) + val number: Int, + + @Column(nullable = false) + val name: String, + + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false) + val project: Project, + + /** + * The portfolio targets (metrics, repetitions). + */ + @Type(type = "json") + @Column(columnDefinition = "jsonb", nullable = false, updatable = false) + val targets: Targets, +) { + /** + * The scenarios in this portfolio. + */ + @OneToMany(cascade = [CascadeType.ALL], mappedBy = "portfolio", orphanRemoval = true) + @OrderBy("id ASC") + val scenarios: MutableSet<Scenario> = mutableSetOf() + + /** + * Return a string representation of this portfolio. + */ + override fun toString(): String = "Job[id=$id,name=$name,project=${project.id},targets=$targets]" +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Project.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Project.kt new file mode 100644 index 00000000..e0440bf4 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Project.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.model + +import java.time.Instant +import javax.persistence.* + +/** + * A project in OpenDC encapsulates all the datacenter designs and simulation runs for a set of users. + */ +@Entity +@Table(name = "projects") +@NamedQueries( + value = [ + NamedQuery( + name = "Project.findAll", + query = """ + SELECT a + FROM ProjectAuthorization a + WHERE a.key.userId = :userId + """ + ), + NamedQuery( + name = "Project.allocatePortfolio", + query = """ + UPDATE Project p + SET p.portfoliosCreated = :oldState + 1, p.updatedAt = :now + WHERE p.id = :id AND p.portfoliosCreated = :oldState + """ + ), + NamedQuery( + name = "Project.allocateTopology", + query = """ + UPDATE Project p + SET p.topologiesCreated = :oldState + 1, p.updatedAt = :now + WHERE p.id = :id AND p.topologiesCreated = :oldState + """ + ), + NamedQuery( + name = "Project.allocateScenario", + query = """ + UPDATE Project p + SET p.scenariosCreated = :oldState + 1, p.updatedAt = :now + WHERE p.id = :id AND p.scenariosCreated = :oldState + """ + ) + ] +) +class Project( + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + val id: Long, + + @Column(nullable = false) + var name: String, + + @Column(name = "created_at", nullable = false, updatable = false) + val createdAt: Instant, +) { + /** + * The instant at which the project was updated. + */ + @Column(name = "updated_at", nullable = false) + var updatedAt: Instant = createdAt + + /** + * The portfolios belonging to this project. + */ + @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true) + @OrderBy("id ASC") + val portfolios: MutableSet<Portfolio> = mutableSetOf() + + /** + * The number of portfolios created for this project (including deleted portfolios). + */ + @Column(name = "portfolios_created", nullable = false) + var portfoliosCreated: Int = 0 + + /** + * The topologies belonging to this project. + */ + @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true) + @OrderBy("id ASC") + val topologies: MutableSet<Topology> = mutableSetOf() + + /** + * The number of topologies created for this project (including deleted topologies). + */ + @Column(name = "topologies_created", nullable = false) + var topologiesCreated: Int = 0 + + /** + * The scenarios belonging to this project. + */ + @OneToMany(mappedBy = "project", orphanRemoval = true) + val scenarios: MutableSet<Scenario> = mutableSetOf() + + /** + * The number of scenarios created for this project (including deleted scenarios). + */ + @Column(name = "scenarios_created", nullable = false) + var scenariosCreated: Int = 0 + + /** + * The users authorized to access the project. + */ + @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true) + val authorizations: MutableSet<ProjectAuthorization> = mutableSetOf() + + /** + * Return a string representation of this project. + */ + override fun toString(): String = "Project[id=$id,name=$name]" +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorization.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorization.kt new file mode 100644 index 00000000..a72ff06a --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorization.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.model + +import org.opendc.web.proto.user.ProjectRole +import javax.persistence.* + +/** + * An authorization for some user to participate in a project. + */ +@Entity +@Table(name = "project_authorizations") +class ProjectAuthorization( + /** + * The user identifier of the authorization. + */ + @EmbeddedId + val key: ProjectAuthorizationKey, + + /** + * The project that the user is authorized to participate in. + */ + @ManyToOne(optional = false) + @MapsId("projectId") + @JoinColumn(name = "project_id", updatable = false, insertable = false, nullable = false) + val project: Project, + + /** + * The role of the user in the project. + */ + @Column(nullable = false) + val role: ProjectRole +) { + /** + * Return a string representation of this project authorization. + */ + override fun toString(): String = "ProjectAuthorization[project=${key.projectId},user=${key.userId},role=$role]" +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorizationKey.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorizationKey.kt new file mode 100644 index 00000000..b5f66e70 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/ProjectAuthorizationKey.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.model + +import javax.persistence.Column +import javax.persistence.Embeddable + +/** + * Key for representing a [ProjectAuthorization] object. + */ +@Embeddable +data class ProjectAuthorizationKey( + @Column(name = "user_id", nullable = false) + val userId: String, + + @Column(name = "project_id", nullable = false) + val projectId: Long +) : java.io.Serializable diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt new file mode 100644 index 00000000..9a383c7c --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.model + +import io.quarkiverse.hibernate.types.json.JsonBinaryType +import io.quarkiverse.hibernate.types.json.JsonTypes +import org.hibernate.annotations.Type +import org.hibernate.annotations.TypeDef +import org.opendc.web.proto.OperationalPhenomena +import javax.persistence.* + +/** + * A single scenario to be explored by the simulator. + */ +@TypeDef(name = JsonTypes.JSON_BIN, typeClass = JsonBinaryType::class) +@Entity +@Table( + name = "scenarios", + uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])], + indexes = [Index(name = "fn_scenarios_number", columnList = "project_id, number")] +) +@NamedQueries( + value = [ + NamedQuery( + name = "Scenario.findAll", + query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId" + ), + NamedQuery( + name = "Scenario.findAllForPortfolio", + query = """ + SELECT s + FROM Scenario s + JOIN Portfolio p ON p.id = s.portfolio.id AND p.number = :number + WHERE s.project.id = :projectId + """ + ), + NamedQuery( + name = "Scenario.findOne", + query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId AND s.number = :number" + ) + ] +) +class Scenario( + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + val id: Long, + + /** + * Unique number of the scenario for the project. + */ + @Column(nullable = false) + val number: Int, + + @Column(nullable = false, updatable = false) + val name: String, + + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false) + val project: Project, + + @ManyToOne(optional = false) + @JoinColumn(name = "portfolio_id", nullable = false) + val portfolio: Portfolio, + + @Embedded + val workload: Workload, + + @ManyToOne(optional = false) + val topology: Topology, + + @Type(type = JsonTypes.JSON_BIN) + @Column(columnDefinition = JsonTypes.JSON_BIN, nullable = false, updatable = false) + val phenomena: OperationalPhenomena, + + @Column(name = "scheduler_name", nullable = false, updatable = false) + val schedulerName: String, +) { + /** + * The [Job] associated with the scenario. + */ + @OneToOne(cascade = [CascadeType.ALL]) + lateinit var job: Job + + /** + * Return a string representation of this scenario. + */ + override fun toString(): String = "Scenario[id=$id,name=$name,project=${project.id},portfolio=${portfolio.id}]" +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt new file mode 100644 index 00000000..32bf799a --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.model + +import io.quarkiverse.hibernate.types.json.JsonBinaryType +import io.quarkiverse.hibernate.types.json.JsonTypes +import org.hibernate.annotations.Type +import org.hibernate.annotations.TypeDef +import org.opendc.web.proto.Room +import java.time.Instant +import javax.persistence.* + +/** + * A datacenter design in OpenDC. + */ +@TypeDef(name = JsonTypes.JSON_BIN, typeClass = JsonBinaryType::class) +@Entity +@Table( + name = "topologies", + uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])], + indexes = [Index(name = "fn_topologies_number", columnList = "project_id, number")] +) +@NamedQueries( + value = [ + NamedQuery( + name = "Topology.findAll", + query = "SELECT t FROM Topology t WHERE t.project.id = :projectId" + ), + NamedQuery( + name = "Topology.findOne", + query = "SELECT t FROM Topology t WHERE t.project.id = :projectId AND t.number = :number" + ) + ] +) +class Topology( + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + val id: Long, + + /** + * Unique number of the topology for the project. + */ + @Column(nullable = false) + val number: Int, + + @Column(nullable = false) + val name: String, + + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false) + val project: Project, + + @Column(name = "created_at", nullable = false, updatable = false) + val createdAt: Instant, + + /** + * Datacenter design in JSON + */ + @Type(type = JsonTypes.JSON_BIN) + @Column(columnDefinition = JsonTypes.JSON_BIN, nullable = false) + var rooms: List<Room> = emptyList() +) { + /** + * The instant at which the topology was updated. + */ + @Column(name = "updated_at", nullable = false) + var updatedAt: Instant = createdAt + + /** + * Return a string representation of this topology. + */ + override fun toString(): String = "Topology[id=$id,name=$name,project=${project.id}]" +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Trace.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Trace.kt new file mode 100644 index 00000000..2e2d71f8 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Trace.kt @@ -0,0 +1,58 @@ +/* + * 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. + */ + +package org.opendc.web.api.model + +import javax.persistence.* + +/** + * A workload trace available for simulation. + * + * @param id The unique identifier of the trace. + * @param name The name of the trace. + * @param type The type of trace. + */ +@Entity +@Table(name = "traces") +@NamedQueries( + value = [ + NamedQuery( + name = "Trace.findAll", + query = "SELECT t FROM Trace t" + ), + ] +) +class Trace( + @Id + val id: String, + + @Column(nullable = false, updatable = false) + val name: String, + + @Column(nullable = false, updatable = false) + val type: String, +) { + /** + * Return a string representation of this trace. + */ + override fun toString(): String = "Trace[id=$id,name=$name,type=$type]" +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Workload.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Workload.kt new file mode 100644 index 00000000..07fc096b --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Workload.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.model + +import javax.persistence.Column +import javax.persistence.Embeddable +import javax.persistence.ManyToOne + +/** + * Specification of the workload for a [Scenario]. + */ +@Embeddable +class Workload( + @ManyToOne(optional = false) + val trace: Trace, + + @Column(name = "sampling_fraction", nullable = false, updatable = false) + val samplingFraction: Double +) diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/JobRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/JobRepository.kt new file mode 100644 index 00000000..558d7c38 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/JobRepository.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.repository + +import org.opendc.web.api.model.Job +import org.opendc.web.proto.JobState +import java.time.Instant +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject +import javax.persistence.EntityManager + +/** + * A repository to manage [Job] entities. + */ +@ApplicationScoped +class JobRepository @Inject constructor(private val em: EntityManager) { + /** + * Find all jobs currently residing in [state]. + * + * @param state The state in which the jobs should be. + * @return The list of jobs in state [state]. + */ + fun findAll(state: JobState): List<Job> { + return em.createNamedQuery("Job.findAll", Job::class.java) + .setParameter("state", state) + .resultList + } + + /** + * Find the [Job] with the specified [id]. + * + * @param id The unique identifier of the job. + * @return The trace or `null` if it does not exist. + */ + fun findOne(id: Long): Job? { + return em.find(Job::class.java, id) + } + + /** + * Delete the specified [job]. + */ + fun delete(job: Job) { + em.remove(job) + } + + /** + * Save the specified [job] to the database. + */ + fun save(job: Job) { + em.persist(job) + } + + /** + * Atomically update the specified [job]. + * + * @param job The job to update atomically. + * @param newState The new state to enter into. + * @param time The time at which the update occurs. + * @param results The results to possible set. + * @return `true` when the update succeeded`, `false` when there was a conflict. + */ + fun updateOne(job: Job, newState: JobState, time: Instant, results: Map<String, Any>?): Boolean { + val count = em.createNamedQuery("Job.updateOne") + .setParameter("id", job.id) + .setParameter("oldState", job.state) + .setParameter("newState", newState) + .setParameter("updatedAt", Instant.now()) + .setParameter("results", results) + .executeUpdate() + em.refresh(job) + return count > 0 + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/PortfolioRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/PortfolioRepository.kt new file mode 100644 index 00000000..34b3598c --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/PortfolioRepository.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.repository + +import org.opendc.web.api.model.Portfolio +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject +import javax.persistence.EntityManager + +/** + * A repository to manage [Portfolio] entities. + */ +@ApplicationScoped +class PortfolioRepository @Inject constructor(private val em: EntityManager) { + /** + * Find all [Portfolio]s that belong to [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @return The list of portfolios that belong to the specified project. + */ + fun findAll(projectId: Long): List<Portfolio> { + return em.createNamedQuery("Portfolio.findAll", Portfolio::class.java) + .setParameter("projectId", projectId) + .resultList + } + + /** + * Find the [Portfolio] with the specified [number] belonging to [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @param number The number of the portfolio. + * @return The portfolio or `null` if it does not exist. + */ + fun findOne(projectId: Long, number: Int): Portfolio? { + return em.createNamedQuery("Portfolio.findOne", Portfolio::class.java) + .setParameter("projectId", projectId) + .setParameter("number", number) + .setMaxResults(1) + .resultList + .firstOrNull() + } + + /** + * Delete the specified [portfolio]. + */ + fun delete(portfolio: Portfolio) { + em.remove(portfolio) + } + + /** + * Save the specified [portfolio] to the database. + */ + fun save(portfolio: Portfolio) { + em.persist(portfolio) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ProjectRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ProjectRepository.kt new file mode 100644 index 00000000..6529f778 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ProjectRepository.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.repository + +import org.opendc.web.api.model.Project +import org.opendc.web.api.model.ProjectAuthorization +import org.opendc.web.api.model.ProjectAuthorizationKey +import java.time.Instant +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject +import javax.persistence.EntityManager + +/** + * A repository to manage [Project] entities. + */ +@ApplicationScoped +class ProjectRepository @Inject constructor(private val em: EntityManager) { + /** + * List all projects for the user with the specified [userId]. + * + * @param userId The identifier of the user that is requesting the list of projects. + * @return A list of projects that the user has received authorization for. + */ + fun findAll(userId: String): List<ProjectAuthorization> { + return em.createNamedQuery("Project.findAll", ProjectAuthorization::class.java) + .setParameter("userId", userId) + .resultList + } + + /** + * Find the project with [id] for the user with the specified [userId]. + * + * @param userId The identifier of the user that is requesting the list of projects. + * @param id The unique identifier of the project. + * @return The project with the specified identifier or `null` if it does not exist or is not accessible to the + * user with the specified identifier. + */ + fun findOne(userId: String, id: Long): ProjectAuthorization? { + return em.find(ProjectAuthorization::class.java, ProjectAuthorizationKey(userId, id)) + } + + /** + * Delete the specified [project]. + */ + fun delete(project: Project) { + em.remove(project) + } + + /** + * Save the specified [project] to the database. + */ + fun save(project: Project) { + em.persist(project) + } + + /** + * Save the specified [auth] to the database. + */ + fun save(auth: ProjectAuthorization) { + em.persist(auth) + } + + /** + * Allocate the next portfolio number for the specified [project]. + * + * @param project The project to allocate the portfolio number for. + * @param time The time at which the new portfolio is created. + * @param tries The number of times to try to allocate the number before failing. + */ + fun allocatePortfolio(project: Project, time: Instant, tries: Int = 4): Int { + repeat(tries) { + val count = em.createNamedQuery("Project.allocatePortfolio") + .setParameter("id", project.id) + .setParameter("oldState", project.portfoliosCreated) + .setParameter("now", time) + .executeUpdate() + + if (count > 0) { + return project.portfoliosCreated + 1 + } else { + em.refresh(project) + } + } + + throw IllegalStateException("Failed to allocate next portfolio") + } + + /** + * Allocate the next topology number for the specified [project]. + * + * @param project The project to allocate the topology number for. + * @param time The time at which the new topology is created. + * @param tries The number of times to try to allocate the number before failing. + */ + fun allocateTopology(project: Project, time: Instant, tries: Int = 4): Int { + repeat(tries) { + val count = em.createNamedQuery("Project.allocateTopology") + .setParameter("id", project.id) + .setParameter("oldState", project.topologiesCreated) + .setParameter("now", time) + .executeUpdate() + + if (count > 0) { + return project.topologiesCreated + 1 + } else { + em.refresh(project) + } + } + + throw IllegalStateException("Failed to allocate next topology") + } + + /** + * Allocate the next scenario number for the specified [project]. + * + * @param project The project to allocate the scenario number for. + * @param time The time at which the new scenario is created. + * @param tries The number of times to try to allocate the number before failing. + */ + fun allocateScenario(project: Project, time: Instant, tries: Int = 4): Int { + repeat(tries) { + val count = em.createNamedQuery("Project.allocateScenario") + .setParameter("id", project.id) + .setParameter("oldState", project.scenariosCreated) + .setParameter("now", time) + .executeUpdate() + + if (count > 0) { + return project.scenariosCreated + 1 + } else { + em.refresh(project) + } + } + + throw IllegalStateException("Failed to allocate next scenario") + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ScenarioRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ScenarioRepository.kt new file mode 100644 index 00000000..de116ad6 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/ScenarioRepository.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.repository + +import org.opendc.web.api.model.Scenario +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject +import javax.persistence.EntityManager + +/** + * A repository to manage [Scenario] entities. + */ +@ApplicationScoped +class ScenarioRepository @Inject constructor(private val em: EntityManager) { + /** + * Find all [Scenario]s that belong to [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @return The list of scenarios that belong to the specified project. + */ + fun findAll(projectId: Long): List<Scenario> { + return em.createNamedQuery("Scenario.findAll", Scenario::class.java) + .setParameter("projectId", projectId) + .resultList + } + + /** + * Find all [Scenario]s that belong to [portfolio][number] of [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @param number The number of the portfolio to which the scenarios should belong. + * @return The list of scenarios that belong to the specified portfolio. + */ + fun findAll(projectId: Long, number: Int): List<Scenario> { + return em.createNamedQuery("Scenario.findAllForPortfolio", Scenario::class.java) + .setParameter("projectId", projectId) + .setParameter("number", number) + .resultList + } + + /** + * Find the [Scenario] with the specified [number] belonging to [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @param number The number of the scenario. + * @return The scenario or `null` if it does not exist. + */ + fun findOne(projectId: Long, number: Int): Scenario? { + return em.createNamedQuery("Scenario.findOne", Scenario::class.java) + .setParameter("projectId", projectId) + .setParameter("number", number) + .setMaxResults(1) + .resultList + .firstOrNull() + } + + /** + * Delete the specified [scenario]. + */ + fun delete(scenario: Scenario) { + em.remove(scenario) + } + + /** + * Save the specified [scenario] to the database. + */ + fun save(scenario: Scenario) { + em.persist(scenario) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TopologyRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TopologyRepository.kt new file mode 100644 index 00000000..cd8f666e --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TopologyRepository.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.repository + +import org.opendc.web.api.model.Topology +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject +import javax.persistence.EntityManager + +/** + * A repository to manage [Topology] entities. + */ +@ApplicationScoped +class TopologyRepository @Inject constructor(private val em: EntityManager) { + /** + * Find all [Topology]s that belong to [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @return The list of topologies that belong to the specified project. + */ + fun findAll(projectId: Long): List<Topology> { + return em.createNamedQuery("Topology.findAll", Topology::class.java) + .setParameter("projectId", projectId) + .resultList + } + + /** + * Find the [Topology] with the specified [number] belonging to [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @param number The number of the topology. + * @return The topology or `null` if it does not exist. + */ + fun findOne(projectId: Long, number: Int): Topology? { + return em.createNamedQuery("Topology.findOne", Topology::class.java) + .setParameter("projectId", projectId) + .setParameter("number", number) + .setMaxResults(1) + .resultList + .firstOrNull() + } + + /** + * Find the [Topology] with the specified [id]. + * + * @param id Unique identifier of the topology. + * @return The topology or `null` if it does not exist. + */ + fun findOne(id: Long): Topology? { + return em.find(Topology::class.java, id) + } + + /** + * Delete the specified [topology]. + */ + fun delete(topology: Topology) { + em.remove(topology) + } + + /** + * Save the specified [topology] to the database. + */ + fun save(topology: Topology) { + em.persist(topology) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TraceRepository.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TraceRepository.kt new file mode 100644 index 00000000..6652fc80 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/repository/TraceRepository.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.repository + +import org.opendc.web.api.model.Trace +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject +import javax.persistence.EntityManager + +/** + * A repository to manage [Trace] entities. + */ +@ApplicationScoped +class TraceRepository @Inject constructor(private val em: EntityManager) { + /** + * Find all workload traces in the database. + * + * @return The list of available workload traces. + */ + fun findAll(): List<Trace> { + return em.createNamedQuery("Trace.findAll", Trace::class.java).resultList + } + + /** + * Find the [Trace] with the specified [id]. + * + * @param id The unique identifier of the trace. + * @return The trace or `null` if it does not exist. + */ + fun findOne(id: String): Trace? { + return em.find(Trace::class.java, id) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/SchedulerResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/SchedulerResource.kt new file mode 100644 index 00000000..735fdd9b --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/SchedulerResource.kt @@ -0,0 +1,48 @@ +/* + * 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. + */ + +package org.opendc.web.api.rest + +import javax.ws.rs.GET +import javax.ws.rs.Path + +/** + * A resource representing the available schedulers that can be used during experiments. + */ +@Path("/schedulers") +class SchedulerResource { + /** + * Obtain all available schedulers. + */ + @GET + fun getAll() = listOf( + "mem", + "mem-inv", + "core-mem", + "core-mem-inv", + "active-servers", + "active-servers-inv", + "provisioned-cores", + "provisioned-cores-inv", + "random" + ) +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/TraceResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/TraceResource.kt new file mode 100644 index 00000000..e87fe602 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/TraceResource.kt @@ -0,0 +1,51 @@ +/* + * 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. + */ + +package org.opendc.web.api.rest + +import org.opendc.web.api.service.TraceService +import org.opendc.web.proto.Trace +import javax.inject.Inject +import javax.ws.rs.* + +/** + * A resource representing the workload traces available in the OpenDC instance. + */ +@Path("/traces") +class TraceResource @Inject constructor(private val traceService: TraceService) { + /** + * Obtain all available traces. + */ + @GET + fun getAll(): List<Trace> { + return traceService.findAll() + } + + /** + * Obtain trace information by identifier. + */ + @GET + @Path("{id}") + fun get(@PathParam("id") id: String): Trace { + return traceService.findById(id) ?: throw WebApplicationException("Trace not found", 404) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/GenericExceptionMapper.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/GenericExceptionMapper.kt new file mode 100644 index 00000000..fb253758 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/GenericExceptionMapper.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.error + +import org.opendc.web.proto.ProtocolError +import javax.ws.rs.WebApplicationException +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.ext.ExceptionMapper +import javax.ws.rs.ext.Provider + +/** + * Helper class to transform an exception into an JSON error response. + */ +@Provider +class GenericExceptionMapper : ExceptionMapper<Exception> { + override fun toResponse(exception: Exception): Response { + val code = if (exception is WebApplicationException) exception.response.status else 500 + + return Response.status(code) + .entity(ProtocolError(code, exception.message ?: "Unknown error")) + .type(MediaType.APPLICATION_JSON) + .build() + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/MissingKotlinParameterExceptionMapper.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/MissingKotlinParameterExceptionMapper.kt new file mode 100644 index 00000000..57cd35d1 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/error/MissingKotlinParameterExceptionMapper.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.error + +import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException +import org.opendc.web.proto.ProtocolError +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.ext.ExceptionMapper +import javax.ws.rs.ext.Provider + +/** + * An [ExceptionMapper] for [MissingKotlinParameterException] thrown by Jackson. + */ +@Provider +class MissingKotlinParameterExceptionMapper : ExceptionMapper<MissingKotlinParameterException> { + override fun toResponse(exception: MissingKotlinParameterException): Response { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ProtocolError(Response.Status.BAD_REQUEST.statusCode, "Field '${exception.parameter.name}' is missing from body.")) + .type(MediaType.APPLICATION_JSON) + .build() + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/runner/JobResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/runner/JobResource.kt new file mode 100644 index 00000000..7e31e2c5 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/runner/JobResource.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.runner + +import org.opendc.web.api.service.JobService +import org.opendc.web.proto.runner.Job +import javax.annotation.security.RolesAllowed +import javax.inject.Inject +import javax.transaction.Transactional +import javax.validation.Valid +import javax.ws.rs.* + +/** + * A resource representing the available simulation jobs. + */ +@Path("/jobs") +@RolesAllowed("runner") +class JobResource @Inject constructor(private val jobService: JobService) { + /** + * Obtain all pending simulation jobs. + */ + @GET + fun queryPending(): List<Job> { + return jobService.queryPending() + } + + /** + * Get a job by identifier. + */ + @GET + @Path("{job}") + fun get(@PathParam("job") id: Long): Job { + return jobService.findById(id) ?: throw WebApplicationException("Job not found", 404) + } + + /** + * Atomically update the state of a job. + */ + @POST + @Path("{job}") + @Transactional + fun update(@PathParam("job") id: Long, @Valid update: Job.Update): Job { + return jobService.updateState(id, update.state, update.results) ?: throw WebApplicationException("Job not found", 404) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioResource.kt new file mode 100644 index 00000000..e720de75 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioResource.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.user + +import io.quarkus.security.identity.SecurityIdentity +import org.opendc.web.api.service.PortfolioService +import org.opendc.web.proto.user.Portfolio +import javax.annotation.security.RolesAllowed +import javax.inject.Inject +import javax.transaction.Transactional +import javax.validation.Valid +import javax.ws.rs.* + +/** + * A resource representing the portfolios of a project. + */ +@Path("/projects/{project}/portfolios") +@RolesAllowed("openid") +class PortfolioResource @Inject constructor( + private val portfolioService: PortfolioService, + private val identity: SecurityIdentity, +) { + /** + * Get all portfolios that belong to the specified project. + */ + @GET + fun getAll(@PathParam("project") projectId: Long): List<Portfolio> { + return portfolioService.findAll(identity.principal.name, projectId) + } + + /** + * Create a portfolio for this project. + */ + @POST + @Transactional + fun create(@PathParam("project") projectId: Long, @Valid request: Portfolio.Create): Portfolio { + return portfolioService.create(identity.principal.name, projectId, request) ?: throw WebApplicationException("Project not found", 404) + } + + /** + * Obtain a portfolio by its identifier. + */ + @GET + @Path("{portfolio}") + fun get(@PathParam("project") projectId: Long, @PathParam("portfolio") number: Int): Portfolio { + return portfolioService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Portfolio not found", 404) + } + + /** + * Delete a portfolio. + */ + @DELETE + @Path("{portfolio}") + fun delete(@PathParam("project") projectId: Long, @PathParam("portfolio") number: Int): Portfolio { + return portfolioService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Portfolio not found", 404) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResource.kt new file mode 100644 index 00000000..8d24b2eb --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResource.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.user + +import io.quarkus.security.identity.SecurityIdentity +import org.opendc.web.api.service.ScenarioService +import org.opendc.web.proto.user.Scenario +import javax.annotation.security.RolesAllowed +import javax.inject.Inject +import javax.transaction.Transactional +import javax.validation.Valid +import javax.ws.rs.* + +/** + * A resource representing the scenarios of a portfolio. + */ +@Path("/projects/{project}/portfolios/{portfolio}/scenarios") +@RolesAllowed("openid") +class PortfolioScenarioResource @Inject constructor( + private val scenarioService: ScenarioService, + private val identity: SecurityIdentity, +) { + /** + * Get all scenarios that belong to the specified portfolio. + */ + @GET + fun get(@PathParam("project") projectId: Long, @PathParam("portfolio") portfolioNumber: Int): List<Scenario> { + return scenarioService.findAll(identity.principal.name, projectId, portfolioNumber) + } + + /** + * Create a scenario for this portfolio. + */ + @POST + @Transactional + fun create(@PathParam("project") projectId: Long, @PathParam("portfolio") portfolioNumber: Int, @Valid request: Scenario.Create): Scenario { + return scenarioService.create(identity.principal.name, projectId, portfolioNumber, request) ?: throw WebApplicationException("Portfolio not found", 404) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ProjectResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ProjectResource.kt new file mode 100644 index 00000000..a27d50e7 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ProjectResource.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.user + +import io.quarkus.security.identity.SecurityIdentity +import org.opendc.web.api.service.ProjectService +import org.opendc.web.proto.user.Project +import javax.annotation.security.RolesAllowed +import javax.inject.Inject +import javax.transaction.Transactional +import javax.validation.Valid +import javax.ws.rs.* + +/** + * A resource representing the created projects. + */ +@Path("/projects") +@RolesAllowed("openid") +class ProjectResource @Inject constructor( + private val projectService: ProjectService, + private val identity: SecurityIdentity +) { + /** + * Obtain all the projects of the current user. + */ + @GET + fun getAll(): List<Project> { + return projectService.findWithUser(identity.principal.name) + } + + /** + * Create a new project for the current user. + */ + @POST + @Transactional + fun create(@Valid request: Project.Create): Project { + return projectService.createForUser(identity.principal.name, request.name) + } + + /** + * Obtain a single project by its identifier. + */ + @GET + @Path("{project}") + fun get(@PathParam("project") id: Long): Project { + return projectService.findWithUser(identity.principal.name, id) ?: throw WebApplicationException("Project not found", 404) + } + + /** + * Delete a project. + */ + @DELETE + @Path("{project}") + @Transactional + fun delete(@PathParam("project") id: Long): Project { + try { + return projectService.deleteWithUser(identity.principal.name, id) ?: throw WebApplicationException("Project not found", 404) + } catch (e: IllegalArgumentException) { + throw WebApplicationException(e.message, 403) + } + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ScenarioResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ScenarioResource.kt new file mode 100644 index 00000000..3690f987 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/ScenarioResource.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.user + +import io.quarkus.security.identity.SecurityIdentity +import org.opendc.web.api.service.ScenarioService +import org.opendc.web.proto.user.Scenario +import javax.annotation.security.RolesAllowed +import javax.inject.Inject +import javax.transaction.Transactional +import javax.ws.rs.* + +/** + * A resource representing the scenarios of a portfolio. + */ +@Path("/projects/{project}/scenarios") +@RolesAllowed("openid") +class ScenarioResource @Inject constructor( + private val scenarioService: ScenarioService, + private val identity: SecurityIdentity +) { + /** + * Obtain a scenario by its identifier. + */ + @GET + @Path("{scenario}") + fun get(@PathParam("project") projectId: Long, @PathParam("scenario") number: Int): Scenario { + return scenarioService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Scenario not found", 404) + } + + /** + * Delete a scenario. + */ + @DELETE + @Path("{scenario}") + @Transactional + fun delete(@PathParam("project") projectId: Long, @PathParam("scenario") number: Int): Scenario { + return scenarioService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Scenario not found", 404) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/TopologyResource.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/TopologyResource.kt new file mode 100644 index 00000000..52c5eaaa --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/rest/user/TopologyResource.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.user + +import io.quarkus.security.identity.SecurityIdentity +import org.opendc.web.api.service.TopologyService +import org.opendc.web.proto.user.Topology +import javax.annotation.security.RolesAllowed +import javax.inject.Inject +import javax.transaction.Transactional +import javax.validation.Valid +import javax.ws.rs.* + +/** + * A resource representing the constructed datacenter topologies. + */ +@Path("/projects/{project}/topologies") +@RolesAllowed("openid") +class TopologyResource @Inject constructor( + private val topologyService: TopologyService, + private val identity: SecurityIdentity +) { + /** + * Get all topologies that belong to the specified project. + */ + @GET + fun getAll(@PathParam("project") projectId: Long): List<Topology> { + return topologyService.findAll(identity.principal.name, projectId) + } + + /** + * Create a topology for this project. + */ + @POST + @Transactional + fun create(@PathParam("project") projectId: Long, @Valid request: Topology.Create): Topology { + return topologyService.create(identity.principal.name, projectId, request) ?: throw WebApplicationException("Topology not found", 404) + } + + /** + * Obtain a topology by its number. + */ + @GET + @Path("{topology}") + fun get(@PathParam("project") projectId: Long, @PathParam("topology") number: Int): Topology { + return topologyService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Topology not found", 404) + } + + /** + * Update the specified topology by its number. + */ + @PUT + @Path("{topology}") + @Transactional + fun update(@PathParam("project") projectId: Long, @PathParam("topology") number: Int, @Valid request: Topology.Update): Topology { + return topologyService.update(identity.principal.name, projectId, number, request) ?: throw WebApplicationException("Topology not found", 404) + } + + /** + * Delete the specified topology. + */ + @Path("{topology}") + @DELETE + @Transactional + fun delete(@PathParam("project") projectId: Long, @PathParam("topology") number: Int): Topology { + return topologyService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Topology not found", 404) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/JobService.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/JobService.kt new file mode 100644 index 00000000..1b33248d --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/JobService.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.service + +import org.opendc.web.api.repository.JobRepository +import org.opendc.web.proto.JobState +import org.opendc.web.proto.runner.Job +import java.time.Instant +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject + +/** + * Service for managing [Job]s. + */ +@ApplicationScoped +class JobService @Inject constructor(private val repository: JobRepository) { + /** + * Query the pending simulation jobs. + */ + fun queryPending(): List<Job> { + return repository.findAll(JobState.PENDING).map { it.toRunnerDto() } + } + + /** + * Find a job by its identifier. + */ + fun findById(id: Long): Job? { + return repository.findOne(id)?.toRunnerDto() + } + + /** + * Atomically update the state of a [Job]. + */ + fun updateState(id: Long, newState: JobState, results: Map<String, Any>?): Job? { + val entity = repository.findOne(id) ?: return null + val state = entity.state + if (!state.isTransitionLegal(newState)) { + throw IllegalArgumentException("Invalid transition from $state to $newState") + } + + val now = Instant.now() + if (!repository.updateOne(entity, newState, now, results)) { + throw IllegalStateException("Conflicting update") + } + + return entity.toRunnerDto() + } + + /** + * Determine whether the transition from [this] to [newState] is legal. + */ + private fun JobState.isTransitionLegal(newState: JobState): Boolean { + // Note that we always allow transitions from the state + return newState == this || when (this) { + JobState.PENDING -> newState == JobState.CLAIMED + JobState.CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED + JobState.RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED + JobState.FINISHED, JobState.FAILED -> false + } + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/PortfolioService.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/PortfolioService.kt new file mode 100644 index 00000000..1f41c2d7 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/PortfolioService.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.service + +import org.opendc.web.api.model.* +import org.opendc.web.api.repository.PortfolioRepository +import org.opendc.web.api.repository.ProjectRepository +import org.opendc.web.proto.user.Portfolio +import java.time.Instant +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject +import org.opendc.web.api.model.Portfolio as PortfolioEntity + +/** + * Service for managing [Portfolio]s. + */ +@ApplicationScoped +class PortfolioService @Inject constructor( + private val projectRepository: ProjectRepository, + private val portfolioRepository: PortfolioRepository +) { + /** + * List all [Portfolio]s that belong a certain project. + */ + fun findAll(userId: String, projectId: Long): List<Portfolio> { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) ?: return emptyList() + val project = auth.toUserDto() + return portfolioRepository.findAll(projectId).map { it.toUserDto(project) } + } + + /** + * Find a [Portfolio] with the specified [number] belonging to [project][projectId]. + */ + fun findOne(userId: String, projectId: Long, number: Int): Portfolio? { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) ?: return null + return portfolioRepository.findOne(projectId, number)?.toUserDto(auth.toUserDto()) + } + + /** + * Delete the portfolio with the specified [number] belonging to [project][projectId]. + */ + fun delete(userId: String, projectId: Long, number: Int): Portfolio? { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) + + if (auth == null) { + return null + } else if (!auth.role.canEdit) { + throw IllegalStateException("Not permitted to edit project") + } + + val entity = portfolioRepository.findOne(projectId, number) ?: return null + val portfolio = entity.toUserDto(auth.toUserDto()) + portfolioRepository.delete(entity) + return portfolio + } + + /** + * Construct a new [Portfolio] with the specified name. + */ + fun create(userId: String, projectId: Long, request: Portfolio.Create): Portfolio? { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) + + if (auth == null) { + return null + } else if (!auth.role.canEdit) { + throw IllegalStateException("Not permitted to edit project") + } + + val now = Instant.now() + val project = auth.project + val number = projectRepository.allocatePortfolio(auth.project, now) + + val portfolio = PortfolioEntity(0, number, request.name, project, request.targets) + + project.portfolios.add(portfolio) + portfolioRepository.save(portfolio) + + return portfolio.toUserDto(auth.toUserDto()) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ProjectService.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ProjectService.kt new file mode 100644 index 00000000..c3e43395 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ProjectService.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.service + +import org.opendc.web.api.model.* +import org.opendc.web.api.repository.ProjectRepository +import org.opendc.web.proto.user.Project +import org.opendc.web.proto.user.ProjectRole +import java.time.Instant +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject + +/** + * Service for managing [Project]s. + */ +@ApplicationScoped +class ProjectService @Inject constructor(private val repository: ProjectRepository) { + /** + * List all projects for the user with the specified [userId]. + */ + fun findWithUser(userId: String): List<Project> { + return repository.findAll(userId).map { it.toUserDto() } + } + + /** + * Obtain the project with the specified [id] for the user with the specified [userId]. + */ + fun findWithUser(userId: String, id: Long): Project? { + return repository.findOne(userId, id)?.toUserDto() + } + + /** + * Create a new [Project] for the user with the specified [userId]. + */ + fun createForUser(userId: String, name: String): Project { + val now = Instant.now() + val entity = Project(0, name, now) + repository.save(entity) + + val authorization = ProjectAuthorization(ProjectAuthorizationKey(userId, entity.id), entity, ProjectRole.OWNER) + + entity.authorizations.add(authorization) + repository.save(authorization) + + return authorization.toUserDto() + } + + /** + * Delete a project by its identifier. + * + * @param userId The user that invokes the action. + * @param id The identifier of the project. + */ + fun deleteWithUser(userId: String, id: Long): Project? { + val auth = repository.findOne(userId, id) ?: return null + + if (!auth.role.canDelete) { + throw IllegalArgumentException("Not allowed to delete project") + } + + val now = Instant.now() + val project = auth.toUserDto().copy(updatedAt = now) + repository.delete(auth.project) + return project + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/RunnerConversions.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/RunnerConversions.kt new file mode 100644 index 00000000..3722a641 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/RunnerConversions.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.service + +import org.opendc.web.api.model.Job +import org.opendc.web.api.model.Portfolio +import org.opendc.web.api.model.Scenario +import org.opendc.web.api.model.Topology + +/** + * Conversions into DTOs provided to OpenDC runners. + */ + +/** + * Convert a [Topology] into a runner-facing DTO. + */ +internal fun Topology.toRunnerDto(): org.opendc.web.proto.runner.Topology { + return org.opendc.web.proto.runner.Topology(id, number, name, rooms, createdAt, updatedAt) +} + +/** + * Convert a [Portfolio] into a runner-facing DTO. + */ +internal fun Portfolio.toRunnerDto(): org.opendc.web.proto.runner.Portfolio { + return org.opendc.web.proto.runner.Portfolio(id, number, name, targets) +} + +/** + * Convert a [Job] into a runner-facing DTO. + */ +internal fun Job.toRunnerDto(): org.opendc.web.proto.runner.Job { + return org.opendc.web.proto.runner.Job(id, scenario.toRunnerDto(), state, createdAt, updatedAt, results) +} + +/** + * Convert a [Job] into a runner-facing DTO. + */ +internal fun Scenario.toRunnerDto(): org.opendc.web.proto.runner.Scenario { + return org.opendc.web.proto.runner.Scenario( + id, + number, + portfolio.toRunnerDto(), + name, + workload.toDto(), + topology.toRunnerDto(), + phenomena, + schedulerName + ) +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ScenarioService.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ScenarioService.kt new file mode 100644 index 00000000..dd51a929 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/ScenarioService.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.service + +import org.opendc.web.api.model.* +import org.opendc.web.api.repository.* +import org.opendc.web.proto.user.Scenario +import java.time.Instant +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject + +/** + * Service for managing [Scenario]s. + */ +@ApplicationScoped +class ScenarioService @Inject constructor( + private val projectRepository: ProjectRepository, + private val portfolioRepository: PortfolioRepository, + private val topologyRepository: TopologyRepository, + private val traceRepository: TraceRepository, + private val scenarioRepository: ScenarioRepository, +) { + /** + * List all [Scenario]s that belong a certain portfolio. + */ + fun findAll(userId: String, projectId: Long, number: Int): List<Scenario> { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) ?: return emptyList() + val project = auth.toUserDto() + return scenarioRepository.findAll(projectId).map { it.toUserDto(project) } + } + + /** + * Obtain a [Scenario] by identifier. + */ + fun findOne(userId: String, projectId: Long, number: Int): Scenario? { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) ?: return null + val project = auth.toUserDto() + return scenarioRepository.findOne(projectId, number)?.toUserDto(project) + } + + /** + * Delete the specified scenario. + */ + fun delete(userId: String, projectId: Long, number: Int): Scenario? { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) + + if (auth == null) { + return null + } else if (!auth.role.canEdit) { + throw IllegalStateException("Not permitted to edit project") + } + + val entity = scenarioRepository.findOne(projectId, number) ?: return null + val scenario = entity.toUserDto(auth.toUserDto()) + scenarioRepository.delete(entity) + return scenario + } + + /** + * Construct a new [Scenario] with the specified data. + */ + fun create(userId: String, projectId: Long, portfolioNumber: Int, request: Scenario.Create): Scenario? { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) + + if (auth == null) { + return null + } else if (!auth.role.canEdit) { + throw IllegalStateException("Not permitted to edit project") + } + + val portfolio = portfolioRepository.findOne(projectId, portfolioNumber) ?: return null + val topology = requireNotNull( + topologyRepository.findOne( + projectId, + request.topology.toInt() + ) + ) { "Referred topology does not exist" } + val trace = + requireNotNull(traceRepository.findOne(request.workload.trace)) { "Referred trace does not exist" } + + val now = Instant.now() + val project = auth.project + val number = projectRepository.allocateScenario(auth.project, now) + + val scenario = Scenario( + 0, + number, + request.name, + project, + portfolio, + Workload(trace, request.workload.samplingFraction), + topology, + request.phenomena, + request.schedulerName + ) + val job = Job(0, scenario, now, portfolio.targets.repeats) + + scenario.job = job + portfolio.scenarios.add(scenario) + scenarioRepository.save(scenario) + + return scenario.toUserDto(auth.toUserDto()) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TopologyService.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TopologyService.kt new file mode 100644 index 00000000..f3460496 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TopologyService.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.service + +import org.opendc.web.api.repository.ProjectRepository +import org.opendc.web.api.repository.TopologyRepository +import org.opendc.web.proto.user.Topology +import java.time.Instant +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject +import org.opendc.web.api.model.Topology as TopologyEntity + +/** + * Service for managing [Topology]s. + */ +@ApplicationScoped +class TopologyService @Inject constructor( + private val projectRepository: ProjectRepository, + private val topologyRepository: TopologyRepository +) { + /** + * List all [Topology]s that belong a certain project. + */ + fun findAll(userId: String, projectId: Long): List<Topology> { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) ?: return emptyList() + val project = auth.toUserDto() + return topologyRepository.findAll(projectId).map { it.toUserDto(project) } + } + + /** + * Find the [Topology] with the specified [number] belonging to [project][projectId]. + */ + fun findOne(userId: String, projectId: Long, number: Int): Topology? { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) ?: return null + return topologyRepository.findOne(projectId, number)?.toUserDto(auth.toUserDto()) + } + + /** + * Delete the [Topology] with the specified [number] belonging to [project][projectId]. + */ + fun delete(userId: String, projectId: Long, number: Int): Topology? { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) + + if (auth == null) { + return null + } else if (!auth.role.canEdit) { + throw IllegalStateException("Not permitted to edit project") + } + + val entity = topologyRepository.findOne(projectId, number) ?: return null + val now = Instant.now() + val topology = entity.toUserDto(auth.toUserDto()).copy(updatedAt = now) + topologyRepository.delete(entity) + + return topology + } + + /** + * Update a [Topology] with the specified [number] belonging to [project][projectId]. + */ + fun update(userId: String, projectId: Long, number: Int, request: Topology.Update): Topology? { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) + + if (auth == null) { + return null + } else if (!auth.role.canEdit) { + throw IllegalStateException("Not permitted to edit project") + } + + val entity = topologyRepository.findOne(projectId, number) ?: return null + val now = Instant.now() + + entity.updatedAt = now + entity.rooms = request.rooms + + return entity.toUserDto(auth.toUserDto()) + } + + /** + * Construct a new [Topology] with the specified name. + */ + fun create(userId: String, projectId: Long, request: Topology.Create): Topology? { + // User must have access to project + val auth = projectRepository.findOne(userId, projectId) + + if (auth == null) { + return null + } else if (!auth.role.canEdit) { + throw IllegalStateException("Not permitted to edit project") + } + + val now = Instant.now() + val project = auth.project + val number = projectRepository.allocateTopology(auth.project, now) + + val topology = TopologyEntity(0, number, request.name, project, now, request.rooms) + + project.topologies.add(topology) + topologyRepository.save(topology) + + return topology.toUserDto(auth.toUserDto()) + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TraceService.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TraceService.kt new file mode 100644 index 00000000..a942696e --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/TraceService.kt @@ -0,0 +1,48 @@ +/* + * 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. + */ + +package org.opendc.web.api.service + +import org.opendc.web.api.repository.TraceRepository +import org.opendc.web.proto.Trace +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject + +/** + * Service for managing [Trace]s. + */ +@ApplicationScoped +class TraceService @Inject constructor(private val repository: TraceRepository) { + /** + * Obtain all available workload traces. + */ + fun findAll(): List<Trace> { + return repository.findAll().map { it.toUserDto() } + } + + /** + * Obtain a workload trace by identifier. + */ + fun findById(id: String): Trace? { + return repository.findOne(id)?.toUserDto() + } +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/UserConversions.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/UserConversions.kt new file mode 100644 index 00000000..8612ee8c --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/UserConversions.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.service + +import org.opendc.web.api.model.* +import org.opendc.web.proto.user.Project + +/** + * Conversions into DTOs provided to users. + */ + +/** + * Convert a [Trace] entity into a [org.opendc.web.proto.Trace] DTO. + */ +internal fun Trace.toUserDto(): org.opendc.web.proto.Trace { + return org.opendc.web.proto.Trace(id, name, type) +} + +/** + * Convert a [ProjectAuthorization] entity into a [Project] DTO. + */ +internal fun ProjectAuthorization.toUserDto(): Project { + return Project(project.id, project.name, project.createdAt, project.updatedAt, role) +} + +/** + * Convert a [Topology] entity into a [org.opendc.web.proto.user.Topology] DTO. + */ +internal fun Topology.toUserDto(project: Project): org.opendc.web.proto.user.Topology { + return org.opendc.web.proto.user.Topology(id, number, project, name, rooms, createdAt, updatedAt) +} + +/** + * Convert a [Topology] entity into a [org.opendc.web.proto.user.Topology.Summary] DTO. + */ +private fun Topology.toSummaryDto(): org.opendc.web.proto.user.Topology.Summary { + return org.opendc.web.proto.user.Topology.Summary(id, number, name, createdAt, updatedAt) +} + +/** + * Convert a [Portfolio] entity into a [org.opendc.web.proto.user.Portfolio] DTO. + */ +internal fun Portfolio.toUserDto(project: Project): org.opendc.web.proto.user.Portfolio { + return org.opendc.web.proto.user.Portfolio(id, number, project, name, targets, scenarios.map { it.toSummaryDto() }) +} + +/** + * Convert a [Portfolio] entity into a [org.opendc.web.proto.user.Portfolio.Summary] DTO. + */ +private fun Portfolio.toSummaryDto(): org.opendc.web.proto.user.Portfolio.Summary { + return org.opendc.web.proto.user.Portfolio.Summary(id, number, name, targets) +} + +/** + * Convert a [Scenario] entity into a [org.opendc.web.proto.user.Scenario] DTO. + */ +internal fun Scenario.toUserDto(project: Project): org.opendc.web.proto.user.Scenario { + return org.opendc.web.proto.user.Scenario( + id, + number, + project, + portfolio.toSummaryDto(), + name, + workload.toDto(), + topology.toSummaryDto(), + phenomena, + schedulerName, + job.toUserDto() + ) +} + +/** + * Convert a [Scenario] entity into a [org.opendc.web.proto.user.Scenario.Summary] DTO. + */ +private fun Scenario.toSummaryDto(): org.opendc.web.proto.user.Scenario.Summary { + return org.opendc.web.proto.user.Scenario.Summary( + id, + number, + name, + workload.toDto(), + topology.toSummaryDto(), + phenomena, + schedulerName, + job.toUserDto() + ) +} + +/** + * Convert a [Job] entity into a [org.opendc.web.proto.user.Job] DTO. + */ +internal fun Job.toUserDto(): org.opendc.web.proto.user.Job { + return org.opendc.web.proto.user.Job(id, state, createdAt, updatedAt, results) +} + +/** + * Convert a [Workload] entity into a DTO. + */ +internal fun Workload.toDto(): org.opendc.web.proto.Workload { + return org.opendc.web.proto.Workload(trace.toUserDto(), samplingFraction) +} diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/Utils.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/Utils.kt new file mode 100644 index 00000000..254be8b7 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/service/Utils.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.service + +import org.opendc.web.proto.user.ProjectRole + +/** + * Flag to indicate that the user can edit a project. + */ +internal val ProjectRole.canEdit: Boolean + get() = when (this) { + ProjectRole.OWNER, ProjectRole.EDITOR -> true + ProjectRole.VIEWER -> false + } + +/** + * Flag to indicate that the user can delete a project. + */ +internal val ProjectRole.canDelete: Boolean + get() = this == ProjectRole.OWNER diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/KotlinModuleCustomizer.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/KotlinModuleCustomizer.kt new file mode 100644 index 00000000..8d91a00c --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/KotlinModuleCustomizer.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.util + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import io.quarkus.jackson.ObjectMapperCustomizer +import javax.inject.Singleton + +/** + * Helper class to register the Kotlin Jackson module. + */ +@Singleton +class KotlinModuleCustomizer : ObjectMapperCustomizer { + override fun customize(objectMapper: ObjectMapper) { + objectMapper.registerModule(KotlinModule.Builder().build()) + } +} diff --git a/opendc-web/opendc-web-api/src/main/resources/META-INF/branding/logo.png b/opendc-web/opendc-web-api/src/main/resources/META-INF/branding/logo.png Binary files differnew file mode 100644 index 00000000..d743038b --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/resources/META-INF/branding/logo.png diff --git a/opendc-web/opendc-web-api/src/main/resources/application-dev.properties b/opendc-web/opendc-web-api/src/main/resources/application-dev.properties new file mode 100644 index 00000000..6f941067 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/resources/application-dev.properties @@ -0,0 +1,28 @@ +# Copyright (c) 2022 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. + +# Datasource (H2) +quarkus.datasource.db-kind = h2 +quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TYPE IF NOT EXISTS "JSONB" AS text; + +# Hibernate +quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.sql-load-script=init-dev.sql diff --git a/opendc-web/opendc-web-api/src/main/resources/application-prod.properties b/opendc-web/opendc-web-api/src/main/resources/application-prod.properties new file mode 100644 index 00000000..3af1dfd9 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/resources/application-prod.properties @@ -0,0 +1,29 @@ +# Copyright (c) 2022 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. + +# Datasource +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=${OPENDC_DB_USERNAME} +quarkus.datasource.password=${OPENDC_DB_PASSWORD} +quarkus.datasource.jdbc.url=${OPENDC_DB_URL} + +# Hibernate +quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQL95Dialect +quarkus.hibernate-orm.database.generation=validate diff --git a/opendc-web/opendc-web-api/src/main/resources/application-test.properties b/opendc-web/opendc-web-api/src/main/resources/application-test.properties new file mode 100644 index 00000000..ce3a9473 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/resources/application-test.properties @@ -0,0 +1,36 @@ +# Copyright (c) 2022 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. + +# Datasource configuration +quarkus.datasource.db-kind = h2 +quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TYPE "JSONB" AS text; + +quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect +quarkus.hibernate-orm.database.generation=drop-and-create + +# No OIDC for tests +quarkus.oidc.enabled=false +quarkus.oidc.auth-server-url= +quarkus.oidc.client-id= + +# Disable OpenAPI/Swagger +quarkus.smallrye-openapi.enable=false +quarkus.swagger-ui.enable=false +quarkus.smallrye-openapi.oidc-open-id-connect-url= diff --git a/opendc-web/opendc-web-api/src/main/resources/application.properties b/opendc-web/opendc-web-api/src/main/resources/application.properties new file mode 100644 index 00000000..fa134e7e --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/resources/application.properties @@ -0,0 +1,48 @@ +# Copyright (c) 2022 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. + +quarkus.http.cors=true + +# OpenID +quarkus.oidc.auth-server-url=https://${OPENDC_AUTH0_DOMAIN} +quarkus.oidc.client-id=${OPENDC_AUTH0_AUDIENCE} +quarkus.oidc.token.audience=${quarkus.oidc.client-id} +quarkus.oidc.roles.role-claim-path=scope + +# OpenAPI and Swagger +quarkus.smallrye-openapi.info-title=OpenDC REST API +%dev.quarkus.smallrye-openapi.info-title=OpenDC REST API (development) +quarkus.smallrye-openapi.info-version=2.1-rc1 +quarkus.smallrye-openapi.info-description=OpenDC is an open-source datacenter simulator for education, featuring real-time online collaboration, diverse simulation models, and detailed performance feedback statistics. +quarkus.smallrye-openapi.info-contact-email=opendc@atlarge-research.com +quarkus.smallrye-openapi.info-contact-name=OpenDC Support +quarkus.smallrye-openapi.info-contact-url=https://opendc.org +quarkus.smallrye-openapi.info-license-name=MIT +quarkus.smallrye-openapi.info-license-url=https://github.com/atlarge-research/opendc/blob/master/LICENSE.txt + +quarkus.swagger-ui.path=docs +quarkus.swagger-ui.always-include=true +quarkus.swagger-ui.oauth-client-id=${OPENDC_AUTH0_DOCS_CLIENT_ID:} +quarkus.swagger-ui.oauth-additional-query-string-params={"audience":"${OPENDC_AUTH0_AUDIENCE:https://api.opendc.org/}"} + +quarkus.smallrye-openapi.security-scheme=oidc +quarkus.smallrye-openapi.security-scheme-name=Auth0 +quarkus.smallrye-openapi.oidc-open-id-connect-url=https://${OPENDC_AUTH0_DOMAIN:opendc.eu.auth0.com}/.well-known/openid-configuration +quarkus.smallrye-openapi.servers=http://localhost:8080 diff --git a/opendc-web/opendc-web-api/src/main/resources/init-dev.sql b/opendc-web/opendc-web-api/src/main/resources/init-dev.sql new file mode 100644 index 00000000..756eff46 --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/resources/init-dev.sql @@ -0,0 +1,3 @@ + +-- Add example traces +INSERT INTO traces (id, name, type) VALUES ('bitbrains-small', 'Bitbrains Small', 'vm'); diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/SchedulerResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/SchedulerResourceTest.kt new file mode 100644 index 00000000..e88e1c1c --- /dev/null +++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/SchedulerResourceTest.kt @@ -0,0 +1,48 @@ +/* + * 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. + */ + +package org.opendc.web.api.rest + +import io.quarkus.test.junit.QuarkusTest +import io.restassured.http.ContentType +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.junit.jupiter.api.Test + +/** + * Test suite for [SchedulerResource] + */ +@QuarkusTest +class SchedulerResourceTest { + /** + * Test to verify whether we can obtain all schedulers. + */ + @Test + fun testGetSchedulers() { + When { + get("/schedulers") + } Then { + statusCode(200) + contentType(ContentType.JSON) + } + } +} diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/TraceResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/TraceResourceTest.kt new file mode 100644 index 00000000..6fab9953 --- /dev/null +++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/TraceResourceTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest + +import io.mockk.every +import io.quarkiverse.test.junit.mockk.InjectMock +import io.quarkus.test.common.http.TestHTTPEndpoint +import io.quarkus.test.junit.QuarkusMock +import io.quarkus.test.junit.QuarkusTest +import io.restassured.http.ContentType +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.hamcrest.Matchers +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.opendc.web.api.service.TraceService +import org.opendc.web.proto.Trace + +/** + * Test suite for [TraceResource]. + */ +@QuarkusTest +@TestHTTPEndpoint(TraceResource::class) +class TraceResourceTest { + @InjectMock + private lateinit var traceService: TraceService + + @BeforeEach + fun setUp() { + QuarkusMock.installMockForType(traceService, TraceService::class.java) + } + + /** + * Test that tries to obtain all traces (empty response). + */ + @Test + fun testGetAllEmpy() { + every { traceService.findAll() } returns emptyList() + + When { + get() + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("", Matchers.empty<String>()) + } + } + + /** + * Test that tries to obtain a non-existent trace. + */ + @Test + fun testGetNonExisting() { + every { traceService.findById("bitbrains") } returns null + + When { + get("/bitbrains") + } Then { + statusCode(404) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to obtain an existing trace. + */ + @Test + fun testGetExisting() { + every { traceService.findById("bitbrains") } returns Trace("bitbrains", "Bitbrains", "VM") + + When { + get("/bitbrains") + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("name", equalTo("Bitbrains")) + } + } +} diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/runner/JobResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/runner/JobResourceTest.kt new file mode 100644 index 00000000..b82c60e8 --- /dev/null +++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/runner/JobResourceTest.kt @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.runner + +import io.mockk.every +import io.quarkiverse.test.junit.mockk.InjectMock +import io.quarkus.test.common.http.TestHTTPEndpoint +import io.quarkus.test.junit.QuarkusMock +import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.security.TestSecurity +import io.restassured.http.ContentType +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.opendc.web.api.service.JobService +import org.opendc.web.proto.* +import org.opendc.web.proto.Targets +import org.opendc.web.proto.runner.Job +import org.opendc.web.proto.runner.Portfolio +import org.opendc.web.proto.runner.Scenario +import org.opendc.web.proto.runner.Topology +import java.time.Instant + +/** + * Test suite for [JobResource]. + */ +@QuarkusTest +@TestHTTPEndpoint(JobResource::class) +class JobResourceTest { + @InjectMock + private lateinit var jobService: JobService + + /** + * Dummy values + */ + private val dummyPortfolio = Portfolio(1, 1, "test", Targets(emptySet())) + private val dummyTopology = Topology(1, 1, "test", emptyList(), Instant.now(), Instant.now()) + private val dummyTrace = Trace("bitbrains", "Bitbrains", "vm") + private val dummyScenario = Scenario(1, 1, dummyPortfolio, "test", Workload(dummyTrace, 1.0), dummyTopology, OperationalPhenomena(false, false), "test",) + private val dummyJob = Job(1, dummyScenario, JobState.PENDING, Instant.now(), Instant.now()) + + @BeforeEach + fun setUp() { + QuarkusMock.installMockForType(jobService, JobService::class.java) + } + + /** + * Test that tries to query the pending jobs without token. + */ + @Test + fun testQueryWithoutToken() { + When { + get() + } Then { + statusCode(401) + } + } + + /** + * Test that tries to query the pending jobs for a user. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testQueryInvalidScope() { + When { + get() + } Then { + statusCode(403) + } + } + + /** + * Test that tries to query the pending jobs for a runner. + */ + @Test + @TestSecurity(user = "testUser", roles = ["runner"]) + fun testQuery() { + every { jobService.queryPending() } returns listOf(dummyJob) + + When { + get() + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("get(0).id", equalTo(1)) + } + } + + /** + * Test that tries to obtain a non-existent job. + */ + @Test + @TestSecurity(user = "testUser", roles = ["runner"]) + fun testGetNonExisting() { + every { jobService.findById(1) } returns null + + When { + get("/1") + } Then { + statusCode(404) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to obtain a job. + */ + @Test + @TestSecurity(user = "testUser", roles = ["runner"]) + fun testGetExisting() { + every { jobService.findById(1) } returns dummyJob + + When { + get("/1") + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("id", equalTo(1)) + } + } + + /** + * Test that tries to update a non-existent job. + */ + @Test + @TestSecurity(user = "testUser", roles = ["runner"]) + fun testUpdateNonExistent() { + every { jobService.updateState(1, any(), any()) } returns null + + Given { + body(Job.Update(JobState.PENDING)) + contentType(ContentType.JSON) + } When { + post("/1") + } Then { + statusCode(404) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to update a job. + */ + @Test + @TestSecurity(user = "testUser", roles = ["runner"]) + fun testUpdateState() { + every { jobService.updateState(1, any(), any()) } returns dummyJob.copy(state = JobState.CLAIMED) + + Given { + body(Job.Update(JobState.CLAIMED)) + contentType(ContentType.JSON) + } When { + post("/1") + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("state", equalTo(JobState.CLAIMED.toString())) + } + } + + /** + * Test that tries to update a job with invalid input. + */ + @Test + @TestSecurity(user = "testUser", roles = ["runner"]) + fun testUpdateInvalidInput() { + Given { + body("""{ "test": "test" }""") + contentType(ContentType.JSON) + } When { + post("/1") + } Then { + statusCode(400) + contentType(ContentType.JSON) + } + } +} diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioResourceTest.kt new file mode 100644 index 00000000..f74efbca --- /dev/null +++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioResourceTest.kt @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.user + +import io.mockk.every +import io.quarkiverse.test.junit.mockk.InjectMock +import io.quarkus.test.common.http.TestHTTPEndpoint +import io.quarkus.test.junit.QuarkusMock +import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.security.TestSecurity +import io.restassured.http.ContentType +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.hamcrest.Matchers +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.opendc.web.api.service.PortfolioService +import org.opendc.web.proto.Targets +import org.opendc.web.proto.user.Portfolio +import org.opendc.web.proto.user.Project +import org.opendc.web.proto.user.ProjectRole +import java.time.Instant + +/** + * Test suite for [PortfolioResource]. + */ +@QuarkusTest +@TestHTTPEndpoint(PortfolioResource::class) +class PortfolioResourceTest { + @InjectMock + private lateinit var portfolioService: PortfolioService + + /** + * Dummy project and portfolio + */ + private val dummyProject = Project(1, "test", Instant.now(), Instant.now(), ProjectRole.OWNER) + private val dummyPortfolio = Portfolio(1, 1, dummyProject, "test", Targets(emptySet(), 1), emptyList()) + + @BeforeEach + fun setUp() { + QuarkusMock.installMockForType(portfolioService, PortfolioService::class.java) + } + + /** + * Test that tries to obtain the list of portfolios belonging to a project. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGetForProject() { + every { portfolioService.findAll("testUser", 1) } returns emptyList() + + Given { + pathParam("project", "1") + } When { + get() + } Then { + statusCode(200) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to create a topology for a project. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreateNonExistent() { + every { portfolioService.create("testUser", 1, any()) } returns null + + Given { + pathParam("project", "1") + + body(Portfolio.Create("test", Targets(emptySet(), 1))) + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(404) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to create a portfolio for a scenario. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreate() { + every { portfolioService.create("testUser", 1, any()) } returns dummyPortfolio + + Given { + pathParam("project", "1") + + body(Portfolio.Create("test", Targets(emptySet(), 1))) + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("id", Matchers.equalTo(1)) + body("name", Matchers.equalTo("test")) + } + } + + /** + * Test to create a portfolio with an empty body. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreateEmpty() { + Given { + pathParam("project", "1") + + body("{}") + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(400) + contentType(ContentType.JSON) + } + } + + /** + * Test to create a portfolio with a blank name. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreateBlankName() { + Given { + pathParam("project", "1") + + body(Portfolio.Create("", Targets(emptySet(), 1))) + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(400) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to obtain a portfolio without token. + */ + @Test + fun testGetWithoutToken() { + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(401) + } + } + + /** + * Test that tries to obtain a portfolio with an invalid scope. + */ + @Test + @TestSecurity(user = "testUser", roles = ["runner"]) + fun testGetInvalidToken() { + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(403) + } + } + + /** + * Test that tries to obtain a non-existent portfolio. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGetNonExisting() { + every { portfolioService.findOne("testUser", 1, 1) } returns null + + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(404) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to obtain a portfolio. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGetExisting() { + every { portfolioService.findOne("testUser", 1, 1) } returns dummyPortfolio + + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("id", Matchers.equalTo(1)) + } + } + + /** + * Test to delete a non-existent portfolio. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testDeleteNonExistent() { + every { portfolioService.delete("testUser", 1, 1) } returns null + + Given { + pathParam("project", "1") + } When { + delete("/1") + } Then { + statusCode(404) + } + } + + /** + * Test to delete a portfolio. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testDelete() { + every { portfolioService.delete("testUser", 1, 1) } returns dummyPortfolio + + Given { + pathParam("project", "1") + } When { + delete("/1") + } Then { + statusCode(200) + contentType(ContentType.JSON) + } + } +} diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResourceTest.kt new file mode 100644 index 00000000..dbafa8c0 --- /dev/null +++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/PortfolioScenarioResourceTest.kt @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.user + +import io.mockk.every +import io.quarkiverse.test.junit.mockk.InjectMock +import io.quarkus.test.common.http.TestHTTPEndpoint +import io.quarkus.test.junit.QuarkusMock +import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.security.TestSecurity +import io.restassured.http.ContentType +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.hamcrest.Matchers +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.opendc.web.api.service.ScenarioService +import org.opendc.web.proto.* +import org.opendc.web.proto.user.* +import java.time.Instant + +/** + * Test suite for [PortfolioScenarioResource]. + */ +@QuarkusTest +@TestHTTPEndpoint(PortfolioScenarioResource::class) +class PortfolioScenarioResourceTest { + @InjectMock + private lateinit var scenarioService: ScenarioService + + /** + * Dummy values + */ + private val dummyProject = Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER) + private val dummyPortfolio = Portfolio.Summary(1, 1, "test", Targets(emptySet())) + private val dummyJob = Job(1, JobState.PENDING, Instant.now(), Instant.now(), null) + private val dummyTrace = Trace("bitbrains", "Bitbrains", "vm") + private val dummyTopology = Topology.Summary(1, 1, "test", Instant.now(), Instant.now()) + private val dummyScenario = Scenario( + 1, + 1, + dummyProject, + dummyPortfolio, + "test", + Workload(dummyTrace, 1.0), + dummyTopology, + OperationalPhenomena(false, false), + "test", + dummyJob + ) + + @BeforeEach + fun setUp() { + QuarkusMock.installMockForType(scenarioService, ScenarioService::class.java) + } + + /** + * Test that tries to obtain a portfolio without token. + */ + @Test + fun testGetWithoutToken() { + Given { + pathParam("project", "1") + pathParam("portfolio", "1") + } When { + get() + } Then { + statusCode(401) + } + } + + /** + * Test that tries to obtain a portfolio with an invalid scope. + */ + @Test + @TestSecurity(user = "testUser", roles = ["runner"]) + fun testGetInvalidToken() { + Given { + pathParam("project", "1") + pathParam("portfolio", "1") + } When { + get() + } Then { + statusCode(403) + } + } + + /** + * Test that tries to obtain a non-existent portfolio. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGet() { + every { scenarioService.findAll("testUser", 1, 1) } returns emptyList() + + Given { + pathParam("project", "1") + pathParam("portfolio", "1") + } When { + get() + } Then { + statusCode(200) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to create a scenario for a portfolio. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreateNonExistent() { + every { scenarioService.create("testUser", 1, any(), any()) } returns null + + Given { + pathParam("project", "1") + pathParam("portfolio", "1") + + body(Scenario.Create("test", Workload.Spec("test", 1.0), 1, OperationalPhenomena(false, false), "test")) + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(404) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to create a scenario for a portfolio. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreate() { + every { scenarioService.create("testUser", 1, 1, any()) } returns dummyScenario + + Given { + pathParam("project", "1") + pathParam("portfolio", "1") + + body(Scenario.Create("test", Workload.Spec("test", 1.0), 1, OperationalPhenomena(false, false), "test")) + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("id", Matchers.equalTo(1)) + body("name", Matchers.equalTo("test")) + } + } + + /** + * Test to create a project with an empty body. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreateEmpty() { + Given { + pathParam("project", "1") + pathParam("portfolio", "1") + + body("{}") + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(400) + contentType(ContentType.JSON) + } + } + + /** + * Test to create a project with a blank name. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreateBlankName() { + Given { + pathParam("project", "1") + pathParam("portfolio", "1") + + body(Scenario.Create("", Workload.Spec("test", 1.0), 1, OperationalPhenomena(false, false), "test")) + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(400) + contentType(ContentType.JSON) + } + } +} diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ProjectResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ProjectResourceTest.kt new file mode 100644 index 00000000..bcbcbab1 --- /dev/null +++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ProjectResourceTest.kt @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.user + +import io.mockk.every +import io.quarkiverse.test.junit.mockk.InjectMock +import io.quarkus.test.common.http.TestHTTPEndpoint +import io.quarkus.test.junit.QuarkusMock +import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.security.TestSecurity +import io.restassured.http.ContentType +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.opendc.web.api.service.ProjectService +import org.opendc.web.proto.user.Project +import org.opendc.web.proto.user.ProjectRole +import java.time.Instant + +/** + * Test suite for [ProjectResource]. + */ +@QuarkusTest +@TestHTTPEndpoint(ProjectResource::class) +class ProjectResourceTest { + @InjectMock + private lateinit var projectService: ProjectService + + /** + * Dummy values. + */ + private val dummyProject = Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER) + + @BeforeEach + fun setUp() { + QuarkusMock.installMockForType(projectService, ProjectService::class.java) + } + + /** + * Test that tries to obtain all projects without token. + */ + @Test + fun testGetAllWithoutToken() { + When { + get() + } Then { + statusCode(401) + } + } + + /** + * Test that tries to obtain all projects with an invalid scope. + */ + @Test + @TestSecurity(user = "testUser", roles = ["runner"]) + fun testGetAllWithInvalidScope() { + When { + get() + } Then { + statusCode(403) + } + } + + /** + * Test that tries to obtain all project for a user. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGetAll() { + val projects = listOf(dummyProject) + every { projectService.findWithUser("testUser") } returns projects + + When { + get() + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("get(0).name", equalTo("test")) + } + } + + /** + * Test that tries to obtain a non-existent project. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGetNonExisting() { + every { projectService.findWithUser("testUser", 1) } returns null + + When { + get("/1") + } Then { + statusCode(404) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to obtain a job. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGetExisting() { + every { projectService.findWithUser("testUser", 1) } returns dummyProject + + When { + get("/1") + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("id", equalTo(0)) + } + } + + /** + * Test that tries to create a project. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreate() { + every { projectService.createForUser("testUser", "test") } returns dummyProject + + Given { + body(Project.Create("test")) + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("id", equalTo(0)) + body("name", equalTo("test")) + } + } + + /** + * Test to create a project with an empty body. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreateEmpty() { + Given { + body("{}") + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(400) + contentType(ContentType.JSON) + } + } + + /** + * Test to create a project with a blank name. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreateBlankName() { + Given { + body(Project.Create("")) + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(400) + contentType(ContentType.JSON) + } + } + + /** + * Test to delete a non-existent project. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testDeleteNonExistent() { + every { projectService.deleteWithUser("testUser", 1) } returns null + + When { + delete("/1") + } Then { + statusCode(404) + contentType(ContentType.JSON) + } + } + + /** + * Test to delete a project. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testDelete() { + every { projectService.deleteWithUser("testUser", 1) } returns dummyProject + + When { + delete("/1") + } Then { + statusCode(200) + contentType(ContentType.JSON) + } + } + + /** + * Test to delete a project which the user does not own. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testDeleteNonOwner() { + every { projectService.deleteWithUser("testUser", 1) } throws IllegalArgumentException("User does not own project") + + When { + delete("/1") + } Then { + statusCode(403) + contentType(ContentType.JSON) + } + } +} diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ScenarioResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ScenarioResourceTest.kt new file mode 100644 index 00000000..65e6e9a1 --- /dev/null +++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/ScenarioResourceTest.kt @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.user + +import io.mockk.every +import io.quarkiverse.test.junit.mockk.InjectMock +import io.quarkus.test.common.http.TestHTTPEndpoint +import io.quarkus.test.junit.QuarkusMock +import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.security.TestSecurity +import io.restassured.http.ContentType +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.hamcrest.Matchers +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.opendc.web.api.service.ScenarioService +import org.opendc.web.proto.* +import org.opendc.web.proto.user.* +import java.time.Instant + +/** + * Test suite for [ScenarioResource]. + */ +@QuarkusTest +@TestHTTPEndpoint(ScenarioResource::class) +class ScenarioResourceTest { + @InjectMock + private lateinit var scenarioService: ScenarioService + + /** + * Dummy values + */ + private val dummyProject = Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER) + private val dummyPortfolio = Portfolio.Summary(1, 1, "test", Targets(emptySet())) + private val dummyJob = Job(1, JobState.PENDING, Instant.now(), Instant.now(), null) + private val dummyTrace = Trace("bitbrains", "Bitbrains", "vm") + private val dummyTopology = Topology.Summary(1, 1, "test", Instant.now(), Instant.now()) + private val dummyScenario = Scenario( + 1, + 1, + dummyProject, + dummyPortfolio, + "test", + Workload(dummyTrace, 1.0), + dummyTopology, + OperationalPhenomena(false, false), + "test", + dummyJob + ) + + @BeforeEach + fun setUp() { + QuarkusMock.installMockForType(scenarioService, ScenarioService::class.java) + } + + /** + * Test that tries to obtain a scenario without token. + */ + @Test + fun testGetWithoutToken() { + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(401) + } + } + + /** + * Test that tries to obtain a scenario with an invalid scope. + */ + @Test + @TestSecurity(user = "testUser", roles = ["runner"]) + fun testGetInvalidToken() { + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(403) + } + } + + /** + * Test that tries to obtain a non-existent scenario. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGetNonExisting() { + every { scenarioService.findOne("testUser", 1, 1) } returns null + + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(404) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to obtain a scenario. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGetExisting() { + every { scenarioService.findOne("testUser", 1, 1) } returns dummyScenario + + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("id", Matchers.equalTo(1)) + } + } + + /** + * Test to delete a non-existent scenario. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testDeleteNonExistent() { + every { scenarioService.delete("testUser", 1, 1) } returns null + + Given { + pathParam("project", "1") + } When { + delete("/1") + } Then { + statusCode(404) + } + } + + /** + * Test to delete a scenario. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testDelete() { + every { scenarioService.delete("testUser", 1, 1) } returns dummyScenario + + Given { + pathParam("project", "1") + } When { + delete("/1") + } Then { + statusCode(200) + contentType(ContentType.JSON) + } + } +} diff --git a/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/TopologyResourceTest.kt b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/TopologyResourceTest.kt new file mode 100644 index 00000000..ececeaca --- /dev/null +++ b/opendc-web/opendc-web-api/src/test/kotlin/org/opendc/web/api/rest/user/TopologyResourceTest.kt @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2022 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. + */ + +package org.opendc.web.api.rest.user + +import io.mockk.every +import io.quarkiverse.test.junit.mockk.InjectMock +import io.quarkus.test.common.http.TestHTTPEndpoint +import io.quarkus.test.junit.QuarkusMock +import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.security.TestSecurity +import io.restassured.http.ContentType +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.hamcrest.Matchers +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.opendc.web.api.service.TopologyService +import org.opendc.web.proto.user.Project +import org.opendc.web.proto.user.ProjectRole +import org.opendc.web.proto.user.Topology +import java.time.Instant + +/** + * Test suite for [TopologyResource]. + */ +@QuarkusTest +@TestHTTPEndpoint(TopologyResource::class) +class TopologyResourceTest { + @InjectMock + private lateinit var topologyService: TopologyService + + /** + * Dummy project and topology. + */ + private val dummyProject = Project(1, "test", Instant.now(), Instant.now(), ProjectRole.OWNER) + private val dummyTopology = Topology(1, 1, dummyProject, "test", emptyList(), Instant.now(), Instant.now()) + + @BeforeEach + fun setUp() { + QuarkusMock.installMockForType(topologyService, TopologyService::class.java) + } + + /** + * Test that tries to obtain the list of topologies belonging to a project. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGetForProject() { + every { topologyService.findAll("testUser", 1) } returns emptyList() + + Given { + pathParam("project", "1") + } When { + get() + } Then { + statusCode(200) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to create a topology for a project. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreateNonExistent() { + every { topologyService.create("testUser", 1, any()) } returns null + + Given { + pathParam("project", "1") + + body(Topology.Create("test", emptyList())) + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(404) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to create a topology for a project. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreate() { + every { topologyService.create("testUser", 1, any()) } returns dummyTopology + + Given { + pathParam("project", "1") + + body(Topology.Create("test", emptyList())) + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("id", Matchers.equalTo(1)) + body("name", Matchers.equalTo("test")) + } + } + + /** + * Test to create a topology with an empty body. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreateEmpty() { + Given { + pathParam("project", "1") + + body("{}") + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(400) + contentType(ContentType.JSON) + } + } + + /** + * Test to create a topology with a blank name. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testCreateBlankName() { + Given { + pathParam("project", "1") + + body(Topology.Create("", emptyList())) + contentType(ContentType.JSON) + } When { + post() + } Then { + statusCode(400) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to obtain a topology without token. + */ + @Test + fun testGetWithoutToken() { + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(401) + } + } + + /** + * Test that tries to obtain a topology with an invalid scope. + */ + @Test + @TestSecurity(user = "testUser", roles = ["runner"]) + fun testGetInvalidToken() { + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(403) + } + } + + /** + * Test that tries to obtain a non-existent topology. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGetNonExisting() { + every { topologyService.findOne("testUser", 1, 1) } returns null + + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(404) + contentType(ContentType.JSON) + } + } + + /** + * Test that tries to obtain a topology. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testGetExisting() { + every { topologyService.findOne("testUser", 1, 1) } returns dummyTopology + + Given { + pathParam("project", "1") + } When { + get("/1") + } Then { + statusCode(200) + contentType(ContentType.JSON) + body("id", Matchers.equalTo(1)) + println(extract().asPrettyString()) + } + } + + /** + * Test to delete a non-existent topology. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testUpdateNonExistent() { + every { topologyService.update("testUser", any(), any(), any()) } returns null + + Given { + pathParam("project", "1") + body(Topology.Update(emptyList())) + contentType(ContentType.JSON) + } When { + put("/1") + } Then { + statusCode(404) + } + } + + /** + * Test to update a topology. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testUpdate() { + every { topologyService.update("testUser", any(), any(), any()) } returns dummyTopology + + Given { + pathParam("project", "1") + body(Topology.Update(emptyList())) + contentType(ContentType.JSON) + } When { + put("/1") + } Then { + statusCode(200) + contentType(ContentType.JSON) + } + } + + /** + * Test to delete a non-existent topology. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testDeleteNonExistent() { + every { topologyService.delete("testUser", 1, 1) } returns null + + Given { + pathParam("project", "1") + } When { + delete("/1") + } Then { + statusCode(404) + } + } + + /** + * Test to delete a topology. + */ + @Test + @TestSecurity(user = "testUser", roles = ["openid"]) + fun testDelete() { + every { topologyService.delete("testUser", 1, 1) } returns dummyTopology + + Given { + pathParam("project", "1") + } When { + delete("/1") + } Then { + statusCode(200) + contentType(ContentType.JSON) + } + } +} diff --git a/opendc-web/opendc-web-api/static/schema.yml b/opendc-web/opendc-web-api/static/schema.yml deleted file mode 100644 index 56cf58e7..00000000 --- a/opendc-web/opendc-web-api/static/schema.yml +++ /dev/null @@ -1,1631 +0,0 @@ -openapi: 3.0.0 -info: - version: 2.1.0 - title: OpenDC REST API v2 - description: OpenDC is an open-source datacenter simulator for education, featuring - real-time online collaboration, diverse simulation models, and detailed - performance feedback statistics. - license: - name: MIT - url: https://spdx.org/licenses/MIT - contact: - name: Support - url: https://opendc.org -servers: - - url: https://api.opendc.org/v2 -externalDocs: - description: OpenDC REST API v2 - url: https://api.opendc.com/v2/docs/ -security: - - auth0: - - openid -paths: - /projects: - get: - tags: - - projects - description: List Projects of the active user - responses: - "200": - description: Successfully - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: "#/components/schemas/Project" - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - post: - tags: - - projects - description: Add a Project. - requestBody: - content: - application/json: - schema: - properties: - name: - type: string - description: The new Project. - required: true - responses: - "200": - description: Successfully added Project. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Project" - "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" - "/projects/{projectId}": - get: - tags: - - projects - description: Get this Project. - parameters: - - name: projectId - in: path - description: Project's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully retrieved Project. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Project" - "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" - put: - tags: - - projects - description: Update this Project. - parameters: - - name: projectId - in: path - description: Project's ID. - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - properties: - project: - $ref: "#/components/schemas/Project" - description: Project's new properties. - required: true - responses: - "200": - description: Successfully updated Project. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Project" - "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" - "403": - description: Forbidden from updating Project. - content: - "application/json": - schema: - $ref: "#/components/schemas/Forbidden" - "404": - description: Project not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - delete: - tags: - - projects - description: Delete this project. - parameters: - - name: projectId - in: path - description: Project's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully deleted Project. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Project" - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - "403": - description: Forbidden from deleting Project. - content: - "application/json": - schema: - $ref: "#/components/schemas/Forbidden" - "404": - description: Project not found. - content: - "application/json": - 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 - description: Add a Topology. - parameters: - - name: projectId - in: path - description: Project's ID. - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - properties: - topology: - $ref: "#/components/schemas/Topology" - description: The new Topology. - required: true - responses: - "200": - description: Successfully added Topology. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Topology" - "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: Project not found. - content: - "application/json": - 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: - - projects - description: Add a Portfolio. - parameters: - - name: projectId - in: path - description: Project's ID. - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - properties: - topology: - $ref: "#/components/schemas/Portfolio" - description: The new Portfolio. - required: true - responses: - "200": - description: Successfully added Portfolio. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Portfolio" - "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: Project not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - "/topologies/{topologyId}": - get: - tags: - - topologies - description: Get this Topology. - parameters: - - name: topologyId - in: path - description: Topology's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully retrieved Topology. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Topology" - "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" - "403": - description: Forbidden from retrieving Topology. - content: - "application/json": - schema: - $ref: "#/components/schemas/Forbidden" - "404": - description: Topology not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - put: - tags: - - topologies - description: Update this Topology's name. - parameters: - - name: topologyId - in: path - description: Topology's ID. - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - properties: - topology: - $ref: "#/components/schemas/Topology" - description: Topology's new properties. - required: true - responses: - "200": - description: Successfully updated Topology. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Topology" - "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" - "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" - delete: - tags: - - topologies - description: Delete this Topology. - parameters: - - name: topologyId - in: path - description: Topology's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully deleted Topology. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Topology" - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - "403": - description: Forbidden from deleting Topology. - content: - "application/json": - schema: - $ref: "#/components/schemas/Forbidden" - "404": - description: Topology not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - "/portfolios/{portfolioId}": - get: - tags: - - portfolios - description: Get this Portfolio. - parameters: - - name: portfolioId - in: path - description: Portfolio's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully retrieved Portfolio. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Portfolio" - "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" - "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" - put: - tags: - - portfolios - description: Update this Portfolio. - parameters: - - name: portfolioId - in: path - description: Portfolio's ID. - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/Portfolio" - description: Portfolio's new properties. - required: true - responses: - "200": - description: Successfully updated Portfolio. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Portfolio" - "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" - "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" - delete: - tags: - - portfolios - description: Delete this Portfolio. - parameters: - - name: portfolioId - in: path - description: Portfolio's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully deleted Portfolio. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Portfolio" - "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" - "/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: - - scenarios - description: Get this Scenario. - parameters: - - name: scenarioId - in: path - description: Scenario's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully retrieved 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" - "403": - description: Forbidden from retrieving Scenario. - content: - "application/json": - schema: - $ref: "#/components/schemas/Forbidden" - "404": - description: Scenario not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - put: - tags: - - scenarios - description: Update this Scenario's name (other properties are read-only). - parameters: - - name: scenarioId - in: path - description: Scenario's ID. - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/Scenario" - description: Scenario with new name. - required: true - responses: - "200": - description: Successfully updated 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" - "403": - description: Forbidden from retrieving Scenario. - content: - "application/json": - schema: - $ref: "#/components/schemas/Forbidden" - "404": - description: Scenario not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - delete: - tags: - - scenarios - description: Delete this Scenario. - parameters: - - name: scenarioId - in: path - description: Scenario's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully deleted Scenario. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Scenario" - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - "403": - description: Forbidden from retrieving Scenario. - content: - "application/json": - schema: - $ref: "#/components/schemas/Forbidden" - "404": - description: Scenario not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - /schedulers: - get: - tags: - - simulation - description: Get all available Schedulers - responses: - "200": - description: Successfully retrieved Schedulers. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: "#/components/schemas/Scheduler" - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - /traces: - get: - tags: - - simulation - description: Get all available Traces - responses: - "200": - description: Successfully retrieved Traces. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - type: array - items: - type: object - properties: - _id: - type: string - name: - type: string - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - "/traces/{traceId}": - get: - tags: - - simulation - description: Get this Trace. - parameters: - - name: traceId - in: path - description: Trace's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully retrieved Trace. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Trace" - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - "404": - description: Trace not found - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - /prefabs: - get: - tags: - - prefabs - description: Get all Prefabs the user has rights to view. - responses: - "200": - description: Successfully retrieved prefabs the user is authorized on. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: "#/components/schemas/Prefab" - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - post: - tags: - - prefabs - description: Add a Prefab. - requestBody: - content: - application/json: - schema: - properties: - name: - type: string - description: The new Prefab. - required: true - responses: - "200": - description: Successfully added Prefab. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Prefab" - "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" - "/prefabs/{prefabId}": - get: - tags: - - prefabs - description: Get this Prefab. - parameters: - - name: prefabId - in: path - description: Prefab's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully retrieved Prefab. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Prefab" - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - "403": - description: Forbidden from retrieving Prefab. - content: - "application/json": - schema: - $ref: "#/components/schemas/Forbidden" - "404": - description: Prefab not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - put: - tags: - - prefabs - description: Update this Prefab. - parameters: - - name: prefabId - in: path - description: Prefab's ID. - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - properties: - prefab: - $ref: "#/components/schemas/Prefab" - description: Prefab's new properties. - required: true - responses: - "200": - description: Successfully updated Prefab. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Prefab" - "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" - "403": - description: Forbidden from retrieving Prefab. - content: - "application/json": - schema: - $ref: "#/components/schemas/Forbidden" - "404": - description: Prefab not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - delete: - tags: - - prefabs - description: Delete this prefab. - parameters: - - name: prefabId - in: path - description: Prefab's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully deleted Prefab. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Prefab" - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - "404": - description: Prefab not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - /jobs: - get: - tags: - - jobs - description: Get all available jobs to run. - responses: - "200": - description: Successfully retrieved available jobs. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: "#/components/schemas/Job" - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - "/jobs/{jobId}": - get: - tags: - - jobs - description: Get this Job. - parameters: - - name: jobId - in: path - description: Job's ID. - required: true - schema: - type: string - responses: - "200": - description: Successfully retrieved Job. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Job" - "401": - description: Unauthorized. - content: - "application/json": - schema: - $ref: "#/components/schemas/Unauthorized" - "403": - description: Forbidden from retrieving Job. - content: - "application/json": - schema: - $ref: "#/components/schemas/Forbidden" - "404": - description: Job not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - post: - tags: - - jobs - description: Update this Job. - parameters: - - name: jobId - in: path - description: Job's ID. - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - properties: - job: - $ref: "#/components/schemas/Job" - description: Job's new properties. - required: true - responses: - "200": - description: Successfully updated Job. - content: - "application/json": - schema: - type: object - required: - - data - properties: - data: - $ref: "#/components/schemas/Job" - "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" - "403": - description: Forbidden from retrieving Job. - content: - "application/json": - schema: - $ref: "#/components/schemas/Forbidden" - "404": - description: Job not found. - content: - "application/json": - schema: - $ref: "#/components/schemas/NotFound" - "409": - description: State conflict. - content: - "application/json": - schema: - $ref: "#/components/schemas/Invalid" -components: - securitySchemes: - auth0: - type: oauth2 - x-token-validation-url: https://opendc.eu.auth0.com/userinfo - flows: - authorizationCode: - authorizationUrl: https://opendc.eu.auth0.com/authorize - tokenUrl: https://opendc.eu.auth0.com/oauth/token - scopes: - openid: Grants access to user_id - runner: Grants access to runner jobs - schemas: - Unauthorized: - type: object - required: - - message - properties: - message: - type: string - Invalid: - type: object - required: - - message - - errors - properties: - message: - type: string - errors: - type: array - items: - type: string - Forbidden: - type: object - required: - - message - properties: - message: - type: string - NotFound: - type: object - required: - - message - properties: - message: - type: string - Scheduler: - type: object - properties: - name: - type: string - Project: - type: object - properties: - _id: - type: string - name: - type: string - datetimeCreated: - type: string - format: dateTime - datetimeLastEdited: - type: string - format: dateTime - topologyIds: - type: array - items: - type: string - portfolioIds: - type: array - items: - type: string - authorizations: - type: array - items: - type: object - properties: - userId: - type: string - level: - type: string - enum: ['OWN', 'EDIT', 'VIEW'] - Topology: - type: object - properties: - _id: - type: string - projectId: - type: string - name: - type: string - rooms: - type: array - items: - type: object - properties: - _id: - type: string - name: - type: string - tiles: - type: array - items: - type: object - properties: - _id: - type: string - positionX: - type: integer - positionY: - type: integer - object: - type: object - properties: - _id: - type: string - name: - type: string - capacity: - type: integer - powerCapacityW: - type: integer - machines: - type: array - items: - type: object - properties: - _id: - type: string - position: - type: integer - cpus: - type: array - items: - type: object - properties: - _id: - type: string - name: - type: string - clockRateMhz: - type: integer - numberOfCores: - type: integer - energyConsumptionW: - type: integer - gpus: - type: array - items: - type: object - properties: - _id: - type: string - name: - type: string - clockRateMhz: - type: integer - numberOfCores: - type: integer - energyConsumptionW: - type: integer - memories: - type: array - items: - type: object - properties: - _id: - type: string - name: - type: string - speedMbPerS: - type: integer - sizeMb: - type: integer - energyConsumptionW: - type: integer - storages: - type: array - items: - type: object - properties: - _id: - type: string - name: - type: string - speedMbPerS: - type: integer - sizeMb: - type: integer - energyConsumptionW: - type: integer - Portfolio: - type: object - properties: - _id: - type: string - projectId: - type: string - name: - type: string - scenarioIds: - type: array - items: - type: string - targets: - type: object - properties: - enabledMetrics: - type: array - items: - type: string - repeatsPerScenario: - type: integer - Scenario: - type: object - properties: - _id: - type: string - portfolioId: - type: string - name: - type: string - trace: - type: object - properties: - traceId: - type: string - loadSamplingFraction: - type: number - topology: - type: object - properties: - topologyId: - type: string - operational: - type: object - properties: - failuresEnabled: - type: boolean - performanceInterferenceEnabled: - type: boolean - schedulerName: - type: string - Job: - type: object - properties: - _id: - type: string - scenarioId: - type: string - state: - type: string - heartbeat: - type: string - results: - type: object - Trace: - type: object - properties: - _id: - type: string - name: - type: string - path: - type: string - type: - type: string - Prefab: - type: object - properties: - _id: - type: string - name: - type: string - datetimeCreated: - type: string - format: dateTime - datetimeLastEdited: - type: string - format: dateTime diff --git a/opendc-web/opendc-web-api/tests/api/test_jobs.py b/opendc-web/opendc-web-api/tests/api/test_jobs.py deleted file mode 100644 index 2efe6933..00000000 --- a/opendc-web/opendc-web-api/tests/api/test_jobs.py +++ /dev/null @@ -1,139 +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. - -# -# 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: -# -# -from datetime import datetime - -from opendc.exts import db - -test_id = 24 * '1' -test_id_2 = 24 * '2' - - -def test_get_jobs(client, mocker): - mocker.patch.object(db, 'fetch_all', return_value=[ - {'_id': 'a', 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'}} - ]) - res = client.get('/jobs/') - assert '200' in res.status - - -def test_get_job_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.get(f'/jobs/{test_id}').status - - -def test_get_job(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={ - '_id': 'a', 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'} - }) - res = client.get(f'/jobs/{test_id}') - assert '200' in res.status - - -def test_update_job_nop(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'} - }) - update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', - 'simulation': {'state': 'QUEUED', 'heartbeat': datetime.now()} - }) - res = client.post(f'/jobs/{test_id}', json={'state': 'QUEUED'}) - assert '200' in res.status - update_mock.assert_called_once() - - -def test_update_job_invalid_state(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'} - }) - res = client.post(f'/jobs/{test_id}', json={'state': 'FINISHED'}) - assert '400' in res.status - - -def test_update_job_claim(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'} - }) - update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', - 'simulation': {'state': 'CLAIMED', 'heartbeat': datetime.now()} - }) - res = client.post(f'/jobs/{test_id}', json={'state': 'CLAIMED'}) - assert '200' in res.status - update_mock.assert_called_once() - - -def test_update_job_conflict(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'QUEUED'} - }) - update_mock = mocker.patch.object(db, 'fetch_and_update', return_value=None) - res = client.post(f'/jobs/{test_id}', json={'state': 'CLAIMED'}) - assert '409' in res.status - update_mock.assert_called_once() - - -def test_update_job_run(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'CLAIMED'} - }) - update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', - 'simulation': {'state': 'RUNNING', 'heartbeat': datetime.now()} - }) - res = client.post(f'/jobs/{test_id}', json={'state': 'RUNNING'}) - assert '200' in res.status - update_mock.assert_called_once() - - -def test_update_job_finished(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'RUNNING'} - }) - update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', - 'simulation': {'state': 'FINISHED', 'heartbeat': datetime.now()} - }) - res = client.post(f'/jobs/{test_id}', json={'state': 'FINISHED'}) - assert '200' in res.status - update_mock.assert_called_once() - - -def test_update_job_failed(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', 'simulation': {'state': 'RUNNING'} - }) - update_mock = mocker.patch.object(db, 'fetch_and_update', return_value={ - '_id': test_id, 'scenarioId': 'x', 'portfolioId': 'y', - 'simulation': {'state': 'FAILED', 'heartbeat': datetime.now()} - }) - res = client.post(f'/jobs/{test_id}', json={'state': 'FAILED'}) - assert '200' in res.status - update_mock.assert_called_once() diff --git a/opendc-web/opendc-web-api/tests/api/test_portfolios.py b/opendc-web/opendc-web-api/tests/api/test_portfolios.py deleted file mode 100644 index 196fcb1c..00000000 --- a/opendc-web/opendc-web-api/tests/api/test_portfolios.py +++ /dev/null @@ -1,340 +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 opendc.exts import db - -test_id = 24 * '1' -test_id_2 = 24 * '2' - - -def test_get_portfolio_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.get(f'/portfolios/{test_id}').status - - -def test_get_portfolio_no_authorizations(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={'projectId': test_id, 'authorizations': []}) - res = client.get(f'/portfolios/{test_id}') - assert '403' in res.status - - -def test_get_portfolio_not_authorized(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - 'projectId': test_id, - '_id': test_id, - 'authorizations': [] - }) - res = client.get(f'/portfolios/{test_id}') - assert '403' in res.status - - -def test_get_portfolio(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - 'projectId': test_id, - '_id': test_id, - 'authorizations': [{ - 'userId': 'test', - 'level': 'EDIT' - }] - }) - res = client.get(f'/portfolios/{test_id}') - assert '200' in res.status - - -def test_update_portfolio_missing_parameter(client): - assert '400' in client.put(f'/portfolios/{test_id}').status - - -def test_update_portfolio_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.put(f'/portfolios/{test_id}', json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_update_portfolio_not_authorized(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'userId': 'test', - 'level': 'VIEW' - }] - }) - mocker.patch.object(db, 'update', return_value={}) - assert '403' in client.put(f'/portfolios/{test_id}', json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_update_portfolio(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'userId': 'test', - 'level': 'OWN' - }], - 'targets': { - 'enabledMetrics': [], - 'repeatsPerScenario': 1 - } - }) - mocker.patch.object(db, 'update', return_value={}) - - res = client.put(f'/portfolios/{test_id}', json={'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - }}) - assert '200' in res.status - - -def test_delete_project_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.delete(f'/portfolios/{test_id}').status - - -def test_delete_project_different_user(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'googleId': 'other_test', - 'authorizations': [{ - 'userId': 'test', - 'level': 'VIEW' - }] - }) - mocker.patch.object(db, 'delete_one', return_value=None) - assert '403' in client.delete(f'/portfolios/{test_id}').status - - -def test_delete_project(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'googleId': 'test', - 'portfolioIds': [test_id], - 'authorizations': [{ - 'userId': 'test', - 'level': 'OWN' - }] - }) - mocker.patch.object(db, 'delete_one', return_value={}) - mocker.patch.object(db, 'update', return_value=None) - res = client.delete(f'/portfolios/{test_id}') - assert '200' in res.status - - -def test_add_topology_missing_parameter(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'googleId': 'test', - 'portfolioIds': [test_id], - 'authorizations': [{ - 'userId': 'test', - 'level': 'OWN' - }] - }) - assert '400' in client.post(f'/projects/{test_id}/topologies').status - - -def test_add_topology(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'authorizations': [{ - 'userId': 'test', - 'level': 'OWN' - }], - 'topologyIds': [] - }) - mocker.patch.object(db, - 'insert', - return_value={ - '_id': test_id, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'topologyIds': [] - }) - mocker.patch.object(db, 'update', return_value={}) - res = client.post(f'/projects/{test_id}/topologies', json={'topology': {'name': 'test project', 'rooms': []}}) - assert 'rooms' in res.json['data'] - assert '200' in res.status - - -def test_add_topology_not_authorized(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'userId': 'test', - 'level': 'VIEW' - }] - }) - assert '403' in client.post(f'/projects/{test_id}/topologies', - json={ - 'topology': { - 'name': 'test_topology', - 'rooms': [] - } - }).status - - -def test_add_portfolio_missing_parameter(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'googleId': 'test', - 'portfolioIds': [test_id], - 'authorizations': [{ - 'userId': 'test', - 'level': 'OWN' - }] - }) - assert '400' in client.post(f'/projects/{test_id}/portfolios').status - - -def test_add_portfolio_non_existing_project(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.post(f'/projects/{test_id}/portfolios', - json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_add_portfolio_not_authorized(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'userId': 'test', - 'level': 'VIEW' - }] - }) - assert '403' in client.post(f'/projects/{test_id}/portfolios', - json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }).status - - -def test_add_portfolio(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'portfolioIds': [test_id], - 'authorizations': [{ - 'userId': 'test', - 'level': 'EDIT' - }] - }) - mocker.patch.object(db, - 'insert', - return_value={ - '_id': test_id, - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - }, - 'projectId': test_id, - 'scenarioIds': [], - }) - mocker.patch.object(db, 'update', return_value=None) - res = client.post( - f'/projects/{test_id}/portfolios', - json={ - 'portfolio': { - 'name': 'test', - 'targets': { - 'enabledMetrics': ['test'], - 'repeatsPerScenario': 2 - } - } - }) - 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_prefabs.py b/opendc-web/opendc-web-api/tests/api/test_prefabs.py deleted file mode 100644 index ea3d92d6..00000000 --- a/opendc-web/opendc-web-api/tests/api/test_prefabs.py +++ /dev/null @@ -1,252 +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 unittest.mock import Mock -from opendc.exts import db - -test_id = 24 * '1' -test_id_2 = 24 * '2' - - -def test_add_prefab_missing_parameter(client): - assert '400' in client.post('/prefabs/').status - - -def test_add_prefab(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id, 'authorizations': []}) - mocker.patch.object(db, - 'insert', - return_value={ - '_id': test_id, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': test_id - }) - res = client.post('/prefabs/', json={'prefab': {'name': 'test prefab'}}) - assert 'datetimeCreated' in res.json['data'] - assert 'datetimeLastEdited' in res.json['data'] - assert 'authorId' in res.json['data'] - assert '200' in res.status - - -def test_get_prefabs(client, mocker): - db.fetch_all = Mock() - mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id}) - db.fetch_all.side_effect = [ - [{ - '_id': test_id, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': test_id, - 'visibility' : 'private' - }, - { - '_id': '2' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': test_id, - 'visibility' : 'private' - }, - { - '_id': '3' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': test_id, - 'visibility' : 'public' - }, - { - '_id': '4' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': test_id, - 'visibility' : 'public' - }], - [{ - '_id': '5' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': '2' * 24, - 'visibility' : 'public' - }, - { - '_id': '6' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': '2' * 24, - 'visibility' : 'public' - }, - { - '_id': '7' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': '2' * 24, - 'visibility' : 'public' - }, - { - '_id': '8' * 24, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'authorId': '2' * 24, - 'visibility' : 'public' - }] - ] - mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id}) - res = client.get('/prefabs/') - assert '200' in res.status - - -def test_get_prefab_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.get(f'/prefabs/{test_id}').status - - -def test_get_private_prefab_not_authorized(client, mocker): - db.fetch_one = Mock() - db.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': test_id_2, - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - res = client.get(f'/prefabs/{test_id}') - assert '403' in res.status - - -def test_get_private_prefab(client, mocker): - db.fetch_one = Mock() - db.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': 'test', - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - res = client.get(f'/prefabs/{test_id}') - assert '200' in res.status - - -def test_get_public_prefab(client, mocker): - db.fetch_one = Mock() - db.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': test_id_2, - 'visibility': 'public', - 'rack': {} - }, - { - '_id': test_id - } - ] - res = client.get(f'/prefabs/{test_id}') - assert '200' in res.status - - -def test_update_prefab_missing_parameter(client): - assert '400' in client.put(f'/prefabs/{test_id}').status - - -def test_update_prefab_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.put(f'/prefabs/{test_id}', json={'prefab': {'name': 'S'}}).status - - -def test_update_prefab_not_authorized(client, mocker): - db.fetch_one = Mock() - db.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': test_id_2, - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - mocker.patch.object(db, 'update', return_value={}) - assert '403' in client.put(f'/prefabs/{test_id}', json={'prefab': {'name': 'test prefab', 'rack': {}}}).status - - -def test_update_prefab(client, mocker): - db.fetch_one = Mock() - db.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': 'test', - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - mocker.patch.object(db, 'update', return_value={}) - res = client.put(f'/prefabs/{test_id}', json={'prefab': {'name': 'test prefab', 'rack': {}}}) - assert '200' in res.status - - -def test_delete_prefab_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.delete(f'/prefabs/{test_id}').status - - -def test_delete_prefab_different_user(client, mocker): - db.fetch_one = Mock() - db.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': test_id_2, - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - mocker.patch.object(db, 'delete_one', return_value=None) - assert '403' in client.delete(f'/prefabs/{test_id}').status - - -def test_delete_prefab(client, mocker): - db.fetch_one = Mock() - db.fetch_one.side_effect = [{ - '_id': test_id, - 'name': 'test prefab', - 'authorId': 'test', - 'visibility': 'private', - 'rack': {} - }, - { - '_id': test_id - } - ] - mocker.patch.object(db, 'delete_one', return_value={'prefab': {'name': 'name'}}) - res = client.delete(f'/prefabs/{test_id}') - 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 deleted file mode 100644 index 1cfe4c52..00000000 --- a/opendc-web/opendc-web-api/tests/api/test_projects.py +++ /dev/null @@ -1,197 +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 opendc.exts import db - -test_id = 24 * '1' - - -def test_get_user_projects(client, mocker): - mocker.patch.object(db, 'fetch_all', return_value={'_id': test_id, 'authorizations': [{'userId': 'test', - 'level': 'OWN'}]}) - res = client.get('/projects/') - 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 - - -def test_add_project(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={'_id': test_id, 'authorizations': []}) - mocker.patch.object(db, - 'insert', - return_value={ - '_id': test_id, - 'datetimeCreated': '000', - 'datetimeLastEdited': '000', - 'topologyIds': [] - }) - mocker.patch.object(db, 'update', return_value={}) - res = client.post('/projects/', json={'project': {'name': 'test project'}}) - assert 'datetimeCreated' in res.json['data'] - assert 'datetimeLastEdited' in res.json['data'] - assert 'topologyIds' in res.json['data'] - assert '200' in res.status - - -def test_get_project_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.get(f'/projects/{test_id}').status - - -def test_get_project_no_authorizations(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={'authorizations': []}) - res = client.get(f'/projects/{test_id}') - assert '403' in res.status - - -def test_get_project_not_authorized(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'authorizations': [] - }) - res = client.get(f'/projects/{test_id}') - assert '403' in res.status - - -def test_get_project(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'authorizations': [{ - 'userId': 'test', - 'level': 'EDIT' - }] - }) - res = client.get(f'/projects/{test_id}') - assert '200' in res.status - - -def test_update_project_missing_parameter(client): - assert '400' in client.put(f'/projects/{test_id}').status - - -def test_update_project_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.put(f'/projects/{test_id}', json={'project': {'name': 'S'}}).status - - -def test_update_project_not_authorized(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'authorizations': [{ - 'userId': 'test', - 'level': 'VIEW' - }] - }) - mocker.patch.object(db, 'update', return_value={}) - assert '403' in client.put(f'/projects/{test_id}', json={'project': {'name': 'S'}}).status - - -def test_update_project(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'authorizations': [{ - 'userId': 'test', - 'level': 'OWN' - }] - }) - mocker.patch.object(db, 'update', return_value={}) - - res = client.put(f'/projects/{test_id}', json={'project': {'name': 'S'}}) - assert '200' in res.status - - -def test_delete_project_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.delete(f'/projects/{test_id}').status - - -def test_delete_project_different_user(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'googleId': 'other_test', - 'authorizations': [{ - 'userId': 'test', - 'level': 'VIEW' - }], - 'topologyIds': [] - }) - mocker.patch.object(db, 'delete_one', return_value=None) - assert '403' in client.delete(f'/projects/{test_id}').status - - -def test_delete_project(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'googleId': 'test', - 'authorizations': [{ - 'userId': 'test', - 'level': 'OWN' - }], - 'topologyIds': [], - 'portfolioIds': [], - }) - mocker.patch.object(db, 'update', return_value=None) - mocker.patch.object(db, 'delete_one', return_value={'googleId': 'test'}) - res = client.delete(f'/projects/{test_id}') - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/tests/api/test_scenarios.py b/opendc-web/opendc-web-api/tests/api/test_scenarios.py deleted file mode 100644 index bdd5c4a3..00000000 --- a/opendc-web/opendc-web-api/tests/api/test_scenarios.py +++ /dev/null @@ -1,135 +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 opendc.exts import db - -test_id = 24 * '1' -test_id_2 = 24 * '2' - - -def test_get_scenario_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.get(f'/scenarios/{test_id}').status - - -def test_get_scenario_no_authorizations(client, mocker): - m = mocker.MagicMock() - m.side_effect = ({'portfolioId': test_id}, {'projectId': test_id}, {'authorizations': []}) - mocker.patch.object(db, 'fetch_one', m) - res = client.get(f'/scenarios/{test_id}') - assert '403' in res.status - - -def test_get_scenario(client, mocker): - mocker.patch.object(db, - 'fetch_one', - side_effect=[ - {'portfolioId': test_id}, - {'projectId': test_id}, - {'authorizations': - [{'userId': 'test', 'level': 'OWN'}] - }]) - res = client.get(f'/scenarios/{test_id}') - assert '200' in res.status - - -def test_update_scenario_missing_parameter(client): - assert '400' in client.put(f'/scenarios/{test_id}').status - - -def test_update_scenario_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.put(f'/scenarios/{test_id}', json={ - 'scenario': { - 'name': 'test', - } - }).status - - -def test_update_scenario_not_authorized(client, mocker): - mocker.patch.object(db, - 'fetch_one', - side_effect=[ - {'portfolioId': test_id}, - {'projectId': test_id}, - {'authorizations': - [{'userId': 'test', 'level': 'VIEW'}] - }]) - mocker.patch.object(db, 'update', return_value={}) - assert '403' in client.put(f'/scenarios/{test_id}', json={ - 'scenario': { - 'name': 'test', - } - }).status - - -def test_update_scenario(client, mocker): - mocker.patch.object(db, - 'fetch_one', - side_effect=[ - {'_id': test_id, 'portfolioId': test_id}, - {'projectId': test_id}, - {'authorizations': - [{'userId': 'test', 'level': 'OWN'}] - }]) - mocker.patch.object(db, 'update', return_value={}) - - res = client.put(f'/scenarios/{test_id}', json={'scenario': { - 'name': 'test', - }}) - assert '200' in res.status - - -def test_delete_project_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.delete(f'/scenarios/{test_id}').status - - -def test_delete_project_different_user(client, mocker): - mocker.patch.object(db, - 'fetch_one', - side_effect=[ - {'_id': test_id, 'portfolioId': test_id}, - {'projectId': test_id}, - {'authorizations': - [{'userId': 'test', 'level': 'VIEW'}] - }]) - mocker.patch.object(db, 'delete_one', return_value=None) - assert '403' in client.delete(f'/scenarios/{test_id}').status - - -def test_delete_project(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'portfolioId': test_id, - 'googleId': 'test', - 'scenarioIds': [test_id], - 'authorizations': [{ - 'userId': 'test', - 'level': 'OWN' - }] - }) - mocker.patch.object(db, 'delete_one', return_value={}) - mocker.patch.object(db, 'update', return_value=None) - res = client.delete(f'/scenarios/{test_id}') - assert '200' in res.status diff --git a/opendc-web/opendc-web-api/tests/api/test_schedulers.py b/opendc-web/opendc-web-api/tests/api/test_schedulers.py deleted file mode 100644 index 5d9e6995..00000000 --- a/opendc-web/opendc-web-api/tests/api/test_schedulers.py +++ /dev/null @@ -1,22 +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. - -def test_get_schedulers(client): - assert '200' in client.get('/schedulers/').status diff --git a/opendc-web/opendc-web-api/tests/api/test_topologies.py b/opendc-web/opendc-web-api/tests/api/test_topologies.py deleted file mode 100644 index 6e7c54ef..00000000 --- a/opendc-web/opendc-web-api/tests/api/test_topologies.py +++ /dev/null @@ -1,140 +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 opendc.exts import db - -test_id = 24 * '1' -test_id_2 = 24 * '2' - - -def test_get_topology(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'userId': 'test', - 'level': 'EDIT' - }] - }) - res = client.get(f'/topologies/{test_id}') - assert '200' in res.status - - -def test_get_topology_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.get('/topologies/1').status - - -def test_get_topology_not_authorized(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [] - }) - res = client.get(f'/topologies/{test_id}') - assert '403' in res.status - - -def test_get_topology_no_authorizations(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={'projectId': test_id, 'authorizations': []}) - res = client.get(f'/topologies/{test_id}') - assert '403' in res.status - - -def test_update_topology_missing_parameter(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [] - }) - assert '400' in client.put(f'/topologies/{test_id}').status - - -def test_update_topology_non_existent(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.put(f'/topologies/{test_id}', json={'topology': {'name': 'test_topology', 'rooms': []}}).status - - -def test_update_topology_not_authorized(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [] - }) - mocker.patch.object(db, 'update', return_value={}) - assert '403' in client.put(f'/topologies/{test_id}', json={ - 'topology': { - 'name': 'updated_topology', - 'rooms': [] - } - }).status - - -def test_update_topology(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'authorizations': [{ - 'userId': 'test', - 'level': 'OWN' - }] - }) - mocker.patch.object(db, 'update', return_value={}) - - assert '200' in client.put(f'/topologies/{test_id}', json={ - 'topology': { - 'name': 'updated_topology', - 'rooms': [] - } - }).status - - -def test_delete_topology(client, mocker): - mocker.patch.object(db, - 'fetch_one', - return_value={ - '_id': test_id, - 'projectId': test_id, - 'googleId': 'test', - 'topologyIds': [test_id], - 'authorizations': [{ - 'userId': 'test', - 'level': 'OWN' - }] - }) - mocker.patch.object(db, 'delete_one', return_value={}) - mocker.patch.object(db, 'update', return_value=None) - res = client.delete(f'/topologies/{test_id}') - assert '200' in res.status - - -def test_delete_nonexistent_topology(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.delete(f'/topologies/{test_id}').status diff --git a/opendc-web/opendc-web-api/tests/api/test_traces.py b/opendc-web/opendc-web-api/tests/api/test_traces.py deleted file mode 100644 index 0b252c2f..00000000 --- a/opendc-web/opendc-web-api/tests/api/test_traces.py +++ /dev/null @@ -1,40 +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 opendc.exts import db - -test_id = 24 * '1' - - -def test_get_traces(client, mocker): - mocker.patch.object(db, 'fetch_all', return_value=[]) - assert '200' in client.get('/traces/').status - - -def test_get_trace_non_existing(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value=None) - assert '404' in client.get(f'/traces/{test_id}').status - - -def test_get_trace(client, mocker): - mocker.patch.object(db, 'fetch_one', return_value={'name': 'test trace'}) - res = client.get(f'/traces/{test_id}') - assert 'name' in res.json['data'] - assert '200' in res.status |
