diff --git a/keystonemiddleware/audit.py b/keystonemiddleware/audit.py index 5e230be9..f44da80d 100644 --- a/keystonemiddleware/audit.py +++ b/keystonemiddleware/audit.py @@ -19,9 +19,12 @@ in the pipeline so that it can utilise the information the Identity server provides. """ +import ast +import collections import functools import logging import os.path +import re import sys from oslo_config import cfg @@ -31,8 +34,20 @@ try: messaging = True except ImportError: messaging = False -import pycadf -from pycadf.audit import api +from pycadf import cadftaxonomy as taxonomy +from pycadf import cadftype +from pycadf import credential +from pycadf import endpoint +from pycadf import eventfactory as factory +from pycadf import host +from pycadf import identifier +from pycadf import reason +from pycadf import reporterstep +from pycadf import resource +from pycadf import tag +from pycadf import timestamp +from six.moves import configparser +from six.moves.urllib import parse as urlparse import webob.dec from keystonemiddleware.i18n import _LE, _LI @@ -52,6 +67,226 @@ def _log_and_ignore_error(fn): return wrapper +Service = collections.namedtuple('Service', + ['id', 'name', 'type', 'admin_endp', + 'public_endp', 'private_endp']) + + +AuditMap = collections.namedtuple('AuditMap', + ['path_kw', + 'custom_actions', + 'service_endpoints', + 'default_target_endpoint_type']) + + +class OpenStackAuditApi(object): + + def __init__(self, cfg_file): + """Configure to recognize and map known api paths.""" + path_kw = {} + custom_actions = {} + endpoints = {} + default_target_endpoint_type = None + + if cfg_file: + try: + map_conf = configparser.SafeConfigParser() + map_conf.readfp(open(cfg_file)) + + try: + default_target_endpoint_type = map_conf.get( + 'DEFAULT', 'target_endpoint_type') + except configparser.NoOptionError: + pass + + try: + custom_actions = dict(map_conf.items('custom_actions')) + except configparser.Error: + pass + + try: + path_kw = dict(map_conf.items('path_keywords')) + except configparser.Error: + pass + + try: + endpoints = dict(map_conf.items('service_endpoints')) + except configparser.Error: + pass + except configparser.ParsingError as err: + raise PycadfAuditApiConfigError( + 'Error parsing audit map file: %s' % err) + self._MAP = AuditMap( + path_kw=path_kw, custom_actions=custom_actions, + service_endpoints=endpoints, + default_target_endpoint_type=default_target_endpoint_type) + + @staticmethod + def _clean_path(value): + """Clean path if path has json suffix.""" + return value[:-5] if value.endswith('.json') else value + + def get_action(self, req): + """Take a given Request, parse url path to calculate action type. + + Depending on req.method: + if POST: path ends with 'action', read the body and use as action; + path ends with known custom_action, take action from config; + request ends with known path, assume is create action; + request ends with unknown path, assume is update action. + if GET: request ends with known path, assume is list action; + request ends with unknown path, assume is read action. + if PUT, assume update action. + if DELETE, assume delete action. + if HEAD, assume read action. + + """ + path = req.path[:-1] if req.path.endswith('/') else req.path + url_ending = self._clean_path(path[path.rfind('/') + 1:]) + method = req.method + + if url_ending + '/' + method.lower() in self._MAP.custom_actions: + action = self._MAP.custom_actions[url_ending + '/' + + method.lower()] + elif url_ending in self._MAP.custom_actions: + action = self._MAP.custom_actions[url_ending] + elif method == 'POST': + if url_ending == 'action': + try: + if req.json: + body_action = list(req.json.keys())[0] + action = taxonomy.ACTION_UPDATE + '/' + body_action + else: + action = taxonomy.ACTION_CREATE + except ValueError: + action = taxonomy.ACTION_CREATE + elif url_ending not in self._MAP.path_kw: + action = taxonomy.ACTION_UPDATE + else: + action = taxonomy.ACTION_CREATE + elif method == 'GET': + if url_ending in self._MAP.path_kw: + action = taxonomy.ACTION_LIST + else: + action = taxonomy.ACTION_READ + elif method == 'PUT' or method == 'PATCH': + action = taxonomy.ACTION_UPDATE + elif method == 'DELETE': + action = taxonomy.ACTION_DELETE + elif method == 'HEAD': + action = taxonomy.ACTION_READ + else: + action = taxonomy.UNKNOWN + + return action + + def _get_service_info(self, endp): + service = Service( + type=self._MAP.service_endpoints.get( + endp['type'], + taxonomy.UNKNOWN), + name=endp['name'], + id=identifier.norm_ns(endp['endpoints'][0].get('id', + endp['name'])), + admin_endp=endpoint.Endpoint( + name='admin', + url=endp['endpoints'][0]['adminURL']), + private_endp=endpoint.Endpoint( + name='private', + url=endp['endpoints'][0]['internalURL']), + public_endp=endpoint.Endpoint( + name='public', + url=endp['endpoints'][0]['publicURL'])) + + return service + + def _build_typeURI(self, req, service_type): + """Build typeURI of target + + Combines service type and corresponding path for greater detail. + """ + type_uri = '' + prev_key = None + for key in re.split('/', req.path): + key = self._clean_path(key) + if key in self._MAP.path_kw: + type_uri += '/' + key + elif prev_key in self._MAP.path_kw: + type_uri += '/' + self._MAP.path_kw[prev_key] + prev_key = key + return service_type + type_uri + + def _build_target(self, req, service): + """Build target resource.""" + target_typeURI = ( + self._build_typeURI(req, service.type) + if service.type != taxonomy.UNKNOWN else service.type) + target = resource.Resource(typeURI=target_typeURI, + id=service.id, name=service.name) + if service.admin_endp: + target.add_address(service.admin_endp) + if service.private_endp: + target.add_address(service.private_endp) + if service.public_endp: + target.add_address(service.public_endp) + return target + + def get_target_resource(self, req): + """Retrieve target information + + If discovery is enabled, target will attempt to retrieve information + from service catalog. If not, the information will be taken from + given config file. + """ + service_info = Service(type=taxonomy.UNKNOWN, name=taxonomy.UNKNOWN, + id=taxonomy.UNKNOWN, admin_endp=None, + private_endp=None, public_endp=None) + try: + catalog = ast.literal_eval( + req.environ['HTTP_X_SERVICE_CATALOG']) + except KeyError: + raise PycadfAuditApiConfigError( + 'Service catalog is missing. ' + 'Cannot discover target information') + + default_endpoint = None + for endp in catalog: + admin_urlparse = urlparse.urlparse( + endp['endpoints'][0]['adminURL']) + public_urlparse = urlparse.urlparse( + endp['endpoints'][0]['publicURL']) + req_url = urlparse.urlparse(req.host_url) + if (req_url.netloc == admin_urlparse.netloc + or req_url.netloc == public_urlparse.netloc): + service_info = self._get_service_info(endp) + break + elif (self._MAP.default_target_endpoint_type and + endp['type'] == self._MAP.default_target_endpoint_type): + default_endpoint = endp + else: + if default_endpoint: + service_info = self._get_service_info(default_endpoint) + return self._build_target(req, service_info) + + +class ClientResource(resource.Resource): + def __init__(self, project_id=None, **kwargs): + super(ClientResource, self).__init__(**kwargs) + if project_id is not None: + self.project_id = project_id + + +class KeystoneCredential(credential.Credential): + def __init__(self, identity_status=None, **kwargs): + super(KeystoneCredential, self).__init__(**kwargs) + if identity_status is not None: + self.identity_status = identity_status + + +class PycadfAuditApiConfigError(Exception): + """Error raised when pyCADF fails to configure correctly.""" + + class AuditMiddleware(object): """Create an audit event based on request/response. @@ -83,8 +318,7 @@ class AuditMiddleware(object): self._service_name = conf.get('service_name') self._ignore_req_list = [x.upper().strip() for x in conf.get('ignore_req_list', '').split(',')] - self._cadf_audit = api.OpenStackAuditApi( - conf.get('audit_map_file')) + self._cadf_audit = OpenStackAuditApi(conf.get('audit_map_file')) transport_aliases = self._get_aliases(cfg.CONF.project) if messaging: @@ -107,40 +341,65 @@ class AuditMiddleware(object): 'event_type': event_type, 'payload': payload}) + def _create_event(self, req): + correlation_id = identifier.generate_uuid() + action = self._cadf_audit.get_action(req) + + initiator = ClientResource( + typeURI=taxonomy.ACCOUNT_USER, + id=identifier.norm_ns(str(req.environ['HTTP_X_USER_ID'])), + name=req.environ['HTTP_X_USER_NAME'], + host=host.Host(address=req.client_addr, agent=req.user_agent), + credential=KeystoneCredential( + token=req.environ['HTTP_X_AUTH_TOKEN'], + identity_status=req.environ['HTTP_X_IDENTITY_STATUS']), + project_id=identifier.norm_ns(req.environ['HTTP_X_PROJECT_ID'])) + target = self._cadf_audit.get_target_resource(req) + + event = factory.EventFactory().new_event( + eventType=cadftype.EVENTTYPE_ACTIVITY, + outcome=taxonomy.OUTCOME_PENDING, + action=action, + initiator=initiator, + target=target, + observer=resource.Resource(id='target')) + event.requestPath = req.path_qs + event.add_tag(tag.generate_name_value_tag('correlation_id', + correlation_id)) + # cache model in request to allow tracking of transistive steps. + req.environ['cadf_event'] = event + return event + @_log_and_ignore_error def _process_request(self, request): - correlation_id = pycadf.identifier.generate_uuid() - event = self._cadf_audit.create_event(request, correlation_id) - request.environ['cadf_event'] = event + event = self._create_event(request) self._emit_audit(context.get_admin_context().to_dict(), 'audit.http.request', event.as_dict()) @_log_and_ignore_error def _process_response(self, request, response=None): + # NOTE(gordc): handle case where error processing request if 'cadf_event' not in request.environ: - # NOTE(gordc): handle case where error processing request - correlation_id = pycadf.identifier.generate_uuid() - event = self._cadf_audit.create_event(request, correlation_id) - else: - event = request.environ['cadf_event'] + self._create_event(request) + event = request.environ['cadf_event'] if response: if response.status_int >= 200 and response.status_int < 400: - result = pycadf.cadftaxonomy.OUTCOME_SUCCESS + result = taxonomy.OUTCOME_SUCCESS else: - result = pycadf.cadftaxonomy.OUTCOME_FAILURE - event.reason = pycadf.reason.Reason( + result = taxonomy.OUTCOME_FAILURE + event.reason = reason.Reason( reasonType='HTTP', reasonCode=str(response.status_int)) else: - result = pycadf.cadftaxonomy.UNKNOWN + result = taxonomy.UNKNOWN event.outcome = result event.add_reporterstep( - pycadf.reporterstep.Reporterstep( - role=pycadf.cadftype.REPORTER_ROLE_MODIFIER, - reporter=pycadf.resource.Resource(id='target'), - reporterTime=pycadf.timestamp.get_utc_now())) + reporterstep.Reporterstep( + role=cadftype.REPORTER_ROLE_MODIFIER, + reporter=resource.Resource(id='target'), + reporterTime=timestamp.get_utc_now())) self._emit_audit(context.get_admin_context().to_dict(), 'audit.http.response', event.as_dict()) diff --git a/keystonemiddleware/tests/test_audit_middleware.py b/keystonemiddleware/tests/test_audit_middleware.py index 47261765..89e5aa44 100644 --- a/keystonemiddleware/tests/test_audit_middleware.py +++ b/keystonemiddleware/tests/test_audit_middleware.py @@ -13,9 +13,11 @@ import os import tempfile +import uuid import mock from oslo_config import cfg +from pycadf import identifier import testtools from testtools import matchers import webob @@ -38,28 +40,44 @@ class FakeFailingApp(object): raise Exception('It happens!') -@mock.patch('oslo.messaging.get_transport', mock.MagicMock()) -class AuditMiddlewareTest(testtools.TestCase): - +class BaseAuditMiddlewareTest(testtools.TestCase): def setUp(self): - super(AuditMiddlewareTest, self).setUp() - (self.fd, self.audit_map) = tempfile.mkstemp() + super(BaseAuditMiddlewareTest, self).setUp() + self.fd, self.audit_map = tempfile.mkstemp() + + with open(self.audit_map, "w") as f: + f.write("[custom_actions]\n") + f.write("reboot = start/reboot\n") + f.write("os-migrations/get = read\n\n") + f.write("[path_keywords]\n") + f.write("action = None\n") + f.write("os-hosts = host\n") + f.write("os-migrations = None\n") + f.write("reboot = None\n") + f.write("servers = server\n\n") + f.write("[service_endpoints]\n") + f.write("compute = service/compute") + cfg.CONF([], project='keystonemiddleware') + self.middleware = audit.AuditMiddleware( + FakeApp(), audit_map_file=self.audit_map, + service_name='pycadf') + self.addCleanup(lambda: os.close(self.fd)) self.addCleanup(cfg.CONF.reset) @staticmethod - def _get_environ_header(req_type): + def get_environ_header(req_type): env_headers = {'HTTP_X_SERVICE_CATALOG': '''[{"endpoints_links": [], "endpoints": [{"adminURL": - "http://host:8774/v2/admin", + "http://admin_host:8774", "region": "RegionOne", "publicURL": - "http://host:8774/v2/public", + "http://public_host:8774", "internalURL": - "http://host:8774/v2/internal", + "http://internal_host:8774", "id": "resource_id"}], "type": "compute", "name": "nova"},]''', @@ -71,15 +89,15 @@ class AuditMiddlewareTest(testtools.TestCase): env_headers['REQUEST_METHOD'] = req_type return env_headers + +@mock.patch('oslo.messaging.get_transport', mock.MagicMock()) +class AuditMiddlewareTest(BaseAuditMiddlewareTest): + def test_api_request(self): - middleware = audit.AuditMiddleware( - FakeApp(), - audit_map_file=self.audit_map, - service_name='pycadf') req = webob.Request.blank('/foo/bar', - environ=self._get_environ_header('GET')) + environ=self.get_environ_header('GET')) with mock.patch('oslo.messaging.Notifier.info') as notify: - middleware(req) + self.middleware(req) # Check first notification with only 'request' call_args = notify.call_args_list[0][0] self.assertEqual('audit.http.request', call_args[1]) @@ -97,15 +115,15 @@ class AuditMiddlewareTest(testtools.TestCase): self.assertIn('reporterchain', call_args[2]) def test_api_request_failure(self): - middleware = audit.AuditMiddleware( + self.middleware = audit.AuditMiddleware( FakeFailingApp(), audit_map_file=self.audit_map, service_name='pycadf') req = webob.Request.blank('/foo/bar', - environ=self._get_environ_header('GET')) + environ=self.get_environ_header('GET')) with mock.patch('oslo.messaging.Notifier.info') as notify: try: - middleware(req) + self.middleware(req) self.fail('Application exception has not been re-raised') except Exception: pass @@ -124,47 +142,39 @@ class AuditMiddlewareTest(testtools.TestCase): self.assertIn('reporterchain', call_args[2]) def test_process_request_fail(self): - middleware = audit.AuditMiddleware( - FakeApp(), - audit_map_file=self.audit_map, - service_name='pycadf') req = webob.Request.blank('/foo/bar', - environ=self._get_environ_header('GET')) + environ=self.get_environ_header('GET')) with mock.patch('oslo.messaging.Notifier.info', side_effect=Exception('error')) as notify: - middleware._process_request(req) + self.middleware._process_request(req) self.assertTrue(notify.called) def test_process_response_fail(self): - middleware = audit.AuditMiddleware( - FakeApp(), - audit_map_file=self.audit_map, - service_name='pycadf') req = webob.Request.blank('/foo/bar', - environ=self._get_environ_header('GET')) + environ=self.get_environ_header('GET')) with mock.patch('oslo.messaging.Notifier.info', side_effect=Exception('error')) as notify: - middleware._process_response(req, webob.response.Response()) + self.middleware._process_response(req, webob.response.Response()) self.assertTrue(notify.called) def test_ignore_req_opt(self): - middleware = audit.AuditMiddleware(FakeApp(), - audit_map_file=self.audit_map, - ignore_req_list='get, PUT') + self.middleware = audit.AuditMiddleware(FakeApp(), + audit_map_file=self.audit_map, + ignore_req_list='get, PUT') req = webob.Request.blank('/skip/foo', - environ=self._get_environ_header('GET')) + environ=self.get_environ_header('GET')) req1 = webob.Request.blank('/skip/foo', - environ=self._get_environ_header('PUT')) + environ=self.get_environ_header('PUT')) req2 = webob.Request.blank('/accept/foo', - environ=self._get_environ_header('POST')) + environ=self.get_environ_header('POST')) with mock.patch('oslo.messaging.Notifier.info') as notify: # Check GET/PUT request does not send notification - middleware(req) - middleware(req1) + self.middleware(req) + self.middleware(req1) self.assertEqual([], notify.call_args_list) # Check non-GET/PUT request does send notification - middleware(req2) + self.middleware(req2) self.assertThat(notify.call_args_list, matchers.HasLength(2)) call_args = notify.call_args_list[0][0] self.assertEqual('audit.http.request', call_args[1]) @@ -175,15 +185,11 @@ class AuditMiddlewareTest(testtools.TestCase): self.assertEqual('/accept/foo', call_args[2]['requestPath']) def test_api_request_no_messaging(self): - middleware = audit.AuditMiddleware( - FakeApp(), - audit_map_file=self.audit_map, - service_name='pycadf') req = webob.Request.blank('/foo/bar', - environ=self._get_environ_header('GET')) + environ=self.get_environ_header('GET')) with mock.patch('keystonemiddleware.audit.messaging', None): with mock.patch('keystonemiddleware.audit._LOG.info') as log: - middleware(req) + self.middleware(req) # Check first notification with only 'request' call_args = log.call_args_list[0][0] self.assertEqual('audit.http.request', @@ -200,7 +206,7 @@ class AuditMiddlewareTest(testtools.TestCase): audit_map_file=self.audit_map, service_name='pycadf') req = webob.Request.blank('/foo/bar', - environ=self._get_environ_header('GET')) + environ=self.get_environ_header('GET')) with mock.patch('oslo.messaging.Notifier.info') as notify: middleware(req) self.assertIsNotNone(req.environ.get('cadf_event')) @@ -215,16 +221,265 @@ class AuditMiddlewareTest(testtools.TestCase): audit_map_file=self.audit_map, service_name='pycadf') req = webob.Request.blank('/foo/bar', - environ=self._get_environ_header('GET')) + environ=self.get_environ_header('GET')) with mock.patch('oslo.messaging.Notifier.info', side_effect=Exception('error')) as notify: middleware._process_request(req) self.assertTrue(notify.called) req2 = webob.Request.blank('/foo/bar', - environ=self._get_environ_header('GET')) + environ=self.get_environ_header('GET')) with mock.patch('oslo.messaging.Notifier.info') as notify: middleware._process_response(req2, webob.response.Response()) self.assertTrue(notify.called) # ensure event is not the same across requests self.assertNotEqual(req.environ['cadf_event'].id, notify.call_args_list[0][0][2]['id']) + + +@mock.patch('oslo.messaging', mock.MagicMock()) +class AuditApiLogicTest(BaseAuditMiddlewareTest): + + def api_request(self, method, url): + req = webob.Request.blank(url, environ=self.get_environ_header(method), + remote_addr='192.168.0.1') + self.middleware._process_request(req) + return req + + def test_get_list(self): + req = self.api_request('GET', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['action'], 'read/list') + self.assertEqual(payload['typeURI'], + 'http://schemas.dmtf.org/cloud/audit/1.0/event') + self.assertEqual(payload['outcome'], 'pending') + self.assertEqual(payload['eventType'], 'activity') + self.assertEqual(payload['target']['name'], 'nova') + self.assertEqual(payload['target']['id'], 'openstack:resource_id') + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') + self.assertEqual(len(payload['target']['addresses']), 3) + self.assertEqual(payload['target']['addresses'][0]['name'], 'admin') + self.assertEqual(payload['target']['addresses'][0]['url'], + 'http://admin_host:8774') + self.assertEqual(payload['initiator']['id'], 'openstack:user_id') + self.assertEqual(payload['initiator']['name'], 'user_name') + self.assertEqual(payload['initiator']['project_id'], + 'openstack:tenant_id') + self.assertEqual(payload['initiator']['host']['address'], + '192.168.0.1') + self.assertEqual(payload['initiator']['typeURI'], + 'service/security/account/user') + self.assertNotEqual(payload['initiator']['credential']['token'], + 'token') + self.assertEqual(payload['initiator']['credential']['identity_status'], + 'Confirmed') + self.assertNotIn('reason', payload) + self.assertNotIn('reporterchain', payload) + self.assertEqual(payload['observer']['id'], 'target') + self.assertEqual(req.path, payload['requestPath']) + + def test_get_read(self): + req = self.api_request('GET', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers/' + + str(uuid.uuid4())) + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers/server') + self.assertEqual(payload['action'], 'read') + self.assertEqual(payload['outcome'], 'pending') + + def test_get_unknown_endpoint(self): + req = self.api_request('GET', 'http://unknown:8774/v2/' + + str(uuid.uuid4()) + '/servers') + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['action'], 'read/list') + self.assertEqual(payload['outcome'], 'pending') + self.assertEqual(payload['target']['name'], 'unknown') + self.assertEqual(payload['target']['id'], 'unknown') + self.assertEqual(payload['target']['typeURI'], 'unknown') + + def test_get_unknown_endpoint_default_set(self): + with open(self.audit_map, "w") as f: + f.write("[DEFAULT]\n") + f.write("target_endpoint_type = compute\n") + f.write("[path_keywords]\n") + f.write("servers = server\n\n") + f.write("[service_endpoints]\n") + f.write("compute = service/compute") + + self.middleware = audit.AuditMiddleware( + FakeApp(), audit_map_file=self.audit_map, + service_name='pycadf') + + req = self.api_request('GET', 'http://unknown:8774/v2/' + + str(uuid.uuid4()) + '/servers') + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['action'], 'read/list') + self.assertEqual(payload['outcome'], 'pending') + self.assertEqual(payload['target']['name'], 'nova') + self.assertEqual(payload['target']['id'], 'openstack:resource_id') + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') + + def test_put(self): + req = self.api_request('PUT', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') + self.assertEqual(payload['action'], 'update') + self.assertEqual(payload['outcome'], 'pending') + + def test_delete(self): + req = self.api_request('DELETE', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') + self.assertEqual(payload['action'], 'delete') + self.assertEqual(payload['outcome'], 'pending') + + def test_head(self): + req = self.api_request('HEAD', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') + self.assertEqual(payload['action'], 'read') + self.assertEqual(payload['outcome'], 'pending') + + def test_post_update(self): + req = self.api_request('POST', + 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers/' + + str(uuid.uuid4())) + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers/server') + self.assertEqual(payload['action'], 'update') + self.assertEqual(payload['outcome'], 'pending') + + def test_post_create(self): + req = self.api_request('POST', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers') + self.assertEqual(payload['action'], 'create') + self.assertEqual(payload['outcome'], 'pending') + + def test_post_action(self): + req = webob.Request.blank('http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers/action', + environ=self.get_environ_header('POST')) + req.body = b'{"createImage" : {"name" : "new-image","metadata": ' \ + b'{"ImageType": "Gold","ImageVersion": "2.0"}}}' + self.middleware._process_request(req) + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers/action') + self.assertEqual(payload['action'], 'update/createImage') + self.assertEqual(payload['outcome'], 'pending') + + def test_post_empty_body_action(self): + req = self.api_request('POST', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers/action') + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['typeURI'], + 'service/compute/servers/action') + self.assertEqual(payload['action'], 'create') + self.assertEqual(payload['outcome'], 'pending') + + def test_custom_action(self): + req = self.api_request('GET', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/os-hosts/' + + str(uuid.uuid4()) + '/reboot') + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['typeURI'], + 'service/compute/os-hosts/host/reboot') + self.assertEqual(payload['action'], 'start/reboot') + self.assertEqual(payload['outcome'], 'pending') + + def test_custom_action_complex(self): + req = self.api_request('GET', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/os-migrations') + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['typeURI'], + 'service/compute/os-migrations') + self.assertEqual(payload['action'], 'read') + req = self.api_request('POST', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/os-migrations') + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['typeURI'], + 'service/compute/os-migrations') + self.assertEqual(payload['action'], 'create') + + def test_response_mod_msg(self): + req = self.api_request('GET', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') + payload = req.environ['cadf_event'].as_dict() + self.middleware._process_response(req, webob.Response()) + payload2 = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['id'], payload2['id']) + self.assertEqual(payload['tags'], payload2['tags']) + self.assertEqual(payload2['outcome'], 'success') + self.assertEqual(payload2['reason']['reasonType'], 'HTTP') + self.assertEqual(payload2['reason']['reasonCode'], '200') + self.assertEqual(len(payload2['reporterchain']), 1) + self.assertEqual(payload2['reporterchain'][0]['role'], 'modifier') + self.assertEqual(payload2['reporterchain'][0]['reporter']['id'], + 'target') + + def test_no_response(self): + req = self.api_request('GET', 'http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers') + payload = req.environ['cadf_event'].as_dict() + self.middleware._process_response(req, None) + payload2 = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['id'], payload2['id']) + self.assertEqual(payload['tags'], payload2['tags']) + self.assertEqual(payload2['outcome'], 'unknown') + self.assertNotIn('reason', payload2) + self.assertEqual(len(payload2['reporterchain']), 1) + self.assertEqual(payload2['reporterchain'][0]['role'], 'modifier') + self.assertEqual(payload2['reporterchain'][0]['reporter']['id'], + 'target') + + def test_missing_req(self): + req = webob.Request.blank('http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers', + environ=self.get_environ_header('GET')) + self.assertNotIn('cadf_event', req.environ) + self.middleware._process_response(req, webob.Response()) + self.assertIn('cadf_event', req.environ) + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['outcome'], 'success') + self.assertEqual(payload['reason']['reasonType'], 'HTTP') + self.assertEqual(payload['reason']['reasonCode'], '200') + self.assertEqual(payload['observer']['id'], 'target') + + def test_missing_catalog_endpoint_id(self): + env_headers = {'HTTP_X_SERVICE_CATALOG': + '''[{"endpoints_links": [], + "endpoints": [{"adminURL": + "http://admin_host:8774", + "region": "RegionOne", + "publicURL": + "http://public_host:8774", + "internalURL": + "http://internal_host:8774"}], + "type": "compute", + "name": "nova"},]''', + 'HTTP_X_USER_ID': 'user_id', + 'HTTP_X_USER_NAME': 'user_name', + 'HTTP_X_AUTH_TOKEN': 'token', + 'HTTP_X_PROJECT_ID': 'tenant_id', + 'HTTP_X_IDENTITY_STATUS': 'Confirmed', + 'REQUEST_METHOD': 'GET'} + req = webob.Request.blank('http://admin_host:8774/v2/' + + str(uuid.uuid4()) + '/servers', + environ=env_headers) + self.middleware._process_request(req) + payload = req.environ['cadf_event'].as_dict() + self.assertEqual(payload['target']['id'], identifier.norm_ns('nova'))