From 3df036225cc102f045af800ce1f9335d6e0b769c Mon Sep 17 00:00:00 2001 From: Matt Jia Date: May 31 2017 00:24:21 +0000 Subject: prototype of the POST /decision API --- diff --git a/README.md b/README.md index 05bb11e..e76120c 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,49 @@ Greenwave is a service to decide whether a software artifact can pass certain gating points in a software delivery pipeline, based on test results stored in [ResultsDB](https://pagure.io/taskotron/resultsdb) and waivers stored in [WaiverDB](https://pagure.io/waiverdb). + +## Quick development setup + +Set up a python virtualenv: + + $ sudo dnf install python-virtualenv + $ virtualenv env_greenwave + $ source env_greenwave/bin/activate + $ pip install -r requirements.txt + $ pip install -r dev-requirements.txt + +Install the project: + + $ python setup.py develop + +Run the server: + + $ python run-dev-server.py + +The server is now running at and API calls can be sent to +. + +## Adjusting configuration + +You can configure this app by copying `conf/settings.py.example` into +`conf/setting.py` and adjusting values as you see fit. It overrides default +values in `greenwave/config.py`. + +## Running test suite + +You can run this test suite with the following command:: + + $ py.test greenwave/tests/ + +To test against all supported versions of Python, you can use tox:: + + $ sudo dnf install python3-tox + $ tox + +## Building the docs + +You can view the docs locally with:: + + $ cd docs + $ make html + $ firefox _build/html/index.html diff --git a/conf/settings.py.example b/conf/settings.py.example new file mode 100644 index 0000000..cb5827f --- /dev/null +++ b/conf/settings.py.example @@ -0,0 +1,6 @@ +# Copy this file to `conf/settings.py` to put it into effect. It overrides the values defined +# in `greenwave/config.py`. +SECRET_KEY = 'replace-me-with-something-random' +JOURNAL_LOGGING = False +HOST= '0.0.0.0' +PORT = 5005 diff --git a/dev-requirements.txt b/dev-requirements.txt index 0f26762..a96f607 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,6 +3,7 @@ flake8 mock pytest pytest-cov +requests_mock # Documentation build requirements sphinx diff --git a/greenwave/api_v1.py b/greenwave/api_v1.py new file mode 100644 index 0000000..103d0e2 --- /dev/null +++ b/greenwave/api_v1.py @@ -0,0 +1,121 @@ + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# + +import requests +from flask import Blueprint, request, current_app, jsonify +from werkzeug.exceptions import BadRequest, NotFound, UnsupportedMediaType +from greenwave.policies import policies + +api = (Blueprint('api_v1', __name__)) + + +@api.route('/decision', methods=['POST']) +def make_decision(): + """ + Make a decision after evaluating all applicable policies based on test + results. The request must be + :mimetype:`application/json`. + + :jsonparam string product_version: The product version string used for querying WaiverDB. + :jsonparam string decision_context: The decision context string. + :jsonparam array subject: A list of items about which the caller is requesting a decision + used for querying ResultsDB. For example, a list of build NVRs. + :statuscode 200: A decision was made. + :statuscode 400: Invalid data was given. + """ + if request.get_json(): + if ('product_version' not in request.get_json() or + not request.get_json()['product_version']): + raise BadRequest('Missing required product version') + if ('decision_context' not in request.get_json() or + not request.get_json()['decision_context']): + raise BadRequest('Missing required decision context') + if ('subject' not in request.get_json() or + not request.get_json()['subject']): + raise BadRequest('Missing required subject') + else: + raise UnsupportedMediaType('No JSON payload in request') + if not isinstance(request.get_json()['subject'], list): + raise BadRequest('Invalid subject, must be a list of items') + product_version = request.get_json()['product_version'] + decision_context = request.get_json()['decision_context'] + applicable_policies = {} + for policy_id, policy in policies.iteritems(): + if product_version == policy['product_version'] and \ + decision_context == policy['decision_context']: + applicable_policies[policy_id] = policy + if not applicable_policies: + raise NotFound('Cannot find any applicable policies for %s' % product_version) + subjects = [item.strip() for item in request.get_json()['subject'] if item] + policies_satisified = True + summary = [] + unsatisfied_requirements = [] + with requests.Session() as s: + for policy_id, policy in applicable_policies.iteritems(): + for item in subjects: + res = s.get('{0}/results?item={1}&testcases={2}'.format( + current_app.config['RESULTSDB_API_URL'], item, ','.join(policy['rules'])) + ) + res.raise_for_status() + results = res.json()['data'] + total_failed_results = 0 + if results: + for result in results: + if result['outcome'] not in ('PASSED', 'INFO'): + # query WaiverDB to check whether the result has a waiver + res = s.get('{0}/waivers/?project_version={1}&result_id={2}'.format( + current_app.config['WAIVERDB_API_URL'], product_version, + result['id']) + ) + res.raise_for_status() + waiver = res.json()['data'] + if not waiver or not waiver[0]['waived']: + policies_satisified = False + total_failed_results += 1 + unsatisfied_requirements.append({ + 'type': 'test-result-failed', + 'item': item, + 'testcase': result['testcase']['name'], + 'result_id': result['id']}) + # find missing results + rules_applied = [result['testcase']['name'] for result in results] + for rule in policy['rules']: + if rule not in rules_applied: + total_failed_results += 1 + unsatisfied_requirements.append({ + 'type': 'test-result-missing', + 'item': item, + 'testcase': rule}) + if total_failed_results: + summary.append( + '{0}: {1} of {2} required tests failed, the policy {3} is not satisfied' + .format(item, total_failed_results, len(policy['rules']), + policy_id)) + else: + summary.append( + '%s: policy %s is satisfied as all required tests are passing' % ( + item, policy_id)) + else: + policies_satisified = False + summary.append('%s: no test results found' % item) + for rule in policy['rules']: + unsatisfied_requirements.append({ + 'type': 'test-result-missing', + 'item': item, + 'testcase': rule}) + res = { + 'policies_satisified': policies_satisified, + 'summary': '\n'.join(summary), + 'applicable_policies': list(applicable_policies.keys()), + 'unsatisfied_requirements': unsatisfied_requirements + } + return jsonify(res), 200 diff --git a/greenwave/app_factory.py b/greenwave/app_factory.py new file mode 100644 index 0000000..6ed3b86 --- /dev/null +++ b/greenwave/app_factory.py @@ -0,0 +1,48 @@ + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +import os + +from flask import Flask +from greenwave.logger import init_logging +from greenwave.api_v1 import api + + +def load_config(app): + # Load default config, then override that with a config file + if os.getenv('DEV') == 'true': + default_config_obj = 'greenwave.config.DevelopmentConfig' + default_config_file = os.getcwd() + '/conf/settings.py' + elif os.getenv('TEST') == 'true': + default_config_obj = 'greenwave.config.TestingConfig' + default_config_file = os.getcwd() + '/conf/settings.py' + else: + default_config_obj = 'greenwave.config.ProductionConfig' + default_config_file = '/etc/greenwave/settings.py' + app.config.from_object(default_config_obj) + config_file = os.environ.get('GREENWAVE_CONFIG', default_config_file) + app.config.from_pyfile(config_file) + + +# applicaiton factory http://flask.pocoo.org/docs/0.12/patterns/appfactories/ +def create_app(config_obj=None): + app = Flask(__name__) + if config_obj: + app.config.from_object(config_obj) + else: + load_config(app) + if app.config['PRODUCTION'] and app.secret_key == 'replace-me-with-something-random': + raise Warning("You need to change the app.secret_key value for production") + # initialize logging + init_logging(app) + # register blueprints + app.register_blueprint(api, url_prefix="/api/v1.0") + return app diff --git a/greenwave/config.py b/greenwave/config.py new file mode 100644 index 0000000..1d468f0 --- /dev/null +++ b/greenwave/config.py @@ -0,0 +1,39 @@ + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + + +class Config(object): + """ + A GreenWave Flask configuration. + """ + DEBUG = True + JOURNAL_LOGGING = False + HOST = '0.0.0.0' + PORT = 5005 + PRODUCTION = False + SECRET_KEY = 'replace-me-with-something-random' + RESULTSDB_API_URL = 'https://taskotron.fedoraproject.org/resultsdb_api/api/v2.0' + WAIVERDB_API_URL = 'https://waiverdb.fedoraproject.org/api/v1.0' + + +class ProductionConfig(Config): + DEBUG = False + PRODUCTION = True + + +class DevelopmentConfig(Config): + RESULTSDB_API_URL = 'https://taskotron.stg.fedoraproject.org/resultsdb_api/api/v2.0' + WAIVERDB_API_URL = 'http://waiverdb-dev.fedorainfracloud.org/api/v1.0' + + +class TestingConfig(Config): + RESULTSDB_API_URL = 'https://resultsdb.domain.local/api/v2.0' + WAIVERDB_API_URL = 'https://waiverdb.domain.local/api/v1.0' diff --git a/greenwave/logger.py b/greenwave/logger.py new file mode 100644 index 0000000..49e0c1a --- /dev/null +++ b/greenwave/logger.py @@ -0,0 +1,38 @@ + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +import logging +import sys +import systemd.journal + + +def log_to_stdout(app, level=logging.INFO): + fmt = '[%(filename)s:%(lineno)d] ' if app.debug else '%(module)-12s ' + fmt += '%(asctime)s %(levelname)-7s %(message)s' + datefmt = '%Y-%m-%d %H:%M:%S' + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setLevel(level) + stream_handler.setFormatter(logging.Formatter(fmt=fmt, datefmt=datefmt)) + app.logger.addHandler(stream_handler) + + +def log_to_journal(app, level=logging.INFO): + journal_handler = systemd.journal.JournalHandler() + journal_handler.setLevel(level) + app.logger.addHandler(journal_handler) + + +def init_logging(app): + log_level = logging.DEBUG if app.debug else logging.INFO + if app.config['JOURNAL_LOGGING']: + log_to_journal(app, level=log_level) + else: + log_to_stdout(app, level=log_level) diff --git a/greenwave/policies.py b/greenwave/policies.py new file mode 100644 index 0000000..adbe294 --- /dev/null +++ b/greenwave/policies.py @@ -0,0 +1,28 @@ + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# + +policies = { + # Mimic the default Errata rule used for RHEL-7 https://errata.devel.redhat.com/workflow_rules/1 + # In Errata, in order to transition to QE state, an advisory must complete rpmdiff test. + # A completed rpmdiff test could be some dist.rpmdiff.* testcases in ResultsDB and all the + # tests need to be passed. + '1': { + 'product_version': 'rhel-7', + 'decision_context': 'errta_newfile_to_qe', + 'rules': [ + 'dist.rpmdiff.comparison.xml_validity', + 'dist.rpmdiff.comparison.virus_scan', + 'dist.rpmdiff.comparison.upstream_source', + 'dist.rpmdiff.comparison.symlinks', + 'dist.rpmdiff.comparison.binary_stripping'] + } +} diff --git a/greenwave/tests/conftest.py b/greenwave/tests/conftest.py new file mode 100644 index 0000000..3c288e3 --- /dev/null +++ b/greenwave/tests/conftest.py @@ -0,0 +1,37 @@ + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# + +import pytest +from greenwave.app_factory import create_app + + +@pytest.fixture(scope='session') +def app(request): + app = create_app('greenwave.config.TestingConfig') + # Establish an application context before running the tests. + ctx = app.app_context() + ctx.push() + + def teardown(): + ctx.pop() + + request.addfinalizer(teardown) + return app + + +@pytest.yield_fixture +def client(app): + """A Flask test client. An instance of :class:`flask.testing.TestClient` + by default. + """ + with app.test_client() as client: + yield client diff --git a/greenwave/tests/test_api_v10.py b/greenwave/tests/test_api_v10.py new file mode 100644 index 0000000..6dd7cfe --- /dev/null +++ b/greenwave/tests/test_api_v10.py @@ -0,0 +1,301 @@ + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +import json +import requests_mock +from flask import current_app +from mock import patch + + +def test_cannot_make_decision_without_product_version(client): + data = { + 'decision_context': 'errta_newfile_to_qe', + 'subject': ['foo-1.0.0-1.el7'] + } + r = client.post('/api/v1.0/decision', data=json.dumps(data), + content_type='application/json') + assert r.status_code == 400 + assert 'Missing required product version' in r.get_data() + + +def test_cannot_make_decision_without_decision_context(client): + data = { + 'product_version': 'rhel-7', + 'subject': ['foo-1.0.0-1.el7'] + } + r = client.post('/api/v1.0/decision', data=json.dumps(data), + content_type='application/json') + assert r.status_code == 400 + assert 'Missing required decision context' in r.get_data() + + +def test_cannot_make_decision_without_subject(client): + data = { + 'decision_context': 'errta_newfile_to_qe', + 'product_version': 'rhel-7', + } + r = client.post('/api/v1.0/decision', data=json.dumps(data), + content_type='application/json') + assert r.status_code == 400 + assert 'Missing required subject' in r.get_data() + + +def test_404_for_inapplicable_policies(client): + data = { + 'decision_context': 'dummpy_decision', + 'product_version': 'rhel-7', + 'subject': ['foo-1.0.0-1.el7'] + } + r = client.post('/api/v1.0/decision', data=json.dumps(data), + content_type='application/json') + assert r.status_code == 404 + assert 'Cannot find any applicable policies for %s' % data['product_version'] in r.get_data() + + +def test_make_a_decison_on_passed_result(client): + with requests_mock.Mocker() as m: + mocked_results = { + "data": [ + { + "data": { + "item": [ + "foo-1.0.0-2.el7" + ] + }, + "groups": [ + "5d307e4f-1ade-4c41-9e67-e5a73d5cdd07" + ], + "href": "https://resultsdb.domain.local/api/v2.0/results/331284", + "id": 331284, + "note": "", + "outcome": "PASSED", + "ref_url": "https://rpmdiff.domain.local/run/97683/26", + "submit_time": "2017-05-19T04:41:13.957729", + "testcase": { + "href": 'https://resultsdb.domain.local/api/v2.0/testcases/' + 'dist.rpmdiff.comparison.xml_validity', + "name": "dist.rpmdiff.comparison.xml_validity", + "ref_url": "https://docs.domain.local/display/HTD/rpmdiff-valid-file" + } + } + ] + } + m.register_uri('GET', '{}/results?item={}&testcases={}'.format( + current_app.config['RESULTSDB_API_URL'], + 'foo-1.0.0-2.el7', + 'dist.rpmdiff.comparison.xml_validity' + ), json=mocked_results) + dummy_policies = { + '1': { + 'product_version': 'rhel-7', + 'decision_context': 'dummpy_decision', + 'rules': ['dist.rpmdiff.comparison.xml_validity'] + } + } + with patch.dict('greenwave.policies.policies', dummy_policies): + data = { + 'decision_context': 'dummpy_decision', + 'product_version': 'rhel-7', + 'subject': ['foo-1.0.0-2.el7'] + } + r = client.post('/api/v1.0/decision', data=json.dumps(data), + content_type='application/json') + assert r.status_code == 200 + res_data = json.loads(r.get_data(as_text=True)) + assert res_data['policies_satisified'] == True + assert res_data['applicable_policies'] == ['1'] + assert res_data['summary'] == 'foo-1.0.0-2.el7: policy 1 is satisfied as all required' \ + ' tests are passing' + + +def test_make_a_decison_on_failed_result_with_waiver(client): + with requests_mock.Mocker() as m: + mocked_results = { + "data": [ + { + "data": { + "item": [ + "foo-1.0.0-2.el7" + ] + }, + "groups": [ + "5d307e4f-1ade-4c41-9e67-e5a73d5cdd07" + ], + "href": "https://resultsdb.domain.local/api/v2.0/results/331284", + "id": 331284, + "note": "", + "outcome": "FAILED", + "ref_url": "https://rpmdiff.domain.local/run/97683/26", + "submit_time": "2017-05-19T04:41:13.957729", + "testcase": { + "href": 'https://resultsdb.domain.local/api/v2.0/testcases/' + 'dist.rpmdiff.comparison.xml_validity', + "name": "dist.rpmdiff.comparison.xml_validity", + "ref_url": "https://docs.domain.local/display/HTD/rpmdiff-valid-file" + } + } + ] + } + m.register_uri('GET', '{}/results?item={}&testcases={}'.format( + current_app.config['RESULTSDB_API_URL'], + 'foo-1.0.0-2.el7', + 'dist.rpmdiff.comparison.xml_validity' + ), json=mocked_results) + mocked_waiver = { + "data": [ + { + "id": 1, + "result_id": 331284, + "username": 'fool', + "comment": 'it broke', + "waived": True, + "timestamp": '2017-05-17T03:13:31.735858', + "product_version": 'rhel-7' + } + ] + } + m.register_uri('GET', '{}/waivers/?result_id={}&project_version={}'.format( + current_app.config['WAIVERDB_API_URL'], + 331284, + 'rhel-7' + ), json=mocked_waiver) + dummy_policies = { + '1': { + 'product_version': 'rhel-7', + 'decision_context': 'dummpy_decision', + 'rules': ['dist.rpmdiff.comparison.xml_validity'] + } + } + with patch.dict('greenwave.policies.policies', dummy_policies): + data = { + 'decision_context': 'dummpy_decision', + 'product_version': 'rhel-7', + 'subject': ['foo-1.0.0-2.el7'] + } + r = client.post('/api/v1.0/decision', data=json.dumps(data), + content_type='application/json') + assert r.status_code == 200 + res_data = json.loads(r.get_data(as_text=True)) + assert res_data['policies_satisified'] == True + assert res_data['applicable_policies'] == ['1'] + assert res_data['summary'] == 'foo-1.0.0-2.el7: policy 1 is satisfied as all required' \ + ' tests are passing' + + +def test_make_a_decison_on_failed_result(client): + with requests_mock.Mocker() as m: + mocked_results = { + "data": [ + { + "data": { + "item": [ + "foo-1.0.0-2.el7" + ] + }, + "groups": [ + "5d307e4f-1ade-4c41-9e67-e5a73d5cdd07" + ], + "href": "https://resultsdb.domain.local/api/v2.0/results/331284", + "id": 331284, + "note": "", + "outcome": "FAILED", + "ref_url": "https://rpmdiff.domain.local/run/97683/26", + "submit_time": "2017-05-19T04:41:13.957729", + "testcase": { + "href": 'https://resultsdb.domain.local/api/v2.0/testcases/' + 'dist.rpmdiff.comparison.xml_validity', + "name": "dist.rpmdiff.comparison.xml_validity", + "ref_url": "https://docs.domain.local/display/HTD/rpmdiff-valid-file" + } + } + ] + } + m.register_uri('GET', '{}/results?item={}&testcases={}'.format( + current_app.config['RESULTSDB_API_URL'], + 'foo-1.0.0-2.el7', + 'dist.rpmdiff.comparison.xml_validity,dist.rpmdiff.comparison.virus_scan' + ), json=mocked_results) + m.register_uri('GET', '{}/waivers/?result_id={}&project_version={}'.format( + current_app.config['WAIVERDB_API_URL'], + 331284, + 'rhel-7' + ), json={"data": []}) + dummy_policies = { + '1': { + 'product_version': 'rhel-7', + 'decision_context': 'dummpy_decision', + 'rules': ['dist.rpmdiff.comparison.xml_validity', + 'dist.rpmdiff.comparison.virus_scan'] + } + } + with patch.dict('greenwave.policies.policies', dummy_policies): + data = { + 'decision_context': 'dummpy_decision', + 'product_version': 'rhel-7', + 'subject': ['foo-1.0.0-2.el7'] + } + r = client.post('/api/v1.0/decision', data=json.dumps(data), + content_type='application/json') + assert r.status_code == 200 + res_data = json.loads(r.get_data(as_text=True)) + assert res_data['policies_satisified'] == False + assert res_data['applicable_policies'] == ['1'] + assert res_data['summary'] == 'foo-1.0.0-2.el7: 2 of 2 required tests' \ + ' failed, the policy 1 is not satisfied' + expected_unsatisfied_requirements = [ + { + 'item': 'foo-1.0.0-2.el7', + 'result_id': 331284, + 'testcase': 'dist.rpmdiff.comparison.xml_validity', + 'type': 'test-result-failed' + }, + { + 'item': 'foo-1.0.0-2.el7', + 'testcase': 'dist.rpmdiff.comparison.virus_scan', + 'type': 'test-result-missing' + }] + assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements + + +def test_make_a_decison_on_no_results(client): + with requests_mock.Mocker() as m: + m.register_uri('GET', '{}/results?item={}&testcases={}'.format( + current_app.config['RESULTSDB_API_URL'], + 'foo-1.0.0-2.el7', + 'dist.rpmdiff.comparison.xml_validity' + ), json={"data": []}) + dummy_policies = { + '1': { + 'product_version': 'rhel-7', + 'decision_context': 'dummpy_decision', + 'rules': ['dist.rpmdiff.comparison.xml_validity'] + } + } + with patch.dict('greenwave.policies.policies', dummy_policies): + data = { + 'decision_context': 'dummpy_decision', + 'product_version': 'rhel-7', + 'subject': ['foo-1.0.0-2.el7'] + } + r = client.post('/api/v1.0/decision', data=json.dumps(data), + content_type='application/json') + assert r.status_code == 200 + res_data = json.loads(r.get_data(as_text=True)) + assert res_data['policies_satisified'] == False + assert res_data['applicable_policies'] == ['1'] + assert res_data['summary'] == 'foo-1.0.0-2.el7: no test results found' + expected_unsatisfied_requirements = [ + { + 'item': 'foo-1.0.0-2.el7', + 'testcase': 'dist.rpmdiff.comparison.xml_validity', + 'type': 'test-result-missing' + }] + assert res_data['unsatisfied_requirements'] == expected_unsatisfied_requirements diff --git a/requirements.txt b/requirements.txt index e39cea1..79c347b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ flask -sqlalchemy +systemd +requests diff --git a/run-dev-server.py b/run-dev-server.py new file mode 100644 index 0000000..a72d21c --- /dev/null +++ b/run-dev-server.py @@ -0,0 +1,22 @@ +#!/usr/bin/python + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# + +from greenwave.app_factory import create_app + +if __name__ == '__main__': + app = create_app('greenwave.config.DevelopmentConfig') + app.run( + host=app.config['HOST'], + port=app.config['PORT'], + debug=app.config['DEBUG'], + )