Skip to content

Commit 156f310

Browse files
committed
Add writer API and tests
1 parent 7e56d45 commit 156f310

5 files changed

Lines changed: 247 additions & 1 deletion

File tree

runbook/api/v1/writer/__init__.py

Whitespace-only changes.

runbook/api/v1/writer/runbook_.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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+
import jsonschema
19+
20+
from runbook.api import utils
21+
from runbook import storage
22+
23+
API_TYPE = "writer"
24+
bp = flask.Blueprint("runbooks", __name__)
25+
26+
27+
def get_blueprints():
28+
return [["/region", bp]]
29+
30+
31+
RUNBOOK_SCHEMA = {
32+
"type": "object",
33+
"$schema": "http://json-schema.org/draft-04/schema",
34+
"properties": {
35+
"name": {"type": "string"},
36+
"description": {"type": "string"},
37+
"type": {"type": "string"},
38+
"runbook": {"type": "string"},
39+
},
40+
"required": ["name", "description", "runbook", "type"],
41+
"additionalProperties": False
42+
}
43+
44+
45+
def _convert(hit):
46+
body = {k: v for k, v in hit["_source"].items()}
47+
body["_id"] = hit["_id"]
48+
return body
49+
50+
51+
@bp.route("/<region>/runbooks", methods=["POST"])
52+
@utils.check_regions(API_TYPE)
53+
def handle_runbooks(region):
54+
es = storage.get_elasticsearch(API_TYPE)
55+
index_name = "ms_runbooks_{}".format(region)
56+
57+
runbook = flask.request.get_json(silent=True)
58+
try:
59+
jsonschema.validate(runbook, RUNBOOK_SCHEMA)
60+
except jsonschema.ValidationError as e:
61+
# NOTE(kzaitsev): jsonschema exception has really good unicode
62+
# error representation
63+
return flask.jsonify(
64+
{"error": u"{}".format(e)}), 400
65+
66+
resp = es.index(
67+
index=index_name,
68+
doc_type="runbook",
69+
body=runbook,
70+
)
71+
if resp['_shards']['successful']:
72+
# at least 1 means we're good
73+
return flask.jsonify({"_id": resp["_id"]}), 201
74+
# should not really be here
75+
return flask.jsonify({"error": "Was unable to save document"}), 500
76+
77+
78+
@bp.route("/<region>/runbooks/<book_id>", methods=["PUT", "DELETE"])
79+
@utils.check_regions(API_TYPE)
80+
def handle_single_runbook(region, book_id):
81+
es = storage.get_elasticsearch(API_TYPE)
82+
index_name = "ms_runbooks_{}".format(region)
83+
84+
if flask.request.method == "DELETE":
85+
try:
86+
es.delete(
87+
index=index_name,
88+
doc_type="runbook",
89+
id=book_id
90+
)
91+
return flask.jsonify({}), 204
92+
except elasticsearch.NotFoundError:
93+
flask.abort(404)
94+
95+
elif flask.request.method == "PUT":
96+
runbook = flask.request.get_json(silent=True)
97+
try:
98+
jsonschema.validate(runbook, RUNBOOK_SCHEMA)
99+
except jsonschema.ValidationError as e:
100+
# NOTE(kzaitsev): jsonschema exception has really good unicode
101+
# error representation
102+
return flask.jsonify(
103+
{"error": u"{}".format(e)}), 400
104+
105+
try:
106+
resp = es.update(
107+
index=index_name,
108+
doc_type="runbook",
109+
id=book_id,
110+
body={"doc": runbook},
111+
)
112+
except elasticsearch.NotFoundError:
113+
flask.abort(404)
114+
115+
if resp['_shards']['successful'] or resp['result'] == 'noop':
116+
# noop means nothing to update, also ok
117+
return flask.jsonify({"_id": resp["_id"]})
118+
return flask.jsonify({"error": "Was unable to update document"}), 500
119+
return flask.jsonify({"error": "Unreachable"}), 500
120+
121+
122+
@bp.route("/<region>/runbooks/<book_id>/run", methods=["POST"])
123+
@utils.check_regions
124+
def run_runbook(region, book_id):
125+
return flask.jsonify("fixme!")

runbook/main_writer.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.writer import runbook_ as runbook_api
2222
from runbook import config
2323

2424

tests/unit/api/v1/writer/__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 json
17+
18+
import elasticsearch
19+
import mock
20+
21+
from tests.unit.api.v1 import base
22+
23+
24+
class WriterRunbookTestCase(base.APITestCase):
25+
api_type = "writer"
26+
27+
correct_runbook = {
28+
"name": "test",
29+
"description": "test",
30+
"type": "bash",
31+
"runbook": "echo",
32+
}
33+
34+
incorrect_runbook = {
35+
"name": "test",
36+
"description": "test",
37+
"type": "bash",
38+
}
39+
40+
def test_post_new_runbook_bad_input(self):
41+
resp = self.client.post("/api/v1/region/region_one/runbooks")
42+
self.assertEqual(400, resp.status_code)
43+
44+
resp = self.client.post("/api/v1/region/region_one/runbooks",
45+
data=json.dumps(self.incorrect_runbook),
46+
content_type="application/json")
47+
self.assertEqual(400, resp.status_code)
48+
49+
resp = self.client.post("/api/v1/region/region_one/runbooks",
50+
data=json.dumps(self.correct_runbook),
51+
)
52+
self.assertEqual(400, resp.status_code)
53+
54+
@mock.patch.object(elasticsearch.Elasticsearch, "index")
55+
def test_post_new_runbook(self, es_index):
56+
es_index.return_value = {
57+
"_shards": {"successful": 1},
58+
"_id": "123",
59+
}
60+
resp = self.client.post("/api/v1/region/region_one/runbooks",
61+
data=json.dumps(self.correct_runbook),
62+
content_type="application/json")
63+
self.assertEqual(201, resp.status_code)
64+
resp_json = json.loads(resp.data.decode())
65+
self.assertEqual(resp_json["_id"], "123")
66+
67+
es_index.assert_called_with(index="ms_runbooks_region_one",
68+
doc_type="runbook",
69+
body=self.correct_runbook)
70+
71+
@mock.patch.object(elasticsearch.Elasticsearch, "delete")
72+
def test_del_single_runbook(self, es_delete):
73+
es_delete.side_effect = None
74+
resp = self.client.delete("/api/v1/region/region_one/runbooks/123",
75+
content_type="application/json")
76+
self.assertEqual(204, resp.status_code)
77+
es_delete.assert_called_with(index="ms_runbooks_region_one",
78+
doc_type="runbook",
79+
id="123")
80+
81+
@mock.patch.object(elasticsearch.Elasticsearch, "delete")
82+
def test_del_single_runbook_bad_id(self, es_delete):
83+
es_delete.side_effect = elasticsearch.NotFoundError
84+
resp = self.client.delete("/api/v1/region/region_one/runbooks/123",
85+
content_type="application/json")
86+
self.assertEqual(404, resp.status_code)
87+
es_delete.assert_called_with(index="ms_runbooks_region_one",
88+
doc_type="runbook",
89+
id="123")
90+
91+
@mock.patch.object(elasticsearch.Elasticsearch, "update")
92+
def test_put_single_runbook_bad_id(self, es_update):
93+
es_update.side_effect = elasticsearch.NotFoundError
94+
resp = self.client.put("/api/v1/region/region_one/runbooks/123",
95+
data=json.dumps(self.correct_runbook),
96+
content_type="application/json")
97+
self.assertEqual(404, resp.status_code)
98+
expected_body = {"doc": self.correct_runbook}
99+
es_update.assert_called_with(index="ms_runbooks_region_one",
100+
doc_type="runbook",
101+
id="123",
102+
body=expected_body)
103+
104+
@mock.patch.object(elasticsearch.Elasticsearch, "update")
105+
def test_put_single_runbook(self, es_update):
106+
es_update.return_value = {
107+
"_shards": {"successful": 1},
108+
"_id": "123",
109+
}
110+
resp = self.client.put("/api/v1/region/region_one/runbooks/123",
111+
data=json.dumps(self.correct_runbook),
112+
content_type="application/json")
113+
self.assertEqual(200, resp.status_code)
114+
resp_json = json.loads(resp.data.decode())
115+
self.assertEqual({"_id": "123"}, resp_json)
116+
117+
expected_body = {"doc": self.correct_runbook}
118+
es_update.assert_called_with(index="ms_runbooks_region_one",
119+
doc_type="runbook",
120+
id="123",
121+
body=expected_body)

0 commit comments

Comments
 (0)