Browse Source

Add events endpoint

Provide basic endpoint:
/v1.0/events/
/healthcheck
/version
Endpoint reads request and returns HTTP code 200.
Provide code to run application in gunicorn mode.

Story: 2001112
Task: 4863

Change-Id: Ic7c344360b5acec5af7751a825e2dff8346cf1f7
Depends-On: I18d9f4ec543c76bfe1311ed1ee940827d4162298
Artur Basiak 1 year ago
parent
commit
620a477df0
50 changed files with 1799 additions and 58 deletions
  1. 2
    1
      .gitignore
  2. 3
    0
      .stestr.conf
  3. 6
    0
      devstack/lib/events-api.sh
  4. 28
    0
      devstack/post_test_hook.sh
  5. 1
    1
      devstack/settings
  6. 1
    1
      etc/monasca/events-api-logging.conf
  7. 24
    11
      etc/monasca/events-api-paste.ini
  8. 85
    20
      monasca_events_api/app/api.py
  9. 0
    0
      monasca_events_api/app/common/__init__.py
  10. 179
    0
      monasca_events_api/app/common/events_publisher.py
  11. 44
    0
      monasca_events_api/app/common/helpers.py
  12. 0
    0
      monasca_events_api/app/controller/__init__.py
  13. 0
    0
      monasca_events_api/app/controller/api/__init__.py
  14. 62
    0
      monasca_events_api/app/controller/healthchecks.py
  15. 0
    0
      monasca_events_api/app/controller/v1/__init__.py
  16. 40
    0
      monasca_events_api/app/controller/v1/body_validation.py
  17. 64
    0
      monasca_events_api/app/controller/v1/bulk_processor.py
  18. 71
    0
      monasca_events_api/app/controller/v1/events.py
  19. 114
    0
      monasca_events_api/app/controller/versions.py
  20. 29
    0
      monasca_events_api/app/core/error_handlers.py
  21. 28
    0
      monasca_events_api/app/core/model.py
  22. 10
    12
      monasca_events_api/app/core/request.py
  23. 38
    0
      monasca_events_api/app/core/request_contex.py
  24. 0
    0
      monasca_events_api/app/healthcheck/__init__.py
  25. 94
    0
      monasca_events_api/app/healthcheck/kafka_check.py
  26. 0
    0
      monasca_events_api/app/model/__init__.py
  27. 17
    0
      monasca_events_api/app/model/envelope.py
  28. 23
    0
      monasca_events_api/app/wsgi.py
  29. 2
    0
      monasca_events_api/conf/__init__.py
  30. 39
    0
      monasca_events_api/conf/events_publisher.py
  31. 2
    2
      monasca_events_api/config.py
  32. 0
    0
      monasca_events_api/middleware/__init__.py
  33. 67
    0
      monasca_events_api/middleware/validation_middleware.py
  34. 1
    2
      monasca_events_api/policies/__init__.py
  35. 1
    1
      monasca_events_api/policies/agent.py
  36. 20
    0
      monasca_events_api/tests/unit/base.py
  37. 187
    0
      monasca_events_api/tests/unit/event_template_json/req_multiple_events.json
  38. 91
    0
      monasca_events_api/tests/unit/event_template_json/req_simple_event.json
  39. 47
    0
      monasca_events_api/tests/unit/test_body_valodiation.py
  40. 134
    0
      monasca_events_api/tests/unit/test_events_v1.py
  41. 77
    0
      monasca_events_api/tests/unit/test_healthchecks.py
  42. 8
    6
      monasca_events_api/tests/unit/test_policy.py
  43. 47
    0
      monasca_events_api/tests/unit/test_validation_middleware.py
  44. 95
    0
      monasca_events_api/tests/unit/test_versions.py
  45. 1
    1
      monasca_events_api/version.py
  46. 7
    0
      policy-sample.yaml
  47. 3
    0
      requirements.txt
  48. 4
    0
      setup.cfg
  49. 1
    0
      test-requirements.txt
  50. 2
    0
      tox.ini

+ 2
- 1
.gitignore View File

@@ -7,6 +7,7 @@ cover
7 7
 .coverage
8 8
 *.egg
9 9
 *.egg-info
10
+.stestr
10 11
 .testrepository
11 12
 .tox
12 13
 AUTHORS
@@ -14,7 +15,7 @@ ChangeLog
14 15
 MANIFEST
15 16
 monasca.log
16 17
 
17
-
18
+*.log
18 19
 *.swp
19 20
 *.iml
20 21
 .DS_Store

+ 3
- 0
.stestr.conf View File

@@ -0,0 +1,3 @@
1
+[DEFAULT]
2
+test_path=$LISTOPT
3
+group_regex=monasca_events_api\.tests\.unit(?:\.|_)([^_]+)

+ 6
- 0
devstack/lib/events-api.sh View File

@@ -34,6 +34,10 @@ function install_events_api {
34 34
     fi
35 35
 }
36 36
 
37
+function create_monasca_events_cache_dir {
38
+    sudo install -m 700 -d -o $STACK_USER $MONASCA_EVENTS_API_CACHE_DIR
39
+}
40
+
37 41
 function configure_events_api {
38 42
     if is_events_api_enabled; then
39 43
         echo_summary "Configuring Events Api"
@@ -41,6 +45,8 @@ function configure_events_api {
41 45
         # Put config files in ``$MONASCA_EVENTS_API_CONF_DIR`` for everyone to find
42 46
         sudo install -d -o $STACK_USER $MONASCA_EVENTS_API_CONF_DIR
43 47
 
48
+        create_monasca_events_cache_dir
49
+
44 50
         # ensure fresh installation of configuration files
45 51
         rm -rf $MONASCA_EVENTS_API_CONF $MONASCA_EVENTS_API_PASTE $MONASCA_EVENTS_API_LOGGING_CONF
46 52
 

+ 28
- 0
devstack/post_test_hook.sh View File

@@ -0,0 +1,28 @@
1
+#
2
+# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP
3
+# (C) Copyright 2017 FUJITSU LIMITED
4
+#
5
+# Licensed under the Apache License, Version 2.0 (the "License");
6
+# you may not use this file except in compliance with the License.
7
+# You may obtain a copy of the License at
8
+#
9
+#    http://www.apache.org/licenses/LICENSE-2.0
10
+#
11
+# Unless required by applicable law or agreed to in writing, software
12
+# distributed under the License is distributed on an "AS IS" BASIS,
13
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14
+# implied.
15
+# See the License for the specific language governing permissions and
16
+# limitations under the License.
17
+
18
+# Sleep some time until all services are started
19
+sleep 6
20
+
21
+function load_devstack_utilities {
22
+    source $BASE/new/devstack/stackrc
23
+    source $BASE/new/devstack/functions
24
+    source $BASE/new/devstack/openrc admin admin
25
+
26
+    # print OS_ variables
27
+    env | grep OS_
28
+}

+ 1
- 1
devstack/settings View File

@@ -74,7 +74,7 @@ MONASCA_EVENTS_API_PASTE=${MONASCA_EVENTS_API_PASTE:-$MONASCA_EVENTS_API_CONF_DI
74 74
 MONASCA_EVENTS_API_LOGGING_CONF=${MONASCA_EVENTS_API_LOGGING_CONF:-$MONASCA_EVENTS_API_CONF_DIR/events-api-logging.conf}
75 75
 MONASCA_EVENTS_API_CACHE_DIR=${MONASCA_EVENTS_API_CACHE_DIR:-/var/cache/monasca-events-api}
76 76
 MONASCA_EVENTS_API_SERVICE_HOST=${MONASCA_EVENTS_API_SERVICE_HOST:-${SERVICE_HOST}}
77
-MONASCA_EVENTS_API_SERVICE_PORT=${MONASCA_EVENTS_API_SERVICE_PORT:-5670}
77
+MONASCA_EVENTS_API_SERVICE_PORT=${MONASCA_EVENTS_API_SERVICE_PORT:-5656}
78 78
 MONASCA_EVENTS_API_SERVICE_PROTOCOL=${MONASCA_EVENTS_API_SERVICE_PROTOCOL:-${SERVICE_PROTOCOL}}
79 79
 
80 80
 MONASCA_EVENTS_PERSISTER_CONF_DIR=${MONASCA_EVENTS_PERSISTER_CONF_DIR:-/etc/monasca}

+ 1
- 1
etc/monasca/events-api-logging.conf View File

@@ -28,7 +28,7 @@ class = logging.handlers.RotatingFileHandler
28 28
 level = DEBUG
29 29
 formatter = context
30 30
 # store up to 5*100MB of logs
31
-args = ('monasca-events-api.log', 'a', 104857600, 5)
31
+args = ('/var/log/monasca/monasca-events-api.log', 'a', 104857600, 5)
32 32
 
33 33
 [formatter_context]
34 34
 class = oslo_log.formatters.ContextFormatter

+ 24
- 11
etc/monasca/events-api-paste.ini View File

@@ -18,34 +18,47 @@ name = main
18 18
 [composite:main]
19 19
 use = egg:Paste#urlmap
20 20
 /: events_version
21
+/v1.0: events_api_v1
22
+/healthcheck: events_healthcheck
23
+
24
+[pipeline:events_api_v1]
25
+pipeline = error_trap request_id auth sizelimit middleware api_v1_app
21 26
 
22 27
 [pipeline:events_version]
23
-pipeline = error_trap versionapp
28
+pipeline = error_trap  versionapp
29
+
30
+[pipeline:events_healthcheck]
31
+pipeline = error_trap healthcheckapp
32
+
33
+[app:api_v1_app]
34
+paste.app_factory = monasca_events_api.app.api:create_api_app
24 35
 
25 36
 [app:versionapp]
26 37
 paste.app_factory = monasca_events_api.app.api:create_version_app
27 38
 
39
+[app:healthcheckapp]
40
+paste.app_factory= monasca_events_api.app.api:create_healthcheck_app
41
+
28 42
 [filter:auth]
29 43
 paste.filter_factory = keystonemiddleware.auth_token:filter_factory
30 44
 
31
-[filter:roles]
32
-paste.filter_factory = monasca_events_api.middleware.role_middleware:RoleMiddleware.factory
45
+[filter:error_trap]
46
+paste.filter_factory = oslo_middleware.catch_errors:CatchErrors.factory
33 47
 
34 48
 [filter:request_id]
35 49
 paste.filter_factory = oslo_middleware.request_id:RequestId.factory
36 50
 
37
-# NOTE(trebskit) this is optional
38
-# insert this into either pipeline to get some WSGI environment debug output
39
-[filter:debug]
40
-paste.filter_factory = oslo_middleware.debug:Debug.factory
51
+[filter:middleware]
52
+paste.filter_factory = monasca_events_api.middleware.validation_middleware:ValidationMiddleware.factory
41 53
 
42
-[filter:error_trap]
43
-paste.filter_factory = oslo_middleware.catch_errors:CatchErrors.factory
54
+[filter:sizelimit]
55
+use = egg:oslo.middleware#sizelimit
44 56
 
45 57
 [server:main]
58
+chdir = /opt/stack/monasca-events-api
46 59
 use = egg:gunicorn#main
47
-bind = 127.0.0.1:5670
48
-workers = 9
60
+bind = 127.0.0.1:5656
61
+workers = 2
49 62
 worker-connections = 2000
50 63
 worker-class = eventlet
51 64
 timeout = 30

+ 85
- 20
monasca_events_api/app/api.py View File

@@ -12,47 +12,112 @@
12 12
 # License for the specific language governing permissions and limitations
13 13
 # under the License.
14 14
 
15
-"""Module initializes various applications of monasca-events-api."""
16
-
15
+"""
16
+Module contains factories to initializes various applications
17
+of monasca-events-api.
18
+"""
17 19
 
18 20
 import falcon
19
-from oslo_config import cfg
20 21
 from oslo_log import log
22
+import six
21 23
 
24
+from monasca_events_api.app.controller import healthchecks
25
+from monasca_events_api.app.controller.v1 import events as v1_events
26
+from monasca_events_api.app.controller import versions
27
+from monasca_events_api.app.core import error_handlers
28
+from monasca_events_api.app.core import request
29
+from monasca_events_api import config
22 30
 
23
-LOG = log.getLogger(__name__)
24
-CONF = cfg.CONF
25
-
26
-_CONF_LOADED = False
27 31
 
32
+def error_trap(app_name):
33
+    """Decorator trapping any error during application boot time.
28 34
 
29
-class Versions(object):
30
-    """Versions API.
35
+    :param app_name: Application name
36
+    :type app_name: str
37
+    :return: _wrapper function
38
+    """
39
+    @six.wraps(error_trap)
40
+    def _wrapper(func):
31 41
 
32
-    Versions returns information about API itself.
42
+        @six.wraps(_wrapper)
43
+        def _inner_wrapper(*args, **kwargs):
44
+            try:
45
+                return func(*args, **kwargs)
46
+            except Exception:
47
+                logger = log.getLogger(__name__)
48
+                logger.exception(
49
+                    'Failed to load application: \'{}\''.format(app_name))
50
+                raise
51
+        return _inner_wrapper
52
+    return _wrapper
33 53
 
34
-    """
35 54
 
36
-    def __init__(self):
37
-        """Init the Version App."""
38
-        LOG.info('Initializing VersionsAPI!')
55
+def singleton_config(func):
56
+    """Decorator ensuring that configuration is loaded only once.
39 57
 
40
-    def on_get(self, req, res):
41
-        """On get method."""
42
-        res.status = falcon.HTTP_200
43
-        res.body = '{"version": "v1.0"}'
58
+    :param func: Function to execute
59
+    :return: _wrapper
60
+    """
61
+    @six.wraps(singleton_config)
62
+    def _wrapper(global_conf, **local_conf):
63
+        config.parse_args()
64
+        return func(global_conf, **local_conf)
65
+    return _wrapper
44 66
 
45 67
 
68
+@error_trap('version')
46 69
 def create_version_app(global_conf, **local_conf):
47
-    """Create Version application."""
48
-    ctrl = Versions()
70
+    """Creates Version application"""
71
+
72
+    ctrl = versions.Versions()
49 73
     controllers = {
50 74
         '/': ctrl,   # redirect http://host:port/ down to Version app
51 75
                      # avoid conflicts with actual pipelines and 404 error
52 76
         '/version': ctrl,  # list all the versions
77
+        '/version/{version_id}': ctrl  # display details of the version
53 78
     }
54 79
 
55 80
     wsgi_app = falcon.API()
56 81
     for route, ctrl in controllers.items():
57 82
         wsgi_app.add_route(route, ctrl)
58 83
     return wsgi_app
84
+
85
+
86
+@error_trap('healthcheck')
87
+def create_healthcheck_app(global_conf, **local_conf):
88
+    """Create Healthcheck application"""
89
+
90
+    controllers = {
91
+        '/': healthchecks.HealthChecks(),
92
+    }
93
+
94
+    wsgi_app = falcon.API()
95
+    for route, ctrl in controllers.items():
96
+        wsgi_app.add_route(route, ctrl)
97
+    return wsgi_app
98
+
99
+
100
+@error_trap('api')
101
+@singleton_config
102
+def create_api_app(global_conf, **local_conf):
103
+    """Create Main Events Api application.
104
+
105
+    :param global_conf: Global config
106
+    :param local_conf: Local config
107
+    :return: falcon.API
108
+    """
109
+    controllers = {}
110
+    controllers.update({
111
+        '/events': v1_events.Events()
112
+    })
113
+
114
+    wsgi_app = falcon.API(
115
+        request_type=request.Request
116
+    )
117
+
118
+    for route, ctrl in controllers.items():
119
+        wsgi_app.add_route(route, ctrl)
120
+
121
+    error_handlers.register_error_handler(wsgi_app)
122
+
123
+    return wsgi_app

+ 0
- 0
monasca_events_api/app/common/__init__.py View File


+ 179
- 0
monasca_events_api/app/common/events_publisher.py View File

@@ -0,0 +1,179 @@
1
+# Copyright 2015 kornicameister@gmail.com
2
+# Copyright 2017 FUJITSU LIMITED
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 falcon
17
+from monasca_common.kafka import producer
18
+from monasca_common.rest import utils as rest_utils
19
+from oslo_log import log
20
+
21
+from monasca_events_api import conf
22
+
23
+
24
+LOG = log.getLogger(__name__)
25
+CONF = conf.CONF
26
+
27
+_RETRY_AFTER = 60
28
+_KAFKA_META_DATA_SIZE = 32
29
+_TRUNCATION_SAFE_OFFSET = 1
30
+
31
+
32
+class InvalidMessageException(Exception):
33
+    pass
34
+
35
+
36
+class EventPublisher(object):
37
+    """Publishes events data to Kafka
38
+
39
+    EventPublisher is able to send single message to multiple configured topic.
40
+    It uses following configuration written in conf file ::
41
+
42
+        [event_publisher]
43
+        topics = 'monevents'
44
+        kafka_url = 'localhost:8900'
45
+
46
+    Note:
47
+        Uses :py:class:`monasca_common.kafka.producer.KafkaProducer`
48
+        to ship events to kafka. For more details
49
+        see `monasca-common`_ github repository.
50
+
51
+    .. _monasca-common: https://github.com/openstack/monasca-common
52
+
53
+    """
54
+
55
+    def __init__(self):
56
+
57
+        self._topics = CONF.events_publisher.topics
58
+
59
+        self._kafka_publisher = producer.KafkaProducer(
60
+            url=CONF.events_publisher.kafka_url
61
+        )
62
+
63
+        LOG.info('Initializing EventPublisher <%s>', self)
64
+
65
+    def send_message(self, messages):
66
+        """Sends message to each configured topic.
67
+
68
+        Note:
69
+            Empty content is not shipped to kafka
70
+
71
+        :param dict| list messages:
72
+        """
73
+        if not messages:
74
+            return
75
+        if not isinstance(messages, list):
76
+            messages = [messages]
77
+
78
+        sent_counter = 0
79
+        num_of_msgs = len(messages)
80
+
81
+        LOG.debug('About to publish %d messages to %s topics',
82
+                  num_of_msgs, self._topics)
83
+
84
+        send_messages = []
85
+
86
+        for message in messages:
87
+            try:
88
+                msg = self._transform_message_to_json(message)
89
+                send_messages.append(msg)
90
+            except Exception as ex:
91
+                LOG.exception(
92
+                    'Failed to transform message, '
93
+                    'this massage is dropped {} '
94
+                    'Exception: {}'.format(message, str(ex)))
95
+        try:
96
+            self._publish(send_messages)
97
+            sent_counter = len(send_messages)
98
+        except Exception as ex:
99
+            LOG.exception('Failure in publishing messages to kafka')
100
+            raise ex
101
+        finally:
102
+            self._check_if_all_messages_was_publish(sent_counter, num_of_msgs)
103
+
104
+    def _transform_message_to_json(self, message):
105
+        """Transforms message into JSON.
106
+
107
+        Method transforms message to JSON and
108
+        encode to utf8
109
+        :param str message: instance of message
110
+        :return: serialized message
111
+        :rtype: str
112
+        """
113
+        msg_json = rest_utils.as_json(message)
114
+        return msg_json.encode('utf-8')
115
+
116
+    def _create_message_for_persister_from_request_body(self, body):
117
+        """Create message for persister from request body
118
+
119
+        Method take original request body and them
120
+        transform the request to proper message format
121
+        acceptable by event-prsister
122
+        :param body: original request body
123
+        :return: transformed message
124
+        """
125
+        timestamp = body['timestamp']
126
+        final_body = []
127
+        for events in body['events']:
128
+            ev = events['event'].copy()
129
+            ev.update({'timestamp': timestamp})
130
+            final_body.append(ev)
131
+        return final_body
132
+
133
+    def _ensure_type_bytes(self, message):
134
+        """Ensures that message will have proper type.
135
+
136
+        :param str message: instance of message
137
+
138
+        """
139
+
140
+        return message.encode('utf-8')
141
+
142
+    def _publish(self, messages):
143
+        """Publishes messages to kafka.
144
+
145
+        :param list messages: list of messages
146
+
147
+        """
148
+        num_of_msg = len(messages)
149
+
150
+        LOG.debug('Publishing %d messages', num_of_msg)
151
+
152
+        try:
153
+            for topic in self._topics:
154
+                self._kafka_publisher.publish(
155
+                    topic,
156
+                    messages
157
+                )
158
+                LOG.debug('Sent %d messages to topic %s', num_of_msg, topic)
159
+        except Exception as ex:
160
+            raise falcon.HTTPServiceUnavailable('Service unavailable',
161
+                                                str(ex), 60)
162
+
163
+    def _check_if_all_messages_was_publish(self, send_count, to_send_count):
164
+        """Executed after publishing to sent metrics.
165
+
166
+        :param int send_count: how many messages have been sent
167
+        :param int to_send_count: how many messages should be sent
168
+
169
+        """
170
+
171
+        failed_to_send = to_send_count - send_count
172
+
173
+        if failed_to_send == 0:
174
+            LOG.debug('Successfully published all [%d] messages',
175
+                      send_count)
176
+        else:
177
+            error_str = ('Failed to send all messages, %d '
178
+                         'messages out of %d have not been published')
179
+            LOG.error(error_str, failed_to_send, to_send_count)

+ 44
- 0
monasca_events_api/app/common/helpers.py View File

@@ -0,0 +1,44 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import falcon
16
+
17
+from oslo_log import log
18
+
19
+from monasca_common.rest import exceptions
20
+from monasca_common.rest import utils as rest_utils
21
+
22
+
23
+LOG = log.getLogger(__name__)
24
+
25
+
26
+def read_json_msg_body(req):
27
+    """Read the json_msg from the http request body and return as JSON.
28
+
29
+    :param req: HTTP request object.
30
+    :return: Returns the metrics as a JSON object.
31
+    :raises falcon.HTTPBadRequest:
32
+    """
33
+    try:
34
+        msg = req.stream.read()
35
+        json_msg = rest_utils.from_json(msg)
36
+        return json_msg
37
+    except exceptions.DataConversionException as ex:
38
+        LOG.debug(ex)
39
+        raise falcon.HTTPBadRequest('Bad request',
40
+                                    'Request body is not valid JSON')
41
+    except ValueError as ex:
42
+        LOG.debug(ex)
43
+        raise falcon.HTTPBadRequest('Bad request',
44
+                                    'Request body is not valid JSON')

+ 0
- 0
monasca_events_api/app/controller/__init__.py View File


+ 0
- 0
monasca_events_api/app/controller/api/__init__.py View File


+ 62
- 0
monasca_events_api/app/controller/healthchecks.py View File

@@ -0,0 +1,62 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import collections
16
+
17
+import falcon
18
+from monasca_common.rest import utils as rest_utils
19
+
20
+from monasca_events_api.app.healthcheck import kafka_check
21
+
22
+HealthCheckResult = collections.namedtuple('HealthCheckResult',
23
+                                           ['status', 'details'])
24
+
25
+
26
+class HealthChecks(object):
27
+    # response configuration
28
+    CACHE_CONTROL = ['must-revalidate', 'no-cache', 'no-store']
29
+
30
+    # response codes
31
+    HEALTHY_CODE_GET = falcon.HTTP_OK
32
+    HEALTHY_CODE_HEAD = falcon.HTTP_NO_CONTENT
33
+    NOT_HEALTHY_CODE = falcon.HTTP_SERVICE_UNAVAILABLE
34
+
35
+    def __init__(self):
36
+        self._kafka_check = kafka_check.KafkaHealthCheck()
37
+        super(HealthChecks, self).__init__()
38
+
39
+    def on_head(self, req, res):
40
+        res.status = self.HEALTHY_CODE_HEAD
41
+        res.cache_control = self.CACHE_CONTROL
42
+
43
+    def on_get(self, req, res):
44
+        # at this point we know API is alive, so
45
+        # keep up good work and verify kafka status
46
+
47
+        kafka_result = self._kafka_check.healthcheck()
48
+
49
+        # in case it'd be unhealthy,
50
+        # message will contain error string
51
+        status_data = {
52
+            'kafka': kafka_result.message
53
+        }
54
+
55
+        # Really simple approach, ideally that should be
56
+        # part of monasca-common with some sort of registration of
57
+        # healthchecks concept
58
+
59
+        res.status = (self.HEALTHY_CODE_GET
60
+                      if kafka_result.healthy else self.NOT_HEALTHY_CODE)
61
+        res.cache_control = self.CACHE_CONTROL
62
+        res.body = rest_utils.as_json(status_data)

+ 0
- 0
monasca_events_api/app/controller/v1/__init__.py View File


+ 40
- 0
monasca_events_api/app/controller/v1/body_validation.py View File

@@ -0,0 +1,40 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import six
16
+
17
+from oslo_log import log
18
+from voluptuous import Any
19
+from voluptuous import Required
20
+from voluptuous import Schema
21
+
22
+
23
+LOG = log.getLogger(__name__)
24
+
25
+
26
+default_schema = Schema({Required("events"): Any(list, dict),
27
+                         Required("timestamp"):
28
+                             Any(str, unicode) if six.PY2 else str})
29
+
30
+
31
+def validate_body(request_body):
32
+    """Validate body.
33
+
34
+     Method validate if body contain all required fields,
35
+     and check if all value have correct type.
36
+
37
+
38
+    :param request_body: body
39
+    """
40
+    default_schema(request_body)

+ 64
- 0
monasca_events_api/app/controller/v1/bulk_processor.py View File

@@ -0,0 +1,64 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+from oslo_log import log
16
+
17
+from monasca_events_api.app.common import events_publisher
18
+from monasca_events_api import conf
19
+
20
+LOG = log.getLogger(__name__)
21
+CONF = conf.CONF
22
+
23
+
24
+class EventsBulkProcessor(events_publisher.EventPublisher):
25
+    """BulkProcessor for effective events processing and publishing.
26
+
27
+    BulkProcessor is customized version of
28
+    :py:class:`monasca_events_api.app.base.event_publisher.EventPublisher`
29
+    that utilizes processing of bulk request inside single loop.
30
+
31
+    """
32
+
33
+    def send_message(self, events):
34
+        """Sends bulk package to kafka
35
+
36
+        :param list events: received events
37
+
38
+        """
39
+
40
+        num_of_msgs = len(events) if events else 0
41
+        to_send_msgs = []
42
+
43
+        LOG.debug('Bulk package <events=%d>',
44
+                  num_of_msgs)
45
+
46
+        for ev_el in events:
47
+            try:
48
+                t_el = self._transform_message_to_json(ev_el)
49
+                if t_el:
50
+                    to_send_msgs.append(t_el)
51
+            except Exception as ex:
52
+                LOG.error('Failed to transform message to json. '
53
+                          'message: {} Exception {}'.format(ev_el, str(ex)))
54
+
55
+        sent_count = len(to_send_msgs)
56
+        try:
57
+            self._publish(to_send_msgs)
58
+        except Exception as ex:
59
+            LOG.error('Failed to send bulk package <events=%d, dimensions=%s>',
60
+                      num_of_msgs)
61
+            LOG.exception(ex)
62
+            raise ex
63
+        finally:
64
+            self._check_if_all_messages_was_publish(num_of_msgs, sent_count)

+ 71
- 0
monasca_events_api/app/controller/v1/events.py View File

@@ -0,0 +1,71 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import falcon
16
+from oslo_log import log
17
+from voluptuous import MultipleInvalid
18
+
19
+from monasca_events_api.app.common import helpers
20
+from monasca_events_api.app.controller.v1 import body_validation
21
+from monasca_events_api.app.controller.v1 import bulk_processor
22
+from monasca_events_api.app.core.model import prepare_message_to_sent
23
+
24
+LOG = log.getLogger(__name__)
25
+
26
+
27
+class Events(object):
28
+    """Events.
29
+
30
+    Events acts as a RESTful endpoint accepting messages contains
31
+    collected events from the OpenStack message bus.
32
+    Works as getaway for any further processing for accepted data.
33
+    """
34
+    VERSION = 'v1.0'
35
+    SUPPORTED_CONTENT_TYPES = {'application/json'}
36
+
37
+    def __init__(self):
38
+        super(Events, self).__init__()
39
+
40
+        self._processor = bulk_processor.EventsBulkProcessor()
41
+
42
+    def on_post(self, req, res):
43
+        """Accepts sent events as json.
44
+
45
+        Accepts events sent to resource which should be sent
46
+        to Kafka queue.
47
+
48
+        :param req: current request
49
+        :param res: current response
50
+        """
51
+        policy_action = 'events_api:agent_required'
52
+
53
+        try:
54
+            request_body = helpers.read_json_msg_body(req)
55
+            req.can(policy_action)
56
+            body_validation.validate_body(request_body)
57
+            messages = prepare_message_to_sent(request_body)
58
+            self._processor.send_message(messages)
59
+            res.status = falcon.HTTP_200
60
+        except MultipleInvalid as ex:
61
+            LOG.error('Entire bulk package was rejected, unsupported body')
62
+            LOG.exception(ex)
63
+            res.status = falcon.HTTP_422
64
+        except Exception as ex:
65
+            LOG.error('Entire bulk package was rejected')
66
+            LOG.exception(ex)
67
+            res.status = falcon.HTTP_400
68
+
69
+    @property
70
+    def version(self):
71
+        return getattr(self, 'VERSION')

+ 114
- 0
monasca_events_api/app/controller/versions.py View File

@@ -0,0 +1,114 @@
1
+# Copyright 2015 kornicameister@gmail.com
2
+# Copyright 2017 FUJITSU LIMITED
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 falcon
17
+import six
18
+
19
+from monasca_common.rest import utils as rest_utils
20
+
21
+
22
+_VERSIONS_TPL_DICT = {
23
+    'v1.0': {
24
+        'id': 'v1.0',
25
+        'links': [
26
+            {
27
+                'rel': 'event',
28
+                'href': '/events'
29
+            }
30
+        ],
31
+        'status': 'CURRENT',
32
+        'updated': "2017-09-01T00:00:00Z"
33
+    },
34
+}
35
+
36
+
37
+class Versions(object):
38
+    """Versions Api"""
39
+
40
+    @staticmethod
41
+    def handle_none_version_id(req, res, result):
42
+        for version in _VERSIONS_TPL_DICT:
43
+            selected_version = _parse_version(version, req)
44
+            result['elements'].append(selected_version)
45
+        res.body = rest_utils.as_json(result, sort_keys=True)
46
+        res.status = falcon.HTTP_200
47
+
48
+    @staticmethod
49
+    def handle_version_id(req, res, result, version_id):
50
+        if version_id in _VERSIONS_TPL_DICT:
51
+            result['elements'].append(_parse_version(version_id, req))
52
+            res.body = rest_utils.as_json(result, sort_keys=True)
53
+            res.status = falcon.HTTP_200
54
+        else:
55
+            error_body = {'message': '%s is not valid version' % version_id}
56
+            res.body = rest_utils.as_json(error_body)
57
+            res.status = falcon.HTTP_400
58
+
59
+    def on_get(self, req, res, version_id=None):
60
+        result = {
61
+            'links': _get_common_links(req),
62
+            'elements': []
63
+        }
64
+        if version_id is None:
65
+            self.handle_none_version_id(req, res, result)
66
+        else:
67
+            self.handle_version_id(req, res, result, version_id)
68
+
69
+
70
+def _get_common_links(req):
71
+    self_uri = req.uri
72
+    if six.PY2:
73
+        self_uri = self_uri.decode(rest_utils.ENCODING)
74
+    base_uri = self_uri.replace(req.path, '')
75
+    return [
76
+        {
77
+            'rel': 'self',
78
+            'href': self_uri
79
+        },
80
+        {
81
+            'rel': 'version',
82
+            'href': '%s/version' % base_uri
83
+        },
84
+        {
85
+            'rel': 'healthcheck',
86
+            'href': '%s/healthcheck' % base_uri
87
+        }
88
+    ]
89
+
90
+
91
+def _parse_version(version_id, req):
92
+    self_uri = req.uri
93
+    if six.PY2:
94
+        self_uri = self_uri.decode(rest_utils.ENCODING)
95
+    base_uri = self_uri.replace(req.path, '')
96
+
97
+    # need to get template dict, consecutive calls
98
+    # needs to operate on unmodified instance
99
+
100
+    selected_version = _VERSIONS_TPL_DICT[version_id].copy()
101
+    raw_links = selected_version['links']
102
+    links = []
103
+
104
+    for link in raw_links:
105
+        raw_link_href = link.get('href')
106
+        raw_link_rel = link.get('rel')
107
+        link_href = base_uri + '/' + version_id + raw_link_href
108
+        links.append({
109
+            'href': link_href,
110
+            'rel': raw_link_rel
111
+        })
112
+    selected_version['links'] = links
113
+
114
+    return selected_version

+ 29
- 0
monasca_events_api/app/core/error_handlers.py View File

@@ -0,0 +1,29 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import falcon
16
+
17
+from monasca_events_api.app.model import envelope
18
+
19
+
20
+def events_envelope_exception_handlet(ex, req, resp, params):
21
+    raise falcon.HTTPUnprocessableEntity(
22
+        title='Failed to create Envelope',
23
+        description=ex.message
24
+    )
25
+
26
+
27
+def register_error_handler(app):
28
+    app.add_error_handler(envelope.EventsEnvelopeException,
29
+                          events_envelope_exception_handlet)

+ 28
- 0
monasca_events_api/app/core/model.py View File

@@ -0,0 +1,28 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+
16
+def prepare_message_to_sent(body):
17
+    """prepare_message_to_sent convert message to proper format,
18
+
19
+    :param dict body: original request body
20
+    :return dict: prepared message for publish to kafka
21
+    """
22
+    timestamp = body['timestamp']
23
+    final_body = []
24
+    for events in body['events']:
25
+        ev = events['event'].copy()
26
+        ev.update({'timestamp': timestamp})
27
+        final_body.append(ev)
28
+    return final_body

+ 10
- 12
monasca_events_api/app/core/request.py View File

@@ -13,29 +13,27 @@
13 13
 # under the License.
14 14
 
15 15
 import falcon
16
+from oslo_log import log
16 17
 
17
-from oslo_context import context
18
-
18
+from monasca_events_api.app.core import request_contex
19 19
 from monasca_events_api import policy
20 20
 
21
+LOG = log.getLogger(__name__)
22
+
21 23
 
22 24
 class Request(falcon.Request):
23 25
     """Variation of falcon. Request with context.
24 26
 
25 27
     Following class enhances :py:class:`falcon.Request` with
26
-    :py:class:`context.RequestContext`
28
+    :py:class:`context.CustomRequestContext`
27 29
     """
28 30
 
29 31
     def __init__(self, env, options=None):
30 32
         """Init an Request class."""
31 33
         super(Request, self).__init__(env, options)
32
-        self.is_admin = None
33
-        self.context = context.RequestContext.from_environ(self.env)
34
-
35
-        if self.is_admin is None:
36
-            self.is_admin = policy.check_is_admin(self)
34
+        self.context = \
35
+            request_contex.RequestContext.from_environ(self.env)
36
+        self.is_admin = policy.check_is_admin(self.context)
37 37
 
38
-    def to_policy_values(self):
39
-        policy = self.context.to_policy_values()
40
-        policy['is_admin'] = self.is_admin
41
-        return policy
38
+    def can(self, action, target=None):
39
+        return self.context.can(action, target)

+ 38
- 0
monasca_events_api/app/core/request_contex.py View File

@@ -0,0 +1,38 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+from oslo_context import context
16
+
17
+from monasca_events_api import policy
18
+
19
+
20
+class RequestContext(context.RequestContext):
21
+    """RequestContext.
22
+
23
+    RequestContext is customized version of
24
+    :py:class:oslo_context.context.RequestContext.
25
+    """
26
+
27
+    def to_policy_values(self):
28
+        pl = super(RequestContext, self).to_policy_values()
29
+        pl['is_admin'] = self.is_admin
30
+        return pl
31
+
32
+    def can(self, action, target=None):
33
+
34
+        if target is None:
35
+            target = {'project_id': self.project_id,
36
+                      'user_id': self.user_id}
37
+
38
+        return policy.authorize(self, action=action, target=target)

+ 0
- 0
monasca_events_api/app/healthcheck/__init__.py View File


+ 94
- 0
monasca_events_api/app/healthcheck/kafka_check.py View File

@@ -0,0 +1,94 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import collections
16
+
17
+from monasca_common.kafka_lib import client
18
+from oslo_log import log
19
+
20
+from monasca_events_api import conf
21
+
22
+LOG = log.getLogger(__name__)
23
+CONF = conf.CONF
24
+
25
+
26
+CheckResult = collections.namedtuple('CheckResult', ['healthy', 'message'])
27
+"""Result from the healthcheck, contains healthy(boolean) and message"""
28
+
29
+
30
+class KafkaHealthCheck(object):
31
+    """Evaluates kafka health
32
+
33
+    Healthcheck verifies if:
34
+
35
+    * kafka server is up and running
36
+    * there is a configured topic in kafka
37
+
38
+    If following conditions are met healthcheck returns healthy status.
39
+    Otherwise unhealthy status is returned with explanation.
40
+
41
+     Example of middleware configuration:
42
+
43
+    .. code-block:: ini
44
+
45
+      [events_publisher]
46
+      kafka_url = localhost:8900
47
+      kafka_topics = events
48
+
49
+    Note:
50
+        It is possible to specify multiple topics if necessary.
51
+        Just separate them with ,
52
+
53
+    """
54
+
55
+    def healthcheck(self):
56
+        url = CONF.events_publisher.kafka_url
57
+
58
+        try:
59
+            kafka_client = client.KafkaClient(hosts=url)
60
+        except client.KafkaUnavailableError as ex:
61
+            LOG.error(repr(ex))
62
+            error_str = 'Could not connect to kafka at %s' % url
63
+            return CheckResult(healthy=False, message=error_str)
64
+
65
+        result = self._verify_topics(kafka_client)
66
+        self._disconnect_gracefully(kafka_client)
67
+
68
+        return result
69
+
70
+    # noinspection PyMethodMayBeStatic
71
+    def _verify_topics(self, kafka_client):
72
+        topics = CONF.events_publisher.topics
73
+
74
+        for t in topics:
75
+            # kafka client loads metadata for topics as fast
76
+            # as possible (happens in __init__), therefore this
77
+            # topic_partitions is sure to be filled
78
+            for_topic = t in kafka_client.topic_partitions
79
+            if not for_topic:
80
+                error_str = 'Kafka: Topic %s not found' % t
81
+                LOG.error(error_str)
82
+                return CheckResult(healthy=False, message=error_str)
83
+
84
+        return CheckResult(healthy=True, message='OK')
85
+
86
+    # noinspection PyMethodMayBeStatic
87
+    def _disconnect_gracefully(self, kafka_client):
88
+        # at this point, client is connected so it must be closed
89
+        # regardless of topic existence
90
+        try:
91
+            kafka_client.close()
92
+        except Exception as ex:
93
+            # log that something went wrong and move on
94
+            LOG.error(repr(ex))

+ 0
- 0
monasca_events_api/app/model/__init__.py View File


+ 17
- 0
monasca_events_api/app/model/envelope.py View File

@@ -0,0 +1,17 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+
16
+class EventsEnvelopeException(Exception):
17
+    pass

+ 23
- 0
monasca_events_api/app/wsgi.py View File

@@ -0,0 +1,23 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+"""
16
+Use this file for deploying the API under mod_wsgi.
17
+"""
18
+
19
+from paste import deploy
20
+
21
+base_dir = '/etc/monasca/'
22
+conf = '{0}event-api-paste.ini'.format(base_dir)
23
+application = deploy.loadapp('config:{0}'.format(conf))

+ 2
- 0
monasca_events_api/conf/__init__.py View File

@@ -15,10 +15,12 @@
15 15
 import os
16 16
 import pkgutil
17 17
 
18
+from oslo_config import cfg
18 19
 from oslo_log import log
19 20
 from oslo_utils import importutils
20 21
 
21 22
 LOG = log.getLogger(__name__)
23
+CONF = cfg.CONF
22 24
 
23 25
 
24 26
 def load_conf_modules():

+ 39
- 0
monasca_events_api/conf/events_publisher.py View File

@@ -0,0 +1,39 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+from oslo_config import cfg
16
+
17
+_MAX_MESSAGE_SIZE = 1048576
18
+
19
+events_publisher_opts = [
20
+    cfg.StrOpt('kafka_url',
21
+               required=True,
22
+               help='Url to kafka server',
23
+               default="127.0.0.1:9092"),
24
+    cfg.MultiStrOpt('topics',
25
+                    help='Consumer topics',
26
+                    default=['monevents'],)
27
+]
28
+
29
+events_publisher_group = cfg.OptGroup(name='events_publisher',
30
+                                      title='events_publisher')
31
+
32
+
33
+def register_opts(conf):
34
+    conf.register_group(events_publisher_group)
35
+    conf.register_opts(events_publisher_opts, events_publisher_group)
36
+
37
+
38
+def list_opts():
39
+    return events_publisher_group, events_publisher_opts

+ 2
- 2
monasca_events_api/config.py View File

@@ -42,13 +42,13 @@ def parse_args():
42 42
     log.register_options(CONF)
43 43
 
44 44
     CONF(args=[],
45
-         prog='events-app',
45
+         prog='events-api',
46 46
          project='monasca',
47 47
          version=version.version_str,
48 48
          description='RESTful API to collect events from cloud')
49 49
 
50 50
     log.setup(CONF,
51
-              product_name='monasca-events-app',
51
+              product_name='monasca-events-api',
52 52
               version=version.version_str)
53 53
 
54 54
     conf.register_opts(CONF)

+ 0
- 0
monasca_events_api/middleware/__init__.py View File


+ 67
- 0
monasca_events_api/middleware/validation_middleware.py View File

@@ -0,0 +1,67 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import falcon
16
+from oslo_log import log
17
+from oslo_middleware import base
18
+
19
+from monasca_events_api import config
20
+
21
+CONF = config.CONF
22
+LOG = log.getLogger(__name__)
23
+
24
+SUPPORTED_CONTENT_TYPES = ('application/json',)
25
+
26
+
27
+def _validate_content_type(req):
28
+    """Validate content type.
29
+
30
+    Function validates request against correct content type.
31
+
32
+    If Content-Type cannot be established (i.e. header is missing),
33
+    :py:class:`falcon.HTTPMissingHeader` is thrown.
34
+    If Content-Type is not **application/json**(supported contents
35
+
36
+    types are define in SUPPORTED_CONTENT_TYPES variable),
37
+    :py:class:`falcon.HTTPUnsupportedMediaType` is thrown.
38
+
39
+    :param falcon.Request req: current request
40
+
41
+    :exception: :py:class:`falcon.HTTPMissingHeader`
42
+    :exception: :py:class:`falcon.HTTPUnsupportedMediaType`
43
+    """
44
+    content_type = req.content_type
45
+    LOG.debug('Content-type is {0}'.format(content_type))
46
+
47
+    if content_type is None or len(content_type) == 0:
48
+        raise falcon.HTTPMissingHeader('Content-Type')
49
+
50
+    if content_type not in SUPPORTED_CONTENT_TYPES:
51
+        types = ','.join(SUPPORTED_CONTENT_TYPES)
52
+        details = ('Only [{0}] are accepted as events representation'.
53
+                   format(types))
54
+        raise falcon.HTTPUnsupportedMediaType(description=details)
55
+
56
+
57
+class ValidationMiddleware(base.ConfigurableMiddleware):
58
+    """Middleware that validates request content.
59
+
60
+    """
61
+
62
+    @staticmethod
63
+    def process_request(req):
64
+
65
+        _validate_content_type(req)
66
+
67
+        return

+ 1
- 2
monasca_events_api/policies/__init__.py View File

@@ -28,8 +28,7 @@ def load_policy_modules():
28 28
     Method iterates over modules of :py:mod:`monasca_events_api.policies`
29 29
     and imports only those that contain following methods:
30 30
 
31
-    - list_opts (required by oslo_config.genconfig)
32
-    - register_opts (required by :py:currentmodule:)
31
+    - list_rules
33 32
 
34 33
     """
35 34
     for modname in _list_module_names():

+ 1
- 1
monasca_events_api/policies/agent.py View File

@@ -18,7 +18,7 @@ from oslo_policy import policy
18 18
 agent_policies = [
19 19
     policy.DocumentedRuleDefault(
20 20
         name='events_api:agent_required',
21
-        check_str='role:monasca_events_agent',
21
+        check_str='role:monasca or role:admin',
22 22
         description='Send events to api',
23 23
         operations=[{'path': '/v1.0/events', 'method': 'POST'}]
24 24
     )

+ 20
- 0
monasca_events_api/tests/unit/base.py View File

@@ -14,6 +14,8 @@
14 14
 
15 15
 import os
16 16
 
17
+import falcon
18
+from falcon import testing
17 19
 import fixtures
18 20
 from oslo_config import cfg
19 21
 from oslo_config import fixture as config_fixture
@@ -22,6 +24,7 @@ from oslo_log.fixture import logging_error as log_fixture
22 24
 from oslo_serialization import jsonutils
23 25
 from oslotest import base
24 26
 
27
+from monasca_events_api.app.core import request
25 28
 from monasca_events_api import config
26 29
 from monasca_events_api import policies
27 30
 from monasca_events_api import policy
@@ -93,3 +96,20 @@ class PolicyFixture(fixtures.Fixture):
93 96
         for rule in policies.list_rules():
94 97
             if rule.name not in rules:
95 98
                 rules[rule.name] = rule.check_str
99
+
100
+
101
+class MockedApi(falcon.API):
102
+    """Mocked API.
103
+
104
+    Subclasses :py:class:`falcon.API` in order to overwrite
105
+    request_type property with custom :py:class:`request.Request`
106
+    """
107
+    def __init__(self):
108
+        super(MockedApi, self).__init__(
109
+            media_type=falcon.DEFAULT_MEDIA_TYPE,
110
+            request_type=request.Request
111
+        )
112
+
113
+
114
+class BaseApiTestCase(BaseTestCase, testing.TestBase):
115
+    api_class = MockedApi

+ 187
- 0
monasca_events_api/tests/unit/event_template_json/req_multiple_events.json View File

@@ -0,0 +1,187 @@
1
+{
2
+  "timestamp": "2012-10-29T13:42:11Z+0200",
3
+  "events": [
4
+    {
5
+      "dimensions": {
6
+        "service": "compute",
7
+        "topic": "notification.sample",
8
+        "hostname": "nova-compute:compute"
9
+      },
10
+      "project_id": "6f70656e737461636b20342065766572",
11
+      "event": {
12
+        "event_type": "instance.reboot.end",
13
+        "payload": {
14
+          "nova_object.data": {
15
+            "architecture": "x86_64",
16
+            "availability_zone": "nova",
17
+            "created_at": "2012-10-29T13:42:11Z",
18
+            "deleted_at": null,
19
+            "display_name": "some-server",
20
+            "display_description": "some-server",
21
+            "fault": null,
22
+            "host": "compute",
23
+            "host_name": "some-server",
24
+            "ip_addresses": [
25
+              {
26
+                "nova_object.name": "IpPayload",
27
+                "nova_object.namespace": "nova",
28
+                "nova_object.version": "1.0",
29
+                "nova_object.data": {
30
+                  "mac": "fa:16:3e:4c:2c:30",
31
+                  "address": "192.168.1.3",
32
+                  "port_uuid": "ce531f90-199f-48c0-816c-13e38010b442",
33
+                  "meta": {},
34
+                  "version": 4,
35
+                  "label": "private-network",
36
+                  "device_name": "tapce531f90-19"
37
+                }
38
+              }
39
+            ],
40
+            "key_name": "my-key",
41
+            "auto_disk_config": "MANUAL",
42
+            "kernel_id": "",
43
+            "launched_at": "2012-10-29T13:42:11Z",
44
+            "image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
45
+            "metadata": {},
46
+            "locked": false,
47
+            "node": "fake-mini",
48
+            "os_type": null,
49
+            "progress": 0,
50
+            "ramdisk_id": "",
51
+            "reservation_id": "r-npxv0e40",
52
+            "state": "active",
53
+            "task_state": null,
54
+            "power_state": "running",
55
+            "tenant_id": "6f70656e737461636b20342065766572",
56
+            "terminated_at": null,
57
+            "flavor": {
58
+              "nova_object.name": "FlavorPayload",
59
+              "nova_object.data": {
60
+                "flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3",
61
+                "name": "test_flavor",
62
+                "projects": null,
63
+                "root_gb": 1,
64
+                "vcpus": 1,
65
+                "ephemeral_gb": 0,
66
+                "memory_mb": 512,
67
+                "disabled": false,
68
+                "rxtx_factor": 1.0,
69
+                "extra_specs": {
70
+                  "hw:watchdog_action": "disabled"
71
+                },
72
+                "swap": 0,
73
+                "is_public": true,
74
+                "vcpu_weight": 0
75
+              },
76
+              "nova_object.version": "1.3",
77
+              "nova_object.namespace": "nova"
78
+            },
79
+            "user_id": "fake",
80
+            "uuid": "178b0921-8f85-4257-88b6-2e743b5a975c"
81
+          },
82
+          "nova_object.name": "InstanceActionPayload",
83
+          "nova_object.namespace": "nova",
84
+          "nova_object.version": "1.3"
85
+        },
86
+        "priority": "INFO",
87
+        "publisher_id": "nova-compute:compute"
88
+      }
89
+    },
90
+    {
91
+      "dimensions": {
92
+        "service": "compute",
93
+        "topic": "notification.error",
94
+        "hostname": "nova-compute:compute"
95
+      },
96
+      "project_id": "6f70656e737461636b20342065766572",
97
+      "event": {
98
+        "priority": "ERROR",
99
+        "payload": {
100
+          "nova_object.name": "InstanceActionPayload",
101
+          "nova_object.data": {
102
+            "state": "active",
103
+            "availability_zone": "nova",
104
+            "key_name": "my-key",
105
+            "kernel_id": "",
106
+            "host_name": "some-server",
107
+            "progress": 0,
108
+            "task_state": "rebuilding",
109
+            "deleted_at": null,
110
+            "architecture": null,
111
+            "auto_disk_config": "MANUAL",
112
+            "ramdisk_id": "",
113
+            "locked": false,
114
+            "created_at": "2012-10-29T13:42:11Z",
115
+            "host": "compute",
116
+            "display_name": "some-server",
117
+            "os_type": null,
118
+            "metadata": {},
119
+            "ip_addresses": [
120
+              {
121
+                "nova_object.name": "IpPayload",
122
+                "nova_object.data": {
123
+                  "device_name": "tapce531f90-19",
124
+                  "port_uuid": "ce531f90-199f-48c0-816c-13e38010b442",
125
+                  "address": "192.168.1.3",
126
+                  "version": 4,
127
+                  "meta": {},
128
+                  "label": "private-network",
129
+                  "mac": "fa:16:3e:4c:2c:30"
130
+                },
131
+                "nova_object.version": "1.0",
132
+                "nova_object.namespace": "nova"
133
+              }
134
+            ],
135
+            "power_state": "running",
136
+            "display_description": "some-server",
137
+            "uuid": "5fafd989-4043-44b4-8acc-907e847f4b70",
138
+            "flavor": {
139
+              "nova_object.name": "FlavorPayload",
140
+              "nova_object.data": {
141
+                "disabled": false,
142
+                "ephemeral_gb": 0,
143
+                "extra_specs": {
144
+                  "hw:watchdog_action": "disabled"
145
+                },
146
+                "flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3",
147
+                "is_public": true,
148
+                "memory_mb": 512,
149
+                "name": "test_flavor",
150
+                "projects": null,
151
+                "root_gb": 1,
152
+                "rxtx_factor": 1.0,
153
+                "swap": 0,
154
+                "vcpu_weight": 0,
155
+                "vcpus": 1
156
+              },
157
+              "nova_object.version": "1.3",
158
+              "nova_object.namespace": "nova"
159
+            },
160
+            "reservation_id": "r-pfiic52h",
161
+            "terminated_at": null,
162
+            "tenant_id": "6f70656e737461636b20342065766572",
163
+            "node": "fake-mini",
164
+            "launched_at": "2012-10-29T13:42:11Z",
165
+            "user_id": "fake",
166
+            "image_uuid": "a2459075-d96c-40d5-893e-577ff92e721c",
167
+            "fault": {
168
+              "nova_object.name": "ExceptionPayload",
169
+              "nova_object.data": {
170
+                "module_name": "nova.tests.functional.notification_sample_tests.test_instance",
171
+                "exception_message": "Insufficient compute resources: fake-resource.",
172
+                "function_name": "_compute_resources_unavailable",
173
+                "exception": "ComputeResourcesUnavailable"
174
+              },
175
+              "nova_object.version": "1.0",
176
+              "nova_object.namespace": "nova"
177
+            }
178
+          },
179
+          "nova_object.version": "1.3",
180
+          "nova_object.namespace": "nova"
181
+        },
182
+        "publisher_id": "nova-compute:compute",
183
+        "event_type": "instance.rebuild.error"
184
+      }
185
+    }
186
+  ]
187
+}

+ 91
- 0
monasca_events_api/tests/unit/event_template_json/req_simple_event.json View File

@@ -0,0 +1,91 @@
1
+{
2
+  "timestamp": "2012-10-29T13:42:11Z+0200",
3
+  "events": [
4
+    {
5
+      "dimensions": {
6
+        "service": "compute",
7
+        "topic": "notification.sample",
8
+        "hostname": "nova-compute:compute"
9
+      },
10
+      "project_id": "6f70656e737461636b20342065766572",
11
+      "event": {
12
+        "event_type": "instance.reboot.end",
13
+        "payload": {
14
+          "nova_object.data": {
15
+            "architecture": "x86_64",
16
+            "availability_zone": "nova",
17
+            "created_at": "2012-10-29T13:42:11Z",
18
+            "deleted_at": null,
19
+            "display_name": "some-server",
20
+            "display_description": "some-server",
21
+            "fault": null,
22
+            "host": "compute",
23
+            "host_name": "some-server",
24
+            "ip_addresses": [
25
+              {
26
+                "nova_object.name": "IpPayload",
27
+                "nova_object.namespace": "nova",
28
+                "nova_object.version": "1.0",
29
+                "nova_object.data": {
30
+                  "mac": "fa:16:3e:4c:2c:30",
31
+                  "address": "192.168.1.3",
32
+                  "port_uuid": "ce531f90-199f-48c0-816c-13e38010b442",
33
+                  "meta": {},
34
+                  "version": 4,
35
+                  "label": "private-network",
36
+                  "device_name": "tapce531f90-19"
37
+                }
38
+              }
39
+            ],
40
+            "key_name": "my-key",
41
+            "auto_disk_config": "MANUAL",
42
+            "kernel_id": "",
43
+            "launched_at": "2012-10-29T13:42:11Z",
44
+            "image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
45
+            "metadata": {},
46
+            "locked": false,
47
+            "node": "fake-mini",
48
+            "os_type": null,
49
+            "progress": 0,
50
+            "ramdisk_id": "",
51
+            "reservation_id": "r-npxv0e40",
52
+            "state": "active",
53
+            "task_state": null,
54
+            "power_state": "running",
55
+            "tenant_id": "6f70656e737461636b20342065766572",
56
+            "terminated_at": null,
57
+            "flavor": {
58
+              "nova_object.name": "FlavorPayload",
59
+              "nova_object.data": {
60
+                "flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3",
61
+                "name": "test_flavor",
62
+                "projects": null,
63
+                "root_gb": 1,
64
+                "vcpus": 1,
65
+                "ephemeral_gb": 0,
66
+                "memory_mb": 512,
67
+                "disabled": false,
68
+                "rxtx_factor": 1.0,
69
+                "extra_specs": {
70
+                  "hw:watchdog_action": "disabled"
71
+                },
72
+                "swap": 0,
73
+                "is_public": true,
74
+                "vcpu_weight": 0
75
+              },
76
+              "nova_object.version": "1.3",
77
+              "nova_object.namespace": "nova"
78
+            },
79
+            "user_id": "fake",
80
+            "uuid": "178b0921-8f85-4257-88b6-2e743b5a975c"
81
+          },
82
+          "nova_object.name": "InstanceActionPayload",
83
+          "nova_object.namespace": "nova",
84
+          "nova_object.version": "1.3"
85
+        },
86
+        "priority": "INFO",
87
+        "publisher_id": "nova-compute:compute"
88
+      }
89
+    }
90
+  ]
91
+}

+ 47
- 0
monasca_events_api/tests/unit/test_body_valodiation.py View File

@@ -0,0 +1,47 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+from voluptuous import MultipleInvalid
16
+
17
+from monasca_events_api.app.controller.v1.body_validation import validate_body
18
+from monasca_events_api.tests.unit import base
19
+
20
+
21
+class TestBodyValidation(base.BaseTestCase):
22
+
23
+    def test_missing_events_filed(self):
24
+        body = {'timestamp': '2012-10-29T13:42:11Z+0200'}
25
+        self.assertRaises(MultipleInvalid, validate_body, body)
26
+
27
+    def test_missing_timestamp_field(self):
28
+        body = {'events': []}
29
+        self.assertRaises(MultipleInvalid, validate_body, body)
30
+
31
+    def test_empty_body(self):
32
+        body = {}
33
+        self.assertRaises(MultipleInvalid, validate_body, body)
34
+
35
+    def test_incorrect_timestamp_type(self):
36
+        body = {'events': [], 'timestamp': 9000}
37
+        self.assertRaises(MultipleInvalid, validate_body, body)
38
+
39
+    def test_incorrect_events_type(self):
40
+        body = {'events': 'over9000', 'timestamp': '2012-10-29T13:42:11Z+0200'}
41
+        self.assertRaises(MultipleInvalid, validate_body, body)
42
+
43
+    def test_correct_body(self):
44
+        body = [{'events': [], 'timestamp': '2012-10-29T13:42:11Z+0200'},
45
+                {'events': {}, 'timestamp': u'2012-10-29T13:42:11Z+0200'}]
46
+        for b in body:
47
+            validate_body(b)

+ 134
- 0
monasca_events_api/tests/unit/test_events_v1.py View File

@@ -0,0 +1,134 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import os
16
+
17
+import falcon
18
+import mock
19
+import ujson as json
20
+
21
+from monasca_events_api.app.controller.v1 import events
22
+from monasca_events_api.tests.unit import base
23
+
24
+
25
+ENDPOINT = '/events'
26
+
27
+
28
+def _init_resource(test):
29
+    resource = events.Events()
30
+    test.api.add_route(ENDPOINT, resource)
31
+    return resource
32
+
33
+
34
+@mock.patch('monasca_events_api.app.controller.v1.'
35
+            'bulk_processor.EventsBulkProcessor')
36
+class TestEventsApi(base.BaseApiTestCase):
37
+
38
+    def test_should_pass_simple_event(self, bulk_processor):
39
+        events_resource = _init_resource(self)
40
+        events_resource._processor = bulk_processor
41
+        unit_test_patch = os.path.dirname(__file__)
42
+        json_file_path = 'event_template_json/req_simple_event.json'
43
+        patch_to_req_simple_event_file = os.path.join(unit_test_patch,
44
+                                                      json_file_path)
45
+        with open(patch_to_req_simple_event_file, 'r') as fi:
46
+            body = fi.read()
47
+        self.simulate_request(
48
+            path=ENDPOINT,
49
+            method='POST',
50
+            headers={
51
+                'Content-Type': 'application/json',
52
+                'X_ROLES': 'monasca'
53
+            },
54
+            body=body
55
+        )
56
+        self.assertEqual(falcon.HTTP_200, self.srmock.status)
57
+
58
+    def test_should_multiple_events(self, bulk_processor):
59
+        events_resource = _init_resource(self)
60
+        events_resource._processor = bulk_processor
61
+        unit_test_patch = os.path.dirname(__file__)
62
+        json_file_path = 'event_template_json/req_multiple_events.json'
63
+        req_multiple_events_json = os.path.join(unit_test_patch,
64
+                                                json_file_path)
65
+        with open(req_multiple_events_json, 'r') as fi:
66
+            body = fi.read()
67
+        self.simulate_request(
68
+            path=ENDPOINT,
69
+            method='POST',
70
+            headers={
71
+                'Content-Type': 'application/json',
72
+                'X_ROLES': 'monasca'
73
+            },
74
+            body=body
75
+        )
76
+        self.assertEqual(falcon.HTTP_200, self.srmock.status)
77
+
78
+    def test_should_fail_empty_body(self, bulk_processor):
79
+        events_resource = _init_resource(self)
80
+        events_resource._processor = bulk_processor
81
+        self.simulate_request(
82
+            path=ENDPOINT,
83
+            method='POST',
84
+            headers={
85
+                'Content-Type': 'application/json',
86
+                'X_ROLES': 'monasca'
87
+            },
88
+            body=''
89
+        )
90
+        self.assertEqual(falcon.HTTP_400, self.srmock.status)
91
+
92
+    def test_should_fail_missing_timestamp_in_body(self, bulk_processor):
93
+        events_resource = _init_resource(self)
94
+        events_resource._processor = bulk_processor
95
+        unit_test_patch = os.path.dirname(__file__)
96
+        json_file_path = 'event_template_json/req_simple_event.json'
97
+        patch_to_req_simple_event_file = os.path.join(unit_test_patch,
98
+                                                      json_file_path)
99
+        with open(patch_to_req_simple_event_file, 'r') as fi:
100
+            events = json.load(fi)['events']
101
+        body = {'events': [events]}
102
+        self.simulate_request(
103
+            path=ENDPOINT,
104
+            method='POST',
105
+            headers={
106
+                'Content-Type': 'application/json',
107
+                'X_ROLES': 'monasca'
108
+            },
109
+            body=json.dumps(body)
110
+        )
111
+        self.assertEqual(falcon.HTTP_422, self.srmock.status)
112
+
113
+    def test_should_fail_missing_events_in_body(self, bulk_processor):
114
+        events_resource = _init_resource(self)
115
+        events_resource._processor = bulk_processor
116
+        body = {'timestamp': '2012-10-29T13:42:11Z+0200'}
117
+        self.simulate_request(
118
+            path=ENDPOINT,
119
+            method='POST',
120
+            headers={
121
+                'Content-Type': 'application/json',
122
+                'X_ROLES': 'monasca'
123
+            },
124
+            body=json.dumps(body)
125
+        )
126
+        self.assertEqual(falcon.HTTP_422, self.srmock.status)
127
+
128
+
129
+class TestApiEventsVersion(base.BaseApiTestCase):
130
+    @mock.patch('monasca_events_api.app.controller.v1.'
131
+                'bulk_processor.EventsBulkProcessor')
132
+    def test_should_return_v1_as_version(self, _):
133
+        resource = events.Events()
134
+        self.assertEqual('v1.0', resource.version)

+ 77
- 0
monasca_events_api/tests/unit/test_healthchecks.py View File

@@ -0,0 +1,77 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import falcon
16
+import mock
17
+import ujson as json
18
+
19
+from monasca_events_api.app.controller import healthchecks
20
+from monasca_events_api.app.healthcheck import kafka_check as healthcheck
21
+from monasca_events_api.tests.unit import base
22
+
23
+ENDPOINT = '/healthcheck'
24
+
25
+
26
+class TestApiHealthChecks(base.BaseApiTestCase):
27
+
28
+    def before(self):
29
+        self.resource = healthchecks.HealthChecks()
30
+        self.api.add_route(
31
+            ENDPOINT,
32
+            self.resource
33
+        )
34
+
35
+    def test_should_return_200_for_head(self):
36
+        self.simulate_request(ENDPOINT, method='HEAD')
37
+        self.assertEqual(falcon.HTTP_NO_CONTENT, self.srmock.status)
38
+
39
+    @mock.patch('monasca_events_api.app.healthcheck.'
40
+                'kafka_check.KafkaHealthCheck')
41
+    def test_should_report_healthy_if_kafka_healthy(self, kafka_check):
42
+        kafka_check.healthcheck.return_value = healthcheck.CheckResult(True,
43
+                                                                       'OK')
44
+        self.resource._kafka_check = kafka_check
45
+
46
+        ret = self.simulate_request(ENDPOINT,
47
+                                    headers={
48
+                                        'Content-Type': 'application/json'
49
+                                    },
50
+                                    decode='utf8',
51
+                                    method='GET')
52
+        self.assertEqual(falcon.HTTP_OK, self.srmock.status)
53
+
54
+        ret = json.loads(ret)
55
+        self.assertIn('kafka', ret)
56
+        self.assertEqual('OK', ret.get('kafka'))
57
+
58
+    @mock.patch('monasca_events_api.app.healthcheck.'
59
+                'kafka_check.KafkaHealthCheck')
60
+    def test_should_report_unhealthy_if_kafka_unhealthy(self, kafka_check):
61
+        url = 'localhost:8200'
62
+        err_str = 'Could not connect to kafka at %s' % url
63
+        kafka_check.healthcheck.return_value = healthcheck.CheckResult(False,
64
+                                                                       err_str)
65
+        self.resource._kafka_check = kafka_check
66
+
67
+        ret = self.simulate_request(ENDPOINT,
68
+                                    headers={
69
+                                        'Content-Type': 'application/json'
70
+                                    },
71
+                                    decode='utf8',
72
+                                    method='GET')
73
+        self.assertEqual(falcon.HTTP_SERVICE_UNAVAILABLE, self.srmock.status)
74
+
75
+        ret = json.loads(ret)
76
+        self.assertIn('kafka', ret)
77
+        self.assertEqual(err_str, ret.get('kafka'))

+ 8
- 6
monasca_events_api/tests/unit/test_policy.py View File

@@ -82,7 +82,7 @@ class TestPolicyCase(base.BaseTestCase):
82 82
             )
83 83
         )
84 84
         self.assertRaises(os_policy.PolicyNotRegistered, policy.authorize,
85
-                          ctx, action, {})
85
+                          ctx.context, action, {})
86 86
 
87 87
     def test_authorize_bad_action_throws(self):
88 88
         action = "example:denied"
@@ -97,7 +97,7 @@ class TestPolicyCase(base.BaseTestCase):
97 97
             )
98 98
         )
99 99
         self.assertRaises(os_policy.PolicyNotAuthorized, policy.authorize,
100
-                          ctx, action, {})
100
+                          ctx.context, action, {})
101 101
 
102 102
     def test_authorize_bad_action_no_exception(self):
103 103
         action = "example:denied"
@@ -111,7 +111,7 @@ class TestPolicyCase(base.BaseTestCase):
111 111
                 }
112 112
             )
113 113
         )
114
-        result = policy.authorize(ctx, action, {}, False)
114
+        result = policy.authorize(ctx.context, action, {}, False)
115 115
         self.assertFalse(result)
116 116
 
117 117
     def test_authorize_good_action(self):
@@ -126,7 +126,7 @@ class TestPolicyCase(base.BaseTestCase):
126 126
                 }
127 127
             )
128 128
         )
129
-        result = policy.authorize(ctx, action, False)
129
+        result = policy.authorize(ctx.context, action, False)
130 130
         self.assertTrue(result)
131 131
 
132 132
     def test_ignore_case_role_check(self):
@@ -143,7 +143,9 @@ class TestPolicyCase(base.BaseTestCase):
143 143
                 }
144 144
             )
145 145
         )
146
-        self.assertTrue(policy.authorize(admin_context, lowercase_action,
146
+        self.assertTrue(policy.authorize(admin_context.context,
147
+                                         lowercase_action,
147 148
                                          {}))
148
-        self.assertTrue(policy.authorize(admin_context, uppercase_action,
149
+        self.assertTrue(policy.authorize(admin_context.context,
150
+                                         uppercase_action,
149 151
                                          {}))

+ 47
- 0
monasca_events_api/tests/unit/test_validation_middleware.py View File

@@ -0,0 +1,47 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+import falcon
16
+
17
+from monasca_events_api.middleware import validation_middleware as vm
18
+from monasca_events_api.tests.unit import base
19
+
20
+
21
+class FakeRequest(object):
22
+    def __init__(self, content=None, length=0):
23
+        self.content_type = content if content else None
24
+        self.content_length = (length if length is not None and length > 0
25
+                               else None)
26
+
27
+
28
+class TestValidation(base.BaseTestCase):
29
+
30
+    def setUp(self):
31
+        super(TestValidation, self).setUp()
32
+
33
+    def test_should_validate_right_content_type(self):
34
+        req = FakeRequest('application/json')
35
+        vm._validate_content_type(req)
36
+
37
+    def test_should_fail_missing_content_type(self):
38
+        req = FakeRequest()
39
+        self.assertRaises(falcon.HTTPMissingHeader,
40
+                          vm._validate_content_type,
41
+                          req)
42
+
43
+    def test_should_fail_unsupported_content_type(self):
44
+        req = FakeRequest('test/plain')
45
+        self.assertRaises(falcon.HTTPUnsupportedMediaType,
46
+                          vm._validate_content_type,
47
+                          req)

+ 95
- 0
monasca_events_api/tests/unit/test_versions.py View File

@@ -0,0 +1,95 @@
1
+# Copyright 2017 FUJITSU LIMITED
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+# not use this file except in compliance with the License. You may obtain
5
+# a copy of the License at
6
+#
7
+#      http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+# License for the specific language governing permissions and limitations
13
+# under the License.
14
+
15
+from six.moves.urllib.parse import urlparse as urlparse
16
+
17
+import falcon
18
+import ujson as json
19
+
20
+from monasca_events_api.app.controller import versions
21
+from monasca_events_api.tests.unit import base
22
+
23
+
24
+def _get_versioned_url(version_id):
25
+    return '/version/%s' % version_id
26
+
27
+
28
+class TestVersionApi(base.BaseApiTestCase):
29
+
30
+    def before(self):
31
+        self.versions = versions.Versions()
32
+        self.api.add_route("/version/", self.versions)
33
+        self.api.add_route("/version/{version_id}", self.versions)
34
+
35
+    def test_request_for_incorrect_version(self):
36
+        incorrect_version = 'v2.0'
37
+        uri = _get_versioned_url(incorrect_version)
38
+
39
+        self.simulate_request(
40
+            uri,
41
+            method='GET',
42
+            headers={
43
+                'Content-Type': 'application/json'
44
+            }
45
+        )
46
+
47
+        self.assertEqual(falcon.HTTP_400, self.srmock.status)
48
+
49
+    def test_should_return_supported_event_api_version(self):
50
+
51
+        def _check_global_links(current_endpoint, links):
52
+            expected_links = {'self': current_endpoint,
53
+                              'version': '/version',
54
+                              'healthcheck': '/healthcheck'}
55
+            _check_links(links, expected_links)
56
+
57
+        def _check_links(links, expected_links):
58
+            for link in links:
59
+                self.assertIn('rel', link)
60
+                self.assertIn('href', link)
61
+                key = link.get('rel')
62
+                href_path = urlparse(link.get('href')).path
63
+                self.assertIn(key, expected_links.keys())
64
+                self.assertEqual(expected_links[key], href_path)
65
+
66
+        def _check_elements(elements, expected_versions):
67
+            self.assertIsInstance(elements, list)
68
+            for el in elements:
69
+                self.assertItemsEqual([
70
+                    u'id',
71
+                    u'links',
72
+                    u'status',
73
+                    u'updated'
74
+                ], el.keys())
75
+                id_v = el.get('id')
76
+                self.assertEqual(expected_versions, id_v)
77
+
78
+        supported_versions = ['v1.0']
79
+        version_endpoint = '/version'
80
+        for version in supported_versions:
81
+            endpoint = '%s/%s' % (version_endpoint, version)
82
+            res = self.simulate_request(
83
+                endpoint,
84
+                method='GET',
85
+                headers={
86
+                    'Content-Type': 'application/json'
87
+                },
88
+                decode='utf-8'
89
+            )
90
+            self.assertEqual(falcon.HTTP_200, self.srmock.status)
91
+            response = json.loads(res)
92
+            self.assertIn('links', response)
93
+            _check_global_links(endpoint, response['links'])
94
+            self.assertIn('elements', response)
95
+            _check_elements(response['elements'], version)

+ 1
- 1
monasca_events_api/version.py View File

@@ -14,5 +14,5 @@
14 14
 
15 15
 import pbr.version
16 16
 
17
-version_info = pbr.version.VersionInfo('monasca-events-app')
17
+version_info = pbr.version.VersionInfo('monasca-events-api')
18 18
 version_str = version_info.version_string()

+ 7
- 0
policy-sample.yaml View File

@@ -0,0 +1,7 @@
1
+# Admin role
2
+# POST  /
3
+#"admin_required": "role:admin or is_admin:1"
4
+
5
+# Send events to api
6
+# POST  /v1.0/events
7
+#"events_api:agent_required": "role:monasca_events_agent"

+ 3
- 0
requirements.txt View File

@@ -15,3 +15,6 @@ oslo.serialization>=1.10.0,!=2.19.1 # Apache-2.0
15 15
 oslo.utils>=3.20.0 # Apache-2.0
16 16
 PasteDeploy>=1.5.0 # MIT
17 17
 eventlet!=0.18.3,!=0.20.1,<0.21.0,>=0.18.2 # MIT
18
+monasca-common>=1.4.0 # Apache-2.0
19
+voluptuous>=0.8.9 # BSD License
20
+six>=1.10.0 # MIT

+ 4
- 0
setup.cfg View File

@@ -31,6 +31,9 @@ data_files =
31 31
         etc/monasca/events-api-paste.ini
32 32
         etc/monasca/events-api-logging.conf
33 33
 
34
+wsgi_scripts =
35
+    monasca-events-api-wsgi = monasca_events_api.app.wsgi:main
36
+
34 37
 [entry_points]
35 38
 
36 39
 oslo.config.opts =
@@ -66,5 +69,6 @@ universal = 1
66 69
 [pbr]
67 70
 autodoc_index_modules = True
68 71
 autodoc_exclude_modules =
72
+  monasca_events_api.app.wsgi*
69 73
   monasca_events_api.tests.*
70 74
 api_doc_dir = contributor/api

+ 1
- 0
test-requirements.txt View File

@@ -14,6 +14,7 @@ mock>=2.0 # BSD
14 14
 oslotest>=1.10.0 # Apache-2.0
15 15
 os-testr>=0.8.0 # Apache-2.0
16 16
 simplejson>=2.2.0 # MIT
17
+voluptuous>=0.8.9 # BSD License
17 18
 
18 19
 # documentation
19 20
 doc8 # Apache-2.0

+ 2
- 0
tox.ini View File

@@ -96,7 +96,9 @@ commands =
96 96
 [testenv:devdocs]
97 97
 description = Builds developer documentation
98 98
 commands =
99
+  {[testenv]commands}
99 100
   rm -rf doc/build
101
+  rm -rf doc/source/contributor/api
100 102
   {[testenv:checkjson]commands}
101 103
   python setup.py build_sphinx
102 104
 

Loading…
Cancel
Save