diff --git a/.zuul.yaml b/.zuul.yaml index d8a4190..ffbb032 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -7,6 +7,9 @@ - context: . repository: vexxhost/atmosphere-ingress target: atmosphere-ingress + - context: . + repository: vexxhost/atmosphere-usage + target: atmosphere-usage - job: name: atmosphere:image:upload diff --git a/Dockerfile b/Dockerfile index c7c81ee..42cd07f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,3 +27,6 @@ ENV FLASK_APP=atmosphere.app \ FROM atmosphere AS atmosphere-ingress ENV UWSGI_WSGI_FILE=/usr/local/bin/atmosphere-ingress-wsgi + +FROM atmosphere AS atmosphere-usage +ENV UWSGI_WSGI_FILE=/usr/local/bin/atmosphere-usage-wsgi \ No newline at end of file diff --git a/atmosphere/api/usage.py b/atmosphere/api/usage.py new file mode 100644 index 0000000..ba31428 --- /dev/null +++ b/atmosphere/api/usage.py @@ -0,0 +1,62 @@ +# Copyright 2020 VEXXHOST, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Usage API.""" + +from datetime import datetime + +from flask import abort +from flask import Blueprint +from flask import request +from flask import jsonify +from keystonemiddleware import auth_token +from oslo_config import cfg + +from atmosphere.app import create_app +from atmosphere import models + +CONF = cfg.CONF + +blueprint = Blueprint('usage', __name__) + + +def init_application(config=None): + """Create usage API application.""" + app = create_app(config) + app.register_blueprint(blueprint) + + cfg.CONF([]) + authtoken_config = dict(CONF.keystone_authtoken) + authtoken_config['log_name'] = app.name + + app.wsgi_app = auth_token.AuthProtocol(app.wsgi_app, authtoken_config) + return app + + +@blueprint.route('/v1/resources') +def list_resources(): + """List all resources for a specific project.""" + # Project ID from request (or allow override if admin) + project_id = request.headers['X-Project-Id'] + if 'admin' in request.headers['X-Roles'] and 'project_id' in request.args: + project_id = request.args['project_id'] + + try: + start = datetime.fromisoformat(request.args['start']) + end = datetime.fromisoformat(request.args['end']) + except (KeyError, ValueError): + abort(400) + + resources = models.Resource.get_all_by_time_range(start, end, project_id) + return jsonify(resources) diff --git a/atmosphere/tests/unit/api/test_ingress.py b/atmosphere/tests/unit/api/test_ingress.py index 7f18f64..1cf187f 100644 --- a/atmosphere/tests/unit/api/test_ingress.py +++ b/atmosphere/tests/unit/api/test_ingress.py @@ -15,8 +15,25 @@ from dateutil.relativedelta import relativedelta import pytest +from atmosphere.api import ingress from atmosphere.tests.unit import fake from atmosphere import models +from atmosphere.models import db + + +@pytest.fixture +def app(): + app = ingress.init_application() + app.config['TESTING'] = True + app.config['SQLALCHEMY_ECHO'] = True + return app + + +@pytest.fixture +def _db(app): + db.init_app(app) + db.create_all() + return db @pytest.mark.usefixtures("client", "db_session") diff --git a/atmosphere/tests/unit/api/test_usage.py b/atmosphere/tests/unit/api/test_usage.py new file mode 100644 index 0000000..f118462 --- /dev/null +++ b/atmosphere/tests/unit/api/test_usage.py @@ -0,0 +1,37 @@ +# Copyright 2020 VEXXHOST, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dateutil.relativedelta import relativedelta +import pytest + +from atmosphere.api import usage +from atmosphere.tests.unit import fake +from atmosphere import models +from atmosphere.models import db + + +@pytest.fixture +def app(): + app = usage.init_application() + app.config['TESTING'] = True + app.config['SQLALCHEMY_ECHO'] = True + return app + + +@pytest.mark.usefixtures("client") +class TestResourceNoAuth: + def test_get_resources(self, client): + response = client.get('/v1/resources') + + assert response.status_code == 401 diff --git a/atmosphere/tests/unit/conftest.py b/atmosphere/tests/unit/conftest.py index acf2d47..cb009f3 100644 --- a/atmosphere/tests/unit/conftest.py +++ b/atmosphere/tests/unit/conftest.py @@ -18,7 +18,6 @@ from flask_sqlalchemy import SQLAlchemy from atmosphere.app import create_app from atmosphere.api import ingress -from atmosphere.models import db @pytest.fixture(params=[ @@ -36,19 +35,3 @@ from atmosphere.models import db ]) def ignored_event(request): yield request.param - - -@pytest.fixture -def app(): - app = create_app() - app.config['TESTING'] = True - app.config['SQLALCHEMY_ECHO'] = True - app.register_blueprint(ingress.blueprint) - return app - - -@pytest.fixture -def _db(app): - db.init_app(app) - db.create_all() - return db diff --git a/atmosphere/tests/unit/test_models.py b/atmosphere/tests/unit/test_models.py index 56ee890..8d585dc 100644 --- a/atmosphere/tests/unit/test_models.py +++ b/atmosphere/tests/unit/test_models.py @@ -22,11 +22,28 @@ from dateutil.relativedelta import relativedelta from freezegun import freeze_time import before_after +from atmosphere.api import ingress from atmosphere import models +from atmosphere.models import db from atmosphere import exceptions from atmosphere.tests.unit import fake +@pytest.fixture +def app(): + app = ingress.init_application() + app.config['TESTING'] = True + app.config['SQLALCHEMY_ECHO'] = True + return app + + +@pytest.fixture +def _db(app): + db.init_app(app) + db.create_all() + return db + + class GetOrCreateTestMixin: def test_with_existing_object(self): event = fake.get_normalized_event() diff --git a/requirements.txt b/requirements.txt index cf82064..f17f5d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,6 @@ ceilometer Flask Flask-Migrate Flask-SQLAlchemy +keystonemiddleware python-dateutil PyMySQL diff --git a/setup.cfg b/setup.cfg index 7fbd9ce..8c9a1d1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,6 +8,7 @@ packages = [entry_points] wsgi_scripts = atmosphere-ingress-wsgi = atmosphere.api.ingress:init_application + atmosphere-usage-wsgi = atmosphere.api.usage:init_application [tool:pytest] mocked-sessions=atmosphere.models.db.session diff --git a/tox.ini b/tox.ini index bf4c8a9..1bbb1d7 100644 --- a/tox.ini +++ b/tox.ini @@ -4,10 +4,12 @@ skipsdist = True [testenv] envdir = {toxworkdir}/shared usedevelop = True -setenv = - FLASK_APP=atmosphere.app passenv = + OS_* + FLASK_APP DATABASE_URI +setenv = + FLASK_ENV=development deps = -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt