Skip to content

Commit 4f8a5a6

Browse files
committed
Add reader API for runbooks
Includes tests and common utils for reader/writer operations/testing
1 parent 533aa40 commit 4f8a5a6

9 files changed

Lines changed: 282 additions & 1 deletion

File tree

runbook/api/utils.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
17+
import functools
18+
19+
import flask
20+
21+
from runbook import config
22+
23+
24+
def check_regions(api_type):
25+
def check_decorator(func):
26+
regions = set(config.get_config(api_type)["regions"])
27+
28+
@functools.wraps(func)
29+
def checker(region, *args, **kwargs):
30+
if region not in regions:
31+
return flask.jsonify(
32+
{"error": "Region {} Not Found".format(region)}), 404
33+
return func(region, *args, **kwargs)
34+
35+
return checker
36+
return check_decorator

runbook/api/v1/reader/__init__.py

Whitespace-only changes.

runbook/api/v1/reader/runbook_.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 elasticsearch
17+
import flask
18+
19+
from runbook.api import utils
20+
from runbook import storage
21+
22+
API_TYPE = "reader"
23+
bp = flask.Blueprint("runbooks", __name__)
24+
25+
26+
def get_blueprints():
27+
return [["/region", bp]]
28+
29+
30+
def _convert(hit):
31+
body = {k: v for k, v in hit["_source"].items()}
32+
body["_id"] = hit["_id"]
33+
return body
34+
35+
36+
@bp.route("/<region>/runbooks", methods=["GET"])
37+
@utils.check_regions(API_TYPE)
38+
def handle_runbooks(region):
39+
es = storage.get_elasticsearch(API_TYPE)
40+
index_name = "ms_runbooks_{}".format(region)
41+
42+
result = es.search(index=index_name, doc_type="runbook")
43+
hit_list = [_convert(hit) for hit in result['hits']['hits']]
44+
return flask.jsonify(hit_list)
45+
46+
47+
@bp.route("/<region>/runbooks/<book_id>", methods=["GET"])
48+
@utils.check_regions(API_TYPE)
49+
def handle_single_runbook(region, book_id):
50+
es = storage.get_elasticsearch(API_TYPE)
51+
index_name = "ms_runbooks_{}".format(region)
52+
53+
try:
54+
result = es.get(
55+
index=index_name,
56+
doc_type="runbook",
57+
id=book_id
58+
)
59+
return flask.jsonify(_convert(result))
60+
except elasticsearch.NotFoundError:
61+
flask.abort(404)

runbook/main_reader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import flask
1919
from flask_helpers import routing
2020

21-
from runbook.api.v1 import runbook as runbook_api
21+
from runbook.api.v1.reader import runbook_ as runbook_api
2222
from runbook import config
2323

2424

tests/unit/api/__init__.py

Whitespace-only changes.

tests/unit/api/v1/__init__.py

Whitespace-only changes.

tests/unit/api/v1/base.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 importlib
17+
import json
18+
19+
import mock
20+
import testtools
21+
22+
23+
TEST_CONFIG = {
24+
"flask": {
25+
"PORT": 5000,
26+
"HOST": "0.0.0.0",
27+
"DEBUG": True
28+
},
29+
"backend": {
30+
"type": "elastic",
31+
"connection": [{"host": "127.0.0.1", "port": 9200}]
32+
},
33+
"regions": [
34+
"region_one",
35+
"region_two"
36+
]
37+
}
38+
39+
40+
class APITestCase(testtools.TestCase):
41+
api_type = None
42+
43+
def setUp(self):
44+
super(APITestCase, self).setUp()
45+
self.addCleanup(mock.patch.stopall)
46+
47+
# NOTE(kzaitsev): mock all get_config for all the tests
48+
self.patcher = mock.patch('runbook.config.get_config')
49+
self.get_config = self.patcher.start()
50+
self.get_config.return_value = TEST_CONFIG
51+
52+
if self.api_type not in ["reader", "writer"]:
53+
raise RuntimeError("Unknown api_type '{}'".format(self.api_type))
54+
55+
main = importlib.import_module("runbook.main_{}".format(self.api_type))
56+
self.client = main.app.test_client()
57+
self.app = main.app
58+
59+
def test_not_found(self):
60+
resp = self.client.get('/404')
61+
self.assertEqual({"error": "Not Found"},
62+
json.loads(resp.data.decode()))
63+
self.assertEqual(404, resp.status_code)

tests/unit/api/v1/reader/__init__.py

Whitespace-only changes.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 copy
17+
import json
18+
19+
import elasticsearch
20+
import mock
21+
22+
from tests.unit.api.v1 import base
23+
24+
25+
class RunbookTestCase(base.APITestCase):
26+
api_type = "reader"
27+
28+
correct_runbook = {
29+
"name": "test",
30+
"description": "test",
31+
"type": "bash",
32+
"runbook": "echo",
33+
}
34+
35+
def test_get_no_region(self):
36+
resp = self.client.get("/api/v1/region/region_zero/runbooks")
37+
self.assertEqual(404, resp.status_code)
38+
39+
resp = self.client.post("/api/v1/region/region_zero/runbooks")
40+
self.assertEqual(405, resp.status_code)
41+
42+
@mock.patch.object(elasticsearch.Elasticsearch, "search")
43+
def test_get_runbooks(self, es_search):
44+
es_search.return_value = {
45+
"hits": {"hits": [
46+
{
47+
"_id": "121",
48+
"_source": self.correct_runbook,
49+
},
50+
{
51+
"_id": "122",
52+
"_source": self.correct_runbook,
53+
},
54+
{
55+
"_id": "123",
56+
"_source": self.correct_runbook,
57+
},
58+
]},
59+
}
60+
resp = self.client.get("/api/v1/region/region_one/runbooks",
61+
content_type="application/json")
62+
self.assertEqual(200, resp.status_code)
63+
resp_json = json.loads(resp.data.decode())
64+
expected = []
65+
for book_id in ["121", "122", "123"]:
66+
data = copy.copy(self.correct_runbook)
67+
data["_id"] = book_id
68+
expected.append(data)
69+
self.assertEqual(expected, resp_json)
70+
71+
es_search.assert_called_with(index="ms_runbooks_region_one",
72+
doc_type="runbook")
73+
74+
@mock.patch.object(elasticsearch.Elasticsearch, "get")
75+
def test_get_single_runbook_bad_id(self, es_get):
76+
es_get.side_effect = elasticsearch.NotFoundError
77+
resp = self.client.get("/api/v1/region/region_one/runbooks/123",
78+
content_type="application/json")
79+
self.assertEqual(404, resp.status_code)
80+
81+
es_get.assert_called_with(index="ms_runbooks_region_one",
82+
doc_type="runbook",
83+
id="123")
84+
85+
@mock.patch.object(elasticsearch.Elasticsearch, "get")
86+
def test_get_single_runbook(self, es_get):
87+
es_get.return_value = {
88+
"_source": self.correct_runbook,
89+
"_id": "123",
90+
}
91+
resp = self.client.get("/api/v1/region/region_one/runbooks/123",
92+
content_type="application/json")
93+
self.assertEqual(200, resp.status_code)
94+
95+
resp_json = json.loads(resp.data.decode())
96+
expected = copy.copy(self.correct_runbook)
97+
expected["_id"] = "123"
98+
self.assertEqual(expected, resp_json)
99+
100+
es_get.assert_called_with(index="ms_runbooks_region_one",
101+
doc_type="runbook",
102+
id="123")
103+
104+
@mock.patch.object(elasticsearch.Elasticsearch, "get")
105+
def test_get_single_runbook_reg_two(self, es_get):
106+
es_get.return_value = {
107+
"_source": self.correct_runbook,
108+
"_id": "123",
109+
}
110+
resp = self.client.get("/api/v1/region/region_two/runbooks/123",
111+
content_type="application/json")
112+
self.assertEqual(200, resp.status_code)
113+
114+
resp_json = json.loads(resp.data.decode())
115+
expected = copy.copy(self.correct_runbook)
116+
expected["_id"] = "123"
117+
self.assertEqual(expected, resp_json)
118+
119+
es_get.assert_called_with(index="ms_runbooks_region_two",
120+
doc_type="runbook",
121+
id="123")

0 commit comments

Comments
 (0)