Commit 8c49d003 authored by Jonathan Schaeffer's avatar Jonathan Schaeffer

Merge branch '12factapp' into 'master'

Dockerise and use env variables as config

See merge request !1
parents 2e136f4d b056cbfa
Pipeline #46922 failed with stage
in 44 seconds
......@@ -13,9 +13,3 @@ dist/
build/
*.egg-info/
.tox/
# Ignore configurations for specific environments
eidawsauth/config.yaml
eidawsauth/configurations/*
!eidawsauth/configurations/default.py
!eidawsauth/configurations/__init__.py
image: gricad-registry.univ-grenoble-alpes.fr/kubernetes-alpes/buildah:latest
stages:
- build
- deploy
variables:
REGISTRY_LOGIN: buildah login -u gitlab-ci-token -p $CI_REGISTRY_PASSWORD
REGISTRY_LOGOUT: buildah logout
IMAGE_BUILD: buildah build-using-dockerfile --storage-driver vfs --format docker
IMAGE_PUSH: buildah push --storage-driver vfs
before_script:
- $REGISTRY_LOGIN $CI_REGISTRY
after_script:
- $REGISTRY_LOGOUT $CI_REGISTRY
build ws-dataselect: &build
tags:
- dind
stage: build
only:
- master
variables:
DOCKERFILE: Dockerfile
IMAGE_NAME: $CI_REGISTRY_IMAGE/eidawsauth:$CI_COMMIT_SHORT_SHA
script:
- $IMAGE_BUILD --file $DOCKERFILE $BUILD_ARG --tag $IMAGE_NAME .
- $IMAGE_PUSH $IMAGE_NAME $IMAGE_NAME
FROM python:3.8-slim
MAINTAINER RESIF DC <resif-dc@univ-grenoble-alpes.fr>
RUN apt-get update && apt-get -y install gnupg
COPY setup/gpghome /gpghome
RUN pip3 install --no-cache-dir gunicorn
COPY requirements.txt /
RUN pip3 install --no-cache-dir -r /requirements.txt
COPY eidawsauth/ /app/
WORKDIR /app
CMD ["gunicorn", "-b", "0.0.0.0:8000", "eidawsauth"]
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
click = "==7.1.1"
itsdangerous = "==1.1.0"
psycopg2-binary = "*"
python-gnupg = "==0.4.5"
Flask = "==1.1.1"
Jinja2 = "==2.11.1"
MarkupSafe = "==1.1.1"
Werkzeug = "==1.0.0"
[requires]
python_version = "3.8"
{
"_meta": {
"hash": {
"sha256": "e481fd131256f95b5bb86e069f0550de18c6851d17e1197183700ca81a93c4dd"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.8"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"click": {
"hashes": [
"sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
"sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
],
"index": "pypi",
"version": "==7.1.1"
},
"flask": {
"hashes": [
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
],
"index": "pypi",
"version": "==1.1.1"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"index": "pypi",
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250",
"sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"
],
"index": "pypi",
"version": "==2.11.1"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"index": "pypi",
"version": "==1.1.1"
},
"psycopg2-binary": {
"hashes": [
"sha256:008da3ab51adc70a5f1cfbbe5db3a22607ab030eb44bcecf517ad11a0c2b3cac",
"sha256:07cf82c870ec2d2ce94d18e70c13323c89f2f2a2628cbf1feee700630be2519a",
"sha256:08507efbe532029adee21b8d4c999170a83760d38249936038bd0602327029b5",
"sha256:107d9be3b614e52a192719c6bf32e8813030020ea1d1215daa86ded9a24d8b04",
"sha256:17a0ea0b0eabf07035e5e0d520dabc7950aeb15a17c6d36128ba99b2721b25b1",
"sha256:3286541b9d85a340ee4ed42732d15fc1bb441dc500c97243a768154ab8505bb5",
"sha256:3939cf75fc89c5e9ed836e228c4a63604dff95ad19aed2bbf71d5d04c15ed5ce",
"sha256:40abc319f7f26c042a11658bf3dd3b0b3bceccf883ec1c565d5c909a90204434",
"sha256:51f7823f1b087d2020d8e8c9e6687473d3d239ba9afc162d9b2ab6e80b53f9f9",
"sha256:6bb2dd006a46a4a4ce95201f836194eb6a1e863f69ee5bab506673e0ca767057",
"sha256:702f09d8f77dc4794651f650828791af82f7c2efd8c91ae79e3d9fe4bb7d4c98",
"sha256:7036ccf715925251fac969f4da9ad37e4b7e211b1e920860148a10c0de963522",
"sha256:7b832d76cc65c092abd9505cc670c4e3421fd136fb6ea5b94efbe4c146572505",
"sha256:8f74e631b67482d504d7e9cf364071fc5d54c28e79a093ff402d5f8f81e23bfa",
"sha256:930315ac53dc65cbf52ab6b6d27422611f5fb461d763c531db229c7e1af6c0b3",
"sha256:96d3038f5bd061401996614f65d27a4ecb62d843eb4f48e212e6d129171a721f",
"sha256:a20299ee0ea2f9cca494396ac472d6e636745652a64a418b39522c120fd0a0a4",
"sha256:a34826d6465c2e2bbe9d0605f944f19d2480589f89863ed5f091943be27c9de4",
"sha256:a69970ee896e21db4c57e398646af9edc71c003bc52a3cc77fb150240fefd266",
"sha256:b9a8b391c2b0321e0cd7ec6b4cfcc3dd6349347bd1207d48bcb752aa6c553a66",
"sha256:ba13346ff6d3eb2dca0b6fa0d8a9d999eff3dcd9b55f3a890f12b0b6362b2b38",
"sha256:bb0608694a91db1e230b4a314e8ed00ad07ed0c518f9a69b83af2717e31291a3",
"sha256:c8830b7d5f16fd79d39b21e3d94f247219036b29b30c8270314c46bf8b732389",
"sha256:cac918cd7c4c498a60f5d2a61d4f0a6091c2c9490d81bc805c963444032d0dab",
"sha256:cc30cb900f42c8a246e2cb76539d9726f407330bc244ca7729c41a44e8d807fb",
"sha256:ccdc6a87f32b491129ada4b87a43b1895cf2c20fdb7f98ad979647506ffc41b6",
"sha256:d1a8b01f6a964fec702d6b6dac1f91f2b9f9fe41b310cbb16c7ef1fac82df06d",
"sha256:e004db88e5a75e5fdab1620fb9f90c9598c2a195a594225ac4ed2a6f1c23e162",
"sha256:eb2f43ae3037f1ef5e19339c41cf56947021ac892f668765cd65f8ab9814192e",
"sha256:fa466306fcf6b39b8a61d003123d442b23707d635a5cb05ac4e1b62cc79105cd"
],
"index": "pypi",
"version": "==2.8.5"
},
"python-gnupg": {
"hashes": [
"sha256:3353e59949cd2c15efbf1fca45e347d8a22f4bed0d93e9b89b2657bda19cec05",
"sha256:c095a41f310ad7a4fd393406660ac9bd6c175ccaa0f072f9c18f33be8130a27a"
],
"index": "pypi",
"version": "==0.4.5"
},
"werkzeug": {
"hashes": [
"sha256:169ba8a33788476292d04186ab33b01d6add475033dfc07215e6d219cc077096",
"sha256:6dc65cf9091cf750012f56f2cad759fa9e879f511b5ff8685e456b4e3bf90d16"
],
"index": "pypi",
"version": "==1.0.0"
}
},
"develop": {}
}
......@@ -75,7 +75,21 @@ http POST localhost:8000 < token.asc
## Running tests
## Configuration
The configuration is made through environment variables.
### Databases access
For convenience, all the database connection parameters (except password) are hardcoded when `RUNMODE` is set to `production` or `preproduction`
Look at `eidawsauth/config.py` to see the list en environments needed. Tere are sensitive defaults for RESIF.
Minimal environment for RESIF is
* `RUNMODE`: set to `production` or `preproduction`
* `PGPASSWORD`: if same user is set for both databases, this value will be used for both. Otherwise, set `RESIFINV_PGPASSWORD` and `RESIFAUTH_PGPASSWORD`
# Explanations
......
import os
class config():
"""
Default conifguration
"""
DEBUG=False
TESTING=False
LOGLEVEL='INFO'
ENVIRONMENT='default'
EMAIL='resif-dc@univ-grenoble-alpes.fr'
GNUPGHOMEDIR='../../tests/test_files/gpg_home'
_dbpassword = os.environ.get('DBPASS')
AUTHDBPORT=5432
AUTHDBHOST='localhost'
AUTHDBNAME='resifAuth'
AUTHDBUSER='eidawsauth'
AUTHDBPASSWORD=_dbpassword
PRIVILEGEDBHOST='localhost'
PRIVILEGEDBPORT=5432
PRIVILEGEDBNAME='resifInv-Prod'
PRIVILEGEDBUSER='eidawsauth'
PRIVILEGEDBPASSWORD=_dbpassword
EPOS_FDSN_MAP={
'/epos/alparray': {'networkcode':'Z3', 'startyear':2015, 'endyear': 2022},
}
import gnupg
import re
import datetime
import random
......@@ -7,39 +6,25 @@ import logging
import os
from hashlib import md5
from flask import Flask, request, Response
import configurations
import psycopg2
import gnupg
from config import Configurator
from version import __version__
import importlib
application = Flask(__name__)
if 'RUNMODE' in os.environ:
try:
importlib.import_module('.'+os.environ.get('RUNMODE'), 'configurations')
application.config.from_object('configurations.'+os.environ.get('RUNMODE')+'.config')
except Exception as e:
raise e
# no configuration for this RUNMODE value
application = Flask(__name__)
if os.getenv('RUNMODE') == 'production':
application.logger.setLevel(logging.INFO)
else:
application.logger.setLevel(logging.DEBUG)
# Loglevel can be overrinden by LOGLEVEL env var :
if os.getenv('DEBUG') == 'true':
application.logger.setLevel(logging.DEBUG)
else:
application.config.from_object('configurations.default.config')
from logging.config import dictConfig
dictConfig({
'version': 1,
'formatters': {'default': {
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
}},
'handlers': {'wsgi': {
'class': 'logging.StreamHandler',
'stream': 'ext://flask.logging.wsgi_errors_stream',
'formatter': 'default'
}},
'root': {
'level': application.config['LOGLEVEL'],
'handlers': ['wsgi']
}
})
application.logger.setLevel(logging.INFO)
application.config.from_object(Configurator)
def wsshash(login, password):
"""
......@@ -57,10 +42,10 @@ def verify_token_signature(data, gpg_homedir):
def parse_input_data(data):
# Then we get the token :
token = re.search(r'{(?P<token>.*)}',str(data)).groupdict()['token']
logging.debug(token)
application.logger.debug(token)
d = dict([i for i in kv.split(':',1)] for kv in token.replace('"','').replace(' ','').split(','))
logging.debug("Transformed to dictionary : "+str(d))
application.logger.debug("Transformed to dictionary : %s", d)
return d
def register_login(login, password):
......@@ -71,18 +56,18 @@ def register_login(login, password):
"""
expiration_time=datetime.datetime.now()+datetime.timedelta(days=1)
try:
conn = psycopg2.connect(dbname= application.config['AUTHDBNAME'],
port = application.config['AUTHDBPORT'],
host = application.config['AUTHDBHOST'],
user= application.config['AUTHDBUSER'],
password = application.config['AUTHDBPASSWORD'])
conn = psycopg2.connect(dbname= application.config['RESIFAUTH_PGDATABASE'],
port = application.config['RESIFAUTH_PGPORT'],
host = application.config['RESIFAUTH_PGHOST'],
user= application.config['RESIFAUTH_PGUSER'],
password = application.config['RESIFAUTH_PGPASSWORD'])
cur = conn.cursor()
logging.debug("Connected to users database")
application.logger.debug("Connected to users database")
except Exception as e:
logging.error("Unable to connect to database %s as %s@%s:%s"%(application.config['AUTHDBNAME'],
application.config['AUTHDBUSER'],
application.config['AUTHDBHOST'],
application.config['AUTHDBPORT']))
application.logger.error("Unable to connect to database %s as %s@%s:%s", application.config['RESIFAUTH_PGDATABASE'],
application.config['RESIFAUTH_PGUSER'],
application.config['RESIFAUTH_PGHOST'],
application.config['RESIFAUTH_PGPORT'])
raise e
cur.execute("""
......@@ -105,37 +90,37 @@ def register_privileges(login, fdsn_refs):
- For each fdsn reference, insert the privilege in the access table
"""
try:
conn = psycopg2.connect(dbname= application.config['PRIVILEGEDBNAME'],
port = application.config['PRIVILEGEDBPORT'],
host = application.config['PRIVILEGEDBHOST'],
user= application.config['PRIVILEGEDBUSER'],
password = application.config['PRIVILEGEDBPASSWORD'])
conn = psycopg2.connect(dbname= application.config['RESIFINV_PGDATABASE'],
port = application.config['RESIFINV_PGPORT'],
host = application.config['RESIFINV_PGHOST'],
user= application.config['RESIFINV_PGUSER'],
password = application.config['RESIFINV_PGPASSWORD'])
cur = conn.cursor()
logging.debug("Connected to privileges database")
application.logger.debug("Connected to privileges database")
except Exception as e:
logging.error("Unable to connect to database %s as %s@%s:%s"%(application.config['PRIVILEGEDBNAME'],
application.config['PRIVILEGEDBUSER'],
application.config['PRIVILEGEDBHOST'],
application.config['PRIVILEGEDBPORT']))
application.logger.error("Unable to connect to database %s as %s@%s:%s", application.config['RESIFINV_PGDATABASE'],
application.config['RESIFINV_PGUSER'],
application.config['RESIFINV_PGHOST'],
application.config['RESIFINV_PGPORT'])
raise e
# Get the network id
for ref in fdsn_refs:
ref['login'] = login
ref['expires_at'] = datetime.datetime.now()+datetime.timedelta(days=1)
logging.info(ref)
application.logger.info(ref)
sql_request = "select network_id from networks where start_year=%(startyear)s and end_year=%(endyear)s and network=%(networkcode)s"
try:
cur.execute(sql_request, ref)
except psycopg2.Error as e:
logging.error(e.pgerror)
application.logger.error(e.pgerror)
else:
if cur.rowcount != 1:
logging.info(cur.mogrify(sql_request, ref))
logging.error("%d networks found for %s", cur.rowcount, ref)
application.logger.info(cur.mogrify(sql_request, ref))
application.logger.error("%d networks found for %s", cur.rowcount, ref)
raise NameError(f"{cur.rowcount} networks found for {ref}")
ref['networkid'] = cur.fetchone()[0]
logging.info("Inserting tupple in %s.eida_temp_users: %s", application.config['PRIVILEGEDBNAME'], ref)
application.logger.info("Inserting tupple in %s.eida_temp_users: %s", application.config['RESIFINV_PGDATABASE'], ref)
cur.execute("""
insert into eida_temp_users (network_id, network, start_year, end_year, name, expires_at) values (%(networkid)s, %(networkcode)s, %(startyear)s, %(endyear)s, %(login)s, %(expires_at)s);
""", ref)
......@@ -152,39 +137,39 @@ def cleanup():
"""
Clean old temporary logins and passwords in both databases.
"""
logging.info("Cleaning up expired temporary accounts")
application.logger.info("Cleaning up expired temporary accounts")
rows_deleted = 0
try:
conn = psycopg2.connect(dbname= application.config['AUTHDBNAME'],
port = application.config['AUTHDBPORT'],
host = application.config['AUTHDBHOST'],
user= application.config['AUTHDBUSER'],
password = application.config['AUTHDBPASSWORD'])
conn = psycopg2.connect(dbname= application.config['RESIFAUTH_PGDATABASE'],
port = application.config['RESIFAUTH_PGPORT'],
host = application.config['RESIFAUTH_PGHOST'],
user= application.config['RESIFAUTH_PGUSER'],
password = application.config['RESIFAUTH_PGPASSWORD'])
cur = conn.cursor()
logging.debug("Connected to users database")
application.logger.debug("Connected to users database")
cur.execute("delete from credentials where expires_at < now();")
cur.execute("delete from users where expires_at < now();")
rows_deleted = cur.rowcount
conn.commit()
conn.close()
except psycopg2.Error as e:
logging.error(e.pgerror)
application.logger.error(e.pgerror)
raise e
try:
conn = psycopg2.connect(dbname= application.config['PRIVILEGEDBNAME'],
port = application.config['PRIVILEGEDBPORT'],
host = application.config['PRIVILEGEDBHOST'],
user= application.config['PRIVILEGEDBUSER'],
password = application.config['PRIVILEGEDBPASSWORD'])
conn = psycopg2.connect(dbname= application.config['RESIFINV_PGDATABASE'],
port = application.config['RESIFINV_PGPORT'],
host = application.config['RESIFINV_PGHOST'],
user= application.config['RESIFINV_PGUSER'],
password = application.config['RESIFINV_PGPASSWORD'])
cur = conn.cursor()
logging.debug("Connected to privlieges database")
logging.debug("Deleting from privileges database")
application.logger.debug("Connected to privlieges database")
application.logger.debug("Deleting from privileges database")
cur.execute("delete from eida_temp_users where expires_at < now();")
conn.commit()
conn.close()
except Exception as e:
logging.error(e.pgerror)
application.logger.error(e.pgerror)
raise e
return Response("Deleted %d expired accounts."%(rows_deleted), status=200)
......@@ -192,23 +177,23 @@ def cleanup():
def auth():
login = ''
password = ''
logging.debug(request.mimetype)
application.logger.debug(request.mimetype)
data = request.get_data()
logging.debug("Data: %s", data)
application.logger.debug("Data: %s", data)
try:
verify_token_signature(data, application.config['GNUPGHOMEDIR'])
verify_token_signature(data, application.config['GNUPG_HOMEDIR'])
tokendict = parse_input_data(data)
logging.info("Token signature OK: %s"%str(tokendict))
application.logger.info("Token signature OK: %s"%str(tokendict))
except ValueError as e:
logging.info("Token signature could not be checked: %s"%str(data))
application.logger.info("Token signature could not be checked: %s"%str(data))
return Response(str(e), status=415)
# Now we have a dictionary corresponding to the token's content.
# Verify validity
expiration_ts= datetime.datetime.strptime(tokendict['valid_until'], '%Y-%m-%dT%H:%M:%S.%fZ')
if (expiration_ts - datetime.datetime.now()).total_seconds() < 0:
logging.info("Token is expired")
application.logger.info("Token is expired")
return Response('Token is expired. Please generate a new one at https://geofon.gfz-potsdam.de/eas/', status=400)
logging.info("Token is valid")
application.logger.info("Token is valid")
# Compute a random login and password
login = ''.join(random.choices(string.ascii_uppercase + string.digits, k=14))
......@@ -221,13 +206,13 @@ def auth():
# Check membership and get FDSN references
fdsn_memberships = []
for em in tokendict['memberof'].split(';'):
logging.debug("EPOS membership: "+em)
application.logger.debug("EPOS membership: "+em)
if em in application.config['EPOS_FDSN_MAP']:
logging.debug(" ... is in epos fdsn map")
application.logger.debug(" ... is in epos fdsn map")
fdsn_memberships.append(application.config['EPOS_FDSN_MAP'][em])
if len(fdsn_memberships) > 0:
logging.debug("FDSN memberships: %s"%(fdsn_memberships))
application.logger.debug("FDSN memberships: %s"%(fdsn_memberships))
try:
register_privileges(login, fdsn_memberships)
except NameError as n:
......@@ -236,5 +221,5 @@ def auth():
return "%s:%s"%(login, password)
if __name__ == "__main__":
logging.info("Running in %s mode"%(application.config['ENVIRONMENT']))
application.logger.info("Running in %s mode"%(application.config['ENVIRONMENT']))
application.run(host='0.0.0.0')
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment