Add container action api

Add container action api to get container action information.

Change-Id: Ic3fa5e879baa6a5a01a1ae83740a2149a6f8003b
This commit is contained in:
Chaolei Li 2018-03-01 15:13:40 +08:00 committed by Hongbin Lu
parent 79e01261ce
commit 07dd607dac
9 changed files with 580 additions and 4 deletions

View File

@ -9,7 +9,7 @@ stops, pauses, unpauses, restarts, renames, commits, kills, attaches to containe
gets archive from container, puts archive to container, and adds security group
for specified container, executes command in a running container, gets logs
of a container, displays the running processes in a container, resizes
the tty session used by the exec.
the tty session used by the exec, list actions and action detail for a container.
Create new container
====================
@ -1227,3 +1227,101 @@ Response Example
.. literalinclude:: samples/container-network-list-resp.json
:language: javascript
List Actions For Container
==========================
.. rest_method:: GET /v1/containers/{container_ident}/container_actions
List actions for a container
Response Codes
--------------
.. rest_status_code:: success status.yaml
- 200
.. rest_status_code:: error status.yaml
- 401
- 403
- 404
Request
-------
.. rest_parameters:: parameters.yaml
- container_ident: container_ident
Response
--------
.. rest_parameters:: parameters.yaml
- action: action
- container_uuid: uuid
- message: message
- request_id: request_id_body
- start_time: start_time
- project_id: project_id_container_action
- user_id: user_id
**Example List Actions For Container: JSON response**
.. literalinclude:: samples/container-actions-list-resp.json
:language: javascript
Show Container Action Details
=============================
.. rest_method:: GET /v1/containers/{container_ident}/container_actions/{request_ident}
Show details for a container action.
Response Codes
--------------
.. rest_status_code:: success status.yaml
- 200
.. rest_status_code:: error status.yaml
- 401
- 403
- 404
Request
-------
.. rest_parameters:: parameters.yaml
- container_ident: container_ident
- request_ident: request_ident
Responce
--------
.. rest_parameters:: parameters.yaml
- action: action
- container_uuid: uuid
- message: message
- project_id: project_id_container_action
- request_id: request_id_body
- start_time: start_time
- user_id: user_id
- events: container_action_events
- events.event: event
- events.start_time: event_start_time
- events.finish_time: event_finish_time
- events.result: event_result
- events.traceback: event_traceback
**Example Show Container Action Details: JSON response**
.. literalinclude:: samples/container-action-get-resp.json
:language: javascript

View File

@ -17,6 +17,12 @@ host_ident:
in: path
required: true
type: string
request_ident:
description: |
The ID of the request.
in: path
required: true
type: string
destination_path:
description: |
The destination path in a container when putting archive to a container.
@ -164,6 +170,12 @@ width:
in: query
required: true
type: string
action:
description: |
The name of the action.
in: body
required: true
type: string
addresses:
type: string
description: |
@ -187,6 +199,24 @@ command:
Send command to the container.
in: body
type: string
container_action:
description: |
The container action object.
in: body
required: true
type: object
container_action_events:
description: |
The events occurred in this action.
in: body
required: true
type: array
container_actions:
description: |
List of the actions for the given container.
in: body
required: true
type: array
container_list:
type: array
in: body
@ -240,6 +270,54 @@ environment:
The environment variables.
in: body
type: array
event:
description: |
The name of the event.
in: body
required: true
type: string
event_finish_time:
description: |
The date and time when the event was finished. The date and time
stamp format is `ISO 8601 <https://en.wikipedia.org/wiki/ISO_8601>`_
::
CCYY-MM-DDThh:mm:ss±hh:mm
For example, ``2015-08-27T09:49:58-05:00``. The ``±hh:mm``
value, if included, is the time zone as an offset from UTC. In
the previous example, the offset value is ``-05:00``.
in: body
required: true
type: string
event_result:
description: |
The result of the event.
in: body
required: true
type: string
event_start_time:
description: |
The date and time when the event was started. The date and time
stamp format is `ISO 8601 <https://en.wikipedia.org/wiki/ISO_8601>`_
::
CCYY-MM-DDThh:mm:ss±hh:mm
For example, ``2015-08-27T09:49:58-05:00``. The ``±hh:mm``
value, if included, is the time zone as an offset from UTC. In
the previous example, the offset value is ``-05:00``.
in: body
required: true
type: string
event_traceback:
description: |
The traceback stack if error occurred in this event.
in: body
required: true
type: string
exec_exit_code:
description: |
The exit code of the command executed in a container.
@ -346,6 +424,12 @@ memory:
The container memory size in MiB.
in: body
type: integer
message:
description: |
The error message message about this action when error occurred.
in: body
required: true
type: string
name:
description: |
The name of the container.
@ -394,6 +478,12 @@ ports:
in: body
required: true
type: string
project_id_container_action:
description: |
The UUID of the project that this container belongs to.
in: body
required: ture
type: string
ps_output:
description: |
The output of zun top.
@ -406,6 +496,12 @@ report_count:
in: body
required: true
type: integer
request_id_body:
description: |
The request id generated when execute the API of this action.
in: body
required: true
type: string
restart_policy:
description: |
Restart policy to apply when a container exits. Allowed values are
@ -436,6 +532,21 @@ services:
in: body
required: true
type: array
start_time:
description: |
The date and time when the action was started. The date and time
stamp format is `ISO 8601 <https://en.wikipedia.org/wiki/ISO_8601>`_
::
CCYY-MM-DDThh:mm:ss±hh:mm
For example, ``2015-08-27T09:49:58-05:00``. The ``±hh:mm``
value, if included, is the time zone as an offset from UTC. In
the previous example, the offset value is ``-05:00``.
in: body
required: true
type: string
stat:
description: |
The stat information when doing get_archive.
@ -506,6 +617,12 @@ updated_at:
in: body
required: true
type: string
user_id:
description: |
The user ID of the user who owns the container.
in: body
required: true
type: string
uuid:
description: |
UUID of the resource.

View File

@ -0,0 +1,18 @@
{
"action": "create",
"events": [
{
"event": "container__do_container_start",
"finish_time": "2018-03-04 17:03:07+00:00",
"result": "Success",
"start_time": "2018-03-04 17:02:57+00:00",
"traceback": null
}
],
"container_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13",
"message": null,
"project_id": "853719b303ef4858a195535eb520e58d",
"request_id": "req-3293a3f1-b44c-4609-b8d2-d81b105636b8",
"start_time": "2018-03-04 17:02:54+00:00",
"user_id": "22e81669093742b7a74b1d715a9a5813"
}

View File

@ -0,0 +1,21 @@
[
{
"action": "create",
"container_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13",
"message": null,
"project_id": "853719b303ef4858a195535eb520e58d",
"request_id": "req-25517360-b757-47d3-be45-0e8d2a01b36a",
"start_time": "2018-03-04 19:48:49+00:00",
"user_id": "22e81669093742b7a74b1d715a9a5813"
},
{
"action": "stop",
"container_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13",
"message": null,
"project_id": "853719b303ef4858a195535eb520e58d",
"request_id": "req-3293a3f1-b44c-4609-b8d2-d81b105636b8",
"start_time": "2018-03-04 17:02:54+00:00",
"user_id": "22e81669093742b7a74b1d715a9a5813"
}
]

View File

@ -74,6 +74,82 @@ class ContainerCollection(collection.Collection):
return collection
class ContainersActionsController(base.Controller):
"""Controller for Container Actions."""
def __init__(self):
super(ContainersActionsController, self).__init__()
self._action_keys = ['action', 'container_uuid', 'request_id',
'user_id', 'project_id', 'start_time',
'message']
self._event_keys = ['event', 'start_time', 'finish_time', 'result',
'traceback']
def _format_action(self, action_raw):
action = {}
action_dict = action_raw.as_dict()
for key in self._action_keys:
action[key] = action_dict.get(key)
return action
def _format_event(self, event_raw, show_traceback=False):
event = {}
event_dict = event_raw.as_dict()
for key in self._event_keys:
# By default, non-admins are not allowed to see traceback details.
if key == 'traceback' and not show_traceback:
event['traceback'] = None
continue
event[key] = event_dict.get(key)
return event
@pecan.expose('json')
@exception.wrap_pecan_controller_exception
def get_all(self, container_ident, **kwargs):
"""Retrieve a list of container actions."""
context = pecan.request.context
policy.enforce(context, "container:actions",
action="container:actions")
container = utils.get_container(container_ident)
actions_raw = objects.ContainerAction.get_by_container_uuid(
context, container.uuid)
actions = [self._format_action(action) for action in actions_raw]
return actions
@pecan.expose('json')
@exception.wrap_pecan_controller_exception
def get_one(self, container_ident, request_ident, **kwargs):
"""Retrieve information about the action."""
context = pecan.request.context
policy.enforce(context, "container:actions",
action="container:actions")
container = utils.get_container(container_ident)
action = objects.ContainerAction.get_by_request_id(
context, container.uuid, request_ident)
if action is None:
raise exception.ResourceNotFound(name="Action", id=request_ident)
action_id = action.id
if CONF.database.backend == 'etcd':
# etcd using action.uuid get the unique action instead of action.id
action_id = action.uuid
action = self._format_action(action)
show_traceback = False
if policy.enforce(context, "container:action:events",
do_raise=False, action="container:action:events"):
show_traceback = True
events_raw = objects.ContainerActionEvent.get_by_action(context,
action_id)
action['events'] = [self._format_event(evt, show_traceback)
for evt in events_raw]
return action
class ContainersController(base.Controller):
"""Controller for Containers."""
@ -102,6 +178,8 @@ class ContainersController(base.Controller):
'remove_security_group': ['POST']
}
container_actions = ContainersActionsController()
@pecan.expose('json')
@exception.wrap_pecan_controller_exception
def get_all(self, **kwargs):

View File

@ -15,6 +15,7 @@ import itertools
from zun.common.policies import base
from zun.common.policies import capsule
from zun.common.policies import container
from zun.common.policies import container_action
from zun.common.policies import host
from zun.common.policies import image
from zun.common.policies import network
@ -29,5 +30,6 @@ def list_rules():
zun_service.list_rules(),
host.list_rules(),
capsule.list_rules(),
network.list_rules()
network.list_rules(),
container_action.list_rules()
)

View File

@ -0,0 +1,53 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_policy import policy
from zun.common.policies import base
ACTION = 'container:actions'
EVENT = 'container:action:events'
rules = [
policy.DocumentedRuleDefault(
name=ACTION,
check_str=base.RULE_ADMIN_OR_OWNER,
description='List actions and show action details for a container',
operations=[
{
'path': '/v1/containers/{container_ident}/container_actions/',
'method': 'GET'
},
{
'path': '/v1/containers/{container_ident}/container_actions/'
'{request_id}',
'method': 'GET'
}
]
),
policy.DocumentedRuleDefault(
name=EVENT,
check_str=base.RULE_ADMIN_API,
description="Add events details in action details for a container.",
operations=[
{
'path': '/v1/containers/{container_ident}/container_actions/'
'{request_id}',
'method': 'GET'
}
]
)
]
def list_rules():
return rules

View File

@ -1891,3 +1891,180 @@ class TestContainerEnforcement(api_base.FunctionalTest):
self._owner_check('container:%s' % action, self.post_json,
'/containers/%s/%s/' % (container.uuid, action),
{}, expect_errors=True)
class TestContainerActionController(api_base.FunctionalTest):
def _format_action(self, action, expect_traceback=True):
'''Remove keys that aren't serialized.'''
to_delete = ('id', 'finish_time', 'created_at', 'updated_at',
'deleted_at', 'deleted')
for key in to_delete:
if key in action:
del (action[key])
for event in action.get('events', []):
self._format_event(event, expect_traceback)
return action
def _format_event(self, event, expect_traceback=True):
'''Remove keys that aren't serialized.'''
to_delete = ['id', 'created_at', 'updated_at', 'deleted_at', 'deleted',
'action_id']
if not expect_traceback:
event['traceback'] = None
for key in to_delete:
if key in event:
del (event[key])
return event
@mock.patch('zun.objects.Container.get_by_uuid')
@mock.patch('zun.objects.ContainerAction.get_by_container_uuid')
def test_list_actions(self, mock_get_by_container_uuid,
mock_container_get_by_uuid):
test_container = utils.get_test_container()
test_action = utils.get_test_action_value(
container_uuid=test_container['uuid'])
container_object = objects.Container(self.context, **test_container)
action_object = objects.ContainerAction(self.context, **test_action)
mock_container_get_by_uuid.return_value = container_object
mock_get_by_container_uuid.return_value = [action_object]
response = self.get('/v1/containers/%s/container_actions' %
test_container['uuid'])
mock_get_by_container_uuid.assert_called_once_with(
mock.ANY,
test_container['uuid'])
self.assertEqual(200, response.status_int)
self.assertEqual(self._format_action(test_action),
self._format_action(response.json[0]))
@mock.patch('zun.objects.Container.get_by_uuid')
@mock.patch('zun.common.policy.enforce')
@mock.patch('zun.objects.ContainerActionEvent.get_by_action')
@mock.patch('zun.objects.ContainerAction.get_by_request_id')
def test_get_action_with_events_allowed(self, mock_get_by_request_id,
mock_get_by_action, mock_policy,
mock_container_get_by_uuid):
mock_policy.return_value = True
test_container = utils.get_test_container()
test_action = utils.get_test_action_value(
container_uuid=test_container['uuid'])
test_event = utils.get_test_action_event_value(
action_id=test_action['id'])
test_action['events'] = [test_event]
container_object = objects.Container(self.context, **test_container)
action_object = objects.ContainerAction(self.context, **test_action)
event_object = objects.ContainerActionEvent(self.context, **test_event)
mock_container_get_by_uuid.return_value = container_object
mock_get_by_request_id.return_value = action_object
mock_get_by_action.return_value = [event_object]
response = self.get('/v1/containers/%s/container_actions/%s' % (
test_container['uuid'], test_action['request_id']))
mock_get_by_request_id.assert_called_once_with(
mock.ANY, test_container['uuid'], test_action['request_id'])
mock_get_by_action.assert_called_once_with(mock.ANY, test_action['id'])
self.assertEqual(200, response.status_int)
self.assertEqual(self._format_action(test_action),
self._format_action(response.json))
@mock.patch('zun.objects.Container.get_by_uuid')
@mock.patch('zun.common.policy.enforce')
@mock.patch('zun.objects.ContainerActionEvent.get_by_action')
@mock.patch('zun.objects.ContainerAction.get_by_request_id')
def test_get_action_with_events_not_allowed(self, mock_get_by_request_id,
mock_get_by_action,
mock_policy,
mock_container_get_by_uuid):
mock_policy.return_value = False
test_container = utils.get_test_container()
container_obj = objects.Container(self.context, **test_container)
test_action = utils.get_test_action_value(
container_uuid=test_container['uuid'])
test_event = utils.get_test_action_event_value(
action_id=test_action['id'])
test_action['events'] = [test_event]
action_object = objects.ContainerAction(self.context, **test_action)
event_object = objects.ContainerActionEvent(self.context, **test_event)
mock_container_get_by_uuid.return_value = container_obj
mock_get_by_request_id.return_value = action_object
mock_get_by_action.return_value = [event_object]
response = self.get('/v1/containers/%s/container_actions/%s' % (
test_container['uuid'], test_action['request_id']))
mock_get_by_request_id.assert_called_once_with(
mock.ANY, test_container['uuid'], test_action['request_id'])
mock_get_by_action.assert_called_once_with(mock.ANY, test_action['id'])
self.assertEqual(200, response.status_int)
self.assertEqual(self._format_action(test_action,
expect_traceback=False),
self._format_action(response.json))
@mock.patch('zun.objects.Container.get_by_uuid')
@mock.patch('zun.objects.ContainerAction.get_by_request_id')
def test_action_not_found(self, mock_get_by_request_id,
mock_container_get_by_uuid):
test_container = utils.get_test_container()
container_obj = objects.Container(self.context, **test_container)
mock_container_get_by_uuid.return_value = container_obj
mock_get_by_request_id.return_value = None
fake_request_id = 'request'
self.assertRaises(AppError, self.get,
('/v1/containers/%s/container_actions/%s' %
(test_container['uuid'], fake_request_id)))
mock_get_by_request_id.assert_called_once_with(
mock.ANY, test_container['uuid'], fake_request_id)
@mock.patch('zun.objects.Container.get_by_uuid')
def test_container_not_found(self, mock_container_get_by_uuid):
test_container = utils.get_test_container()
self.assertRaises(AppError, self.get,
('/v1/containers/%s/container_actions'
% test_container['uuid']))
mock_container_get_by_uuid.assert_called_once_with(
mock.ANY, test_container['uuid'])
class TestContainerActionEnforcement(api_base.FunctionalTest):
def _common_policy_check(self, rule, func, *arg, **kwarg):
rules = dict({rule: 'project_id:non_fake'},
**kwarg.pop('bypass_rules', {}))
self.policy.set_rules(rules)
response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertEqual(
"Policy doesn't allow %s to be performed." % rule,
response.json['errors'][0]['detail'])
def test_list_actions_disallow_by_project(self):
container = obj_utils.create_test_container(self.context)
self._common_policy_check(
'container:actions', self.get,
'/v1/containers/%s/container_actions/' % container.uuid,
expect_errors=True)
def test_get_action_disallow_by_project(self):
container = obj_utils.create_test_container(self.context)
self._common_policy_check(
'container:actions', self.get,
'/v1/containers/%s/container_actions/fake_request' %
container.uuid, expect_errors=True)

View File

@ -439,7 +439,7 @@ class FakeObject(object):
return getattr(self, key)
def get_test_action(**kwargs):
def get_test_action_value(**kwargs):
action_values = {
'created_at': kwargs.get('created_at'),
'updated_at': kwargs.get('updated_at'),
@ -455,13 +455,19 @@ def get_test_action(**kwargs):
'message': kwargs.get('message', 'fake-message'),
}
return action_values
def get_test_action(**kwargs):
action_values = get_test_action_value(**kwargs)
fake_action = FakeObject()
for k, v in action_values.items():
setattr(fake_action, k, v)
return fake_action
def get_test_action_event(**kwargs):
def get_test_action_event_value(**kwargs):
event_values = {
'created_at': kwargs.get('created_at'),
@ -475,6 +481,12 @@ def get_test_action_event(**kwargs):
'traceback': kwargs.get('traceback', 'fake-tb'),
}
return event_values
def get_test_action_event(**kwargs):
event_values = get_test_action_event_value(**kwargs)
fake_event = FakeObject()
for k, v in event_values.items():
setattr(fake_event, k, v)