Skip to content

Commit 1a27e76

Browse files
committed
Add runbook config and storage management
Configs are split in two: reader and writer for ro/wo api services respectively. main-reader/main-writer entry point files are added.
1 parent f3303a9 commit 1a27e76

6 files changed

Lines changed: 286 additions & 19 deletions

File tree

etc/sample_config.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"flask": {
3+
"PORT": 5000,
4+
"HOST": "0.0.0.0",
5+
"DEBUG": true
6+
},
7+
"backend": {
8+
"type": "elastic",
9+
"connection": [{"host": "127.0.0.1", "port": 9200}]
10+
},
11+
"regions": [
12+
"region_one",
13+
"region_two"
14+
],
15+
"logging": {
16+
"level": "INFO"
17+
}
18+
}

requirements.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
gunicorn==19.6.0
21
Flask==0.11.1
3-
4-
flask-helpers
2+
elasticsearch==5.0.1
3+
flask-helpers==0.1
4+
gunicorn==19.6.0
5+
jsonschema>=2.0.0,!=2.5.0,<3.0.0 # MIT

runbook/config.py

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,119 @@
1616
import json
1717
import logging
1818
import os
19+
import sys
1920

21+
import jsonschema
2022

21-
config = None
2223

24+
CONF = {
25+
"reader": None,
26+
"writer": None
27+
}
2328

24-
def get_config():
29+
DEFAULT_CONF = {
30+
"reader": {
31+
"flask": {
32+
"HOST": "0.0.0.0",
33+
"PORT": 5000,
34+
"DEBUG": False
35+
},
36+
"backend": {
37+
"type": "elastic",
38+
"connection": [{"host": "127.0.0.1", "port": 9200}]
39+
},
40+
"regions": [],
41+
},
42+
"writer": {
43+
"flask": {
44+
"HOST": "0.0.0.0",
45+
"PORT": 5001,
46+
"DEBUG": False
47+
},
48+
"backend": {
49+
"type": "elastic",
50+
"connection": [{"host": "127.0.0.1", "port": 9200}]
51+
},
52+
"regions": [],
53+
}
54+
}
55+
56+
57+
CONF_SCHEMA = {
58+
"type": "object",
59+
"$schema": "http://json-schema.org/draft-04/schema",
60+
"properties": {
61+
"flask": {
62+
"type": "object",
63+
"properties": {
64+
"PORT": {"type": "integer"},
65+
"HOST": {"type": "string"},
66+
"DEBUG": {"type": "boolean"}
67+
},
68+
},
69+
"backend": {
70+
"type": "object",
71+
"properties": {
72+
"type": {"type": "string"},
73+
"connection": {
74+
"type": "array",
75+
"items": {
76+
"type": "object",
77+
"properties": {
78+
"host": {"type": "string"},
79+
"port": {"type": "integer"}
80+
},
81+
"required": ["host"]
82+
},
83+
"minItems": 1
84+
},
85+
},
86+
"required": ["type", "connection"]
87+
},
88+
"regions": {
89+
"type": "array",
90+
"items": {
91+
"type": "string",
92+
},
93+
},
94+
"logging": {
95+
"type": "object",
96+
"properties": {
97+
"level": {"type": "string"}
98+
}
99+
}
100+
},
101+
"additionalProperties": False
102+
}
103+
104+
105+
def get_config(api_type=None):
25106
"""Return cached configuration.
26107
27108
:returns: application config
28109
:rtype: dict
29110
"""
30-
global config
31-
if not config:
32-
path = os.environ.get("RUNBOOK_CONF", "/etc/runbook/config.json")
111+
if api_type not in ["reader", "writer"]:
112+
raise RuntimeError("Unknown api type '{}'".format(api_type))
113+
114+
global CONF
115+
if not CONF[api_type]:
116+
path = os.environ.get(
117+
"RUNBOOK_{}_CONF".format(api_type.upper()),
118+
"/etc/runbook/{}-config.json".format(api_type))
33119
try:
34-
config = json.load(open(path))
120+
CONF[api_type] = json.load(open(path))
35121
logging.info("Config is '%s'" % path)
36122
except IOError as e:
37123
logging.warning("Config at '%s': %s" % (path, e))
38-
config = {
39-
"flask": {
40-
"HOST": "0.0.0.0",
41-
"PORT": 5000,
42-
"DEBUG": False
43-
}
44-
}
45-
return config
124+
CONF[api_type] = DEFAULT_CONF[api_type]
125+
try:
126+
jsonschema.validate(CONF[api_type], CONF_SCHEMA)
127+
except jsonschema.ValidationError as e:
128+
logging.error(e.message)
129+
sys.exit(1)
130+
except jsonschema.SchemaError as e:
131+
logging.error(e)
132+
sys.exit(1)
133+
else:
134+
return CONF[api_type]

runbook/main.py renamed to runbook/main-reader.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# License for the specific language governing permissions and limitations
1414
# under the License.
1515

16+
import logging
1617

1718
import flask
1819
from flask_helpers import routing
@@ -21,8 +22,11 @@
2122
from runbook import config
2223

2324

25+
CONF = config.get_config("reader")
26+
APP_CONF = CONF.get("flask", {})
27+
2428
app = flask.Flask(__name__, static_folder=None)
25-
app.config.update(config.get_config()["flask"])
29+
app.config.update(APP_CONF)
2630

2731

2832
@app.route("/api/")
@@ -32,9 +36,16 @@ def versions():
3236

3337
@app.errorhandler(404)
3438
def not_found(error):
39+
logging.error(error)
3540
return flask.jsonify({"error": "Not Found"}), 404
3641

3742

43+
@app.errorhandler(500)
44+
def handle_500(error):
45+
logging.error(error)
46+
return flask.jsonify({"error": "Internal Server Error"}), 500
47+
48+
3849
for bp in [runbook_api]:
3950
for url_prefix, blueprint in bp.get_blueprints():
4051
app.register_blueprint(blueprint, url_prefix="/api/v1%s" % url_prefix)
@@ -44,7 +55,8 @@ def not_found(error):
4455

4556

4657
def main():
47-
app.run()
58+
app.run(host=APP_CONF.get("HOST", "0.0.0.0"),
59+
port=APP_CONF.get("PORT", "5000"))
4860

4961

5062
if __name__ == "__main__":

runbook/main-writer.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright 2016: Mirantis Inc.
2+
# All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
# not use this file except in compliance with the License. You may obtain
6+
# a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
16+
import logging
17+
18+
import flask
19+
from flask_helpers import routing
20+
21+
from runbook.api.v1 import runbook as runbook_api
22+
from runbook import config
23+
24+
25+
CONF = config.get_config("writer")
26+
APP_CONF = CONF.get("flask", {})
27+
28+
app = flask.Flask(__name__, static_folder=None)
29+
app.config.update(APP_CONF)
30+
31+
32+
@app.route("/api/")
33+
def versions():
34+
return flask.jsonify({"versions": ["1.0"]})
35+
36+
37+
@app.errorhandler(404)
38+
def not_found(error):
39+
logging.error(error)
40+
return flask.jsonify({"error": "Not Found"}), 404
41+
42+
43+
@app.errorhandler(500)
44+
def handle_500(error):
45+
logging.error(error)
46+
return flask.jsonify({"error": "Internal Server Error"}), 500
47+
48+
49+
for bp in [runbook_api]:
50+
for url_prefix, blueprint in bp.get_blueprints():
51+
app.register_blueprint(blueprint, url_prefix="/api/v1%s" % url_prefix)
52+
53+
54+
app = routing.add_routing_map(app, html_uri=None, json_uri="/")
55+
56+
57+
def main():
58+
app.run(host=APP_CONF.get("HOST", "0.0.0.0"),
59+
port=APP_CONF.get("PORT", "5001"))
60+
61+
62+
if __name__ == "__main__":
63+
main()

runbook/storage.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright 2016: Mirantis Inc.
2+
# All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
# not use this file except in compliance with the License. You may obtain
6+
# a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
16+
import json
17+
import logging
18+
19+
import elasticsearch
20+
21+
from runbook import config
22+
23+
24+
LOG = logging.getLogger("storage")
25+
26+
ES_MAPPINGS = {
27+
"mappings": {
28+
"runbook": {
29+
"_all": {"enabled": False},
30+
"properties": {
31+
"runbook": {"type": "binary"},
32+
"description": {"type": "text"},
33+
"name": {"type": "keyword"},
34+
"type": {"type": "keyword"},
35+
}
36+
},
37+
"run": {
38+
"_all": {"enabled": False},
39+
"properties": {
40+
"date": {"type": "date"},
41+
"user": {"type": "string"},
42+
"output": {"type": "binary"},
43+
"return_code": {"type": "integer"},
44+
"status": {"type": "keyword"},
45+
}
46+
}
47+
}
48+
}
49+
50+
ES_CLIENT = None
51+
52+
53+
def get_elasticsearch(api_type):
54+
"""Configures or returns already configured ES client."""
55+
global ES_CLIENT
56+
if not ES_CLIENT:
57+
nodes = config.get_config(api_type)["backend"]["connection"]
58+
ES_CLIENT = elasticsearch.Elasticsearch(nodes)
59+
return ES_CLIENT
60+
61+
62+
def ensure_index(index, api_type):
63+
"""Esures index exists in es."""
64+
es = get_elasticsearch(api_type)
65+
66+
try:
67+
if not es.indices.exists(index):
68+
mapping = json.dumps(ES_MAPPINGS)
69+
LOG.info("Creating Elasticsearch index: {}".format(index))
70+
es.indices.create(index, body=mapping)
71+
except elasticsearch.exceptions.ElasticsearchException:
72+
LOG.exception("Was unable to get or create index: {}".format(index))
73+
raise
74+
75+
76+
def configure_regions(api_type="writer"):
77+
"""Ensure there are indices for every region we have configured."""
78+
79+
for region in config.get_config(api_type)["regions"]:
80+
ensure_index("ms_{}_{}".format("runbooks", region), api_type)
81+
82+
83+
if __name__ == "__main__":
84+
configure_regions()

0 commit comments

Comments
 (0)