From c2f7d6585818c04e626aa4b6c292e5c2660cb8b3 Mon Sep 17 00:00:00 2001
From: Yikun Jiang <yikunkero@gmail.com>
Date: Wed, 28 Mar 2018 11:59:09 +0800
Subject: [PATCH] Add host/hostId to instance action events API

This patch adds a new microversion to
``GET /servers/{server_id}/os-instance-actions/{req_id}`` API to
include the ``host`` field for admin and an ``hostId`` for all users
by default. And the display of newly added ``host`` field will be
controlled by the same policy as the ``traceback`` field.

The newly added fields can be used to determine on which host a
given action event occurred.

Part of blueprint: add-host-to-instance-action-events

Change-Id: I2f8b4a12a088b9ed96b428eafde2e0c478fb1db5
---
 api-ref/source/os-instance-actions.inc        | 10 +++--
 api-ref/source/parameters.yaml                | 23 +++++++++++
 .../instance-action-get-non-admin-resp.json   | 21 ++++++++++
 .../v2.62/instance-action-get-resp.json       | 23 +++++++++++
 .../v2.62/instance-actions-list-resp.json     | 24 +++++++++++
 ...instance-actions-list-with-limit-resp.json | 20 ++++++++++
 ...nstance-actions-list-with-marker-resp.json | 14 +++++++
 ...ce-actions-list-with-timestamp-filter.json | 14 +++++++
 .../versions/v21-version-get-resp.json        |  2 +-
 .../versions/versions-get-resp.json           |  2 +-
 nova/api/openstack/api_version_request.py     |  4 +-
 .../api/openstack/compute/instance_actions.py | 21 ++++++++--
 .../compute/rest_api_version_history.rst      | 11 +++++
 nova/policies/instance_actions.py             |  5 ++-
 ...nstance-action-get-non-admin-resp.json.tpl | 21 ++++++++++
 .../v2.62/instance-action-get-resp.json.tpl   | 23 +++++++++++
 .../v2.62/instance-actions-list-resp.json.tpl | 24 +++++++++++
 ...ance-actions-list-with-limit-resp.json.tpl | 20 ++++++++++
 ...nce-actions-list-with-marker-resp.json.tpl | 14 +++++++
 ...ctions-list-with-timestamp-filter.json.tpl | 14 +++++++
 .../api_sample_tests/test_instance_actions.py | 17 ++++++++
 .../compute/test_instance_actions.py          | 40 +++++++++++++++----
 nova/tests/unit/fake_server_actions.py        | 11 +++--
 ...stance-action-events-aad2cc18fe191afa.yaml | 10 +++++
 24 files changed, 366 insertions(+), 22 deletions(-)
 create mode 100644 doc/api_samples/os-instance-actions/v2.62/instance-action-get-non-admin-resp.json
 create mode 100644 doc/api_samples/os-instance-actions/v2.62/instance-action-get-resp.json
 create mode 100644 doc/api_samples/os-instance-actions/v2.62/instance-actions-list-resp.json
 create mode 100644 doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-limit-resp.json
 create mode 100644 doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-marker-resp.json
 create mode 100644 doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-timestamp-filter.json
 create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-action-get-non-admin-resp.json.tpl
 create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-action-get-resp.json.tpl
 create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-resp.json.tpl
 create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-limit-resp.json.tpl
 create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-marker-resp.json.tpl
 create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-timestamp-filter.json.tpl
 create mode 100644 releasenotes/notes/add-host-to-instance-action-events-aad2cc18fe191afa.yaml

diff --git a/api-ref/source/os-instance-actions.inc b/api-ref/source/os-instance-actions.inc
index 9e2530703bde..df961d45e9b1 100644
--- a/api-ref/source/os-instance-actions.inc
+++ b/api-ref/source/os-instance-actions.inc
@@ -111,14 +111,16 @@ Response
   - events.finish_time: event_finish_time
   - events.result: event_result
   - events.traceback: event_traceback
+  - events.hostId: event_hostId
+  - events.host: event_host
   - updated_at: updated_instance_action
 
-**Example Show Server Action Details For Admin (v2.1)**
+**Example Show Server Action Details For Admin (v2.62)**
 
-.. literalinclude:: ../../doc/api_samples/os-instance-actions/instance-action-get-resp.json
+.. literalinclude:: ../../doc/api_samples/os-instance-actions/v2.62/instance-action-get-resp.json
    :language: javascript
 
-**Example Show Server Action Details For Non-Admin (v2.51)**
+**Example Show Server Action Details For Non-Admin (v2.62)**
 
-.. literalinclude:: ../../doc/api_samples/os-instance-actions/v2.51/instance-action-get-non-admin-resp.json
+.. literalinclude:: ../../doc/api_samples/os-instance-actions/v2.62/instance-action-get-non-admin-resp.json
    :language: javascript
diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml
index e3c1be0664ca..a1926f811de8 100644
--- a/api-ref/source/parameters.yaml
+++ b/api-ref/source/parameters.yaml
@@ -2357,6 +2357,29 @@ event_finish_time:
   in: body
   required: true
   type: string
+event_host:
+  min_version: 2.62
+  description: |
+    The name of the host on which the event occurred.
+
+    Policy defaults enable only users with the administrative role to see
+    an instance action event host. Cloud providers can change these
+    permissions through the ``policy.json`` file.
+  in: body
+  required: false
+  type: string
+event_hostId:
+  min_version: 2.62
+  description: |
+    An obfuscated hashed host ID string, or the empty string if there is no
+    host for the event. This is a hashed value so will not actually look like
+    a hostname, and is hashed with data from the project_id, so the same
+    physical host as seen by two different project_ids will be different.
+    This is useful when within the same project you need to determine if two
+    events occurred on the same or different physical hosts.
+  in: body
+  required: true
+  type: string
 event_name:
   description: |
     The event name. A valid value is ``network-changed``, ``network-vif-plugged``,
diff --git a/doc/api_samples/os-instance-actions/v2.62/instance-action-get-non-admin-resp.json b/doc/api_samples/os-instance-actions/v2.62/instance-action-get-non-admin-resp.json
new file mode 100644
index 000000000000..115604d6ac89
--- /dev/null
+++ b/doc/api_samples/os-instance-actions/v2.62/instance-action-get-non-admin-resp.json
@@ -0,0 +1,21 @@
+{
+    "instanceAction": {
+        "action": "stop",
+        "events": [
+            {
+                "event": "compute_stop_instance",
+                "finish_time": "2018-04-25T01:26:34.784165",
+                "hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6",
+                "result": "Success",
+                "start_time": "2018-04-25T01:26:34.612020"
+            }
+        ],
+        "instance_uuid": "79edaa44-ad4f-4af7-b994-154518c2b927",
+        "message": null,
+        "project_id": "6f70656e737461636b20342065766572",
+        "request_id": "req-8eb28d4a-db6c-4337-bab8-ce154e9c620e",
+        "start_time": "2018-04-25T01:26:34.388280",
+        "updated_at": "2018-04-25T01:26:34.784165",
+        "user_id": "fake"
+    }
+}
\ No newline at end of file
diff --git a/doc/api_samples/os-instance-actions/v2.62/instance-action-get-resp.json b/doc/api_samples/os-instance-actions/v2.62/instance-action-get-resp.json
new file mode 100644
index 000000000000..57ae490f023f
--- /dev/null
+++ b/doc/api_samples/os-instance-actions/v2.62/instance-action-get-resp.json
@@ -0,0 +1,23 @@
+{
+    "instanceAction": {
+        "action": "stop",
+        "events": [
+            {
+                "event": "compute_stop_instance",
+                "finish_time": "2018-04-25T01:26:36.790544",
+                "host": "compute",
+                "hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6",
+                "result": "Success",
+                "start_time": "2018-04-25T01:26:36.539271",
+                "traceback": null
+            }
+        ],
+        "instance_uuid": "4bf3473b-d550-4b65-9409-292d44ab14a2",
+        "message": null,
+        "project_id": "6f70656e737461636b20342065766572",
+        "request_id": "req-0d819d5c-1527-4669-bdf0-ffad31b5105b",
+        "start_time": "2018-04-25T01:26:36.341290",
+        "updated_at": "2018-04-25T01:26:36.790544",
+        "user_id": "admin"
+    }
+}
\ No newline at end of file
diff --git a/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-resp.json b/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-resp.json
new file mode 100644
index 000000000000..0b2254126b1d
--- /dev/null
+++ b/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-resp.json
@@ -0,0 +1,24 @@
+{
+    "instanceActions": [
+        {
+            "action": "stop",
+            "instance_uuid": "15835b6f-1e14-4cfa-9f66-1abea1a1c0d5",
+            "message": null,
+            "project_id": "6f70656e737461636b20342065766572",
+            "request_id": "req-f04d4b92-6241-42da-b82d-2cedb225c58d",
+            "start_time": "2018-04-25T01:26:36.036697",
+            "updated_at": "2018-04-25T01:26:36.525308",
+            "user_id": "admin"
+        },
+        {
+            "action": "create",
+            "instance_uuid": "15835b6f-1e14-4cfa-9f66-1abea1a1c0d5",
+            "message": null,
+            "project_id": "6f70656e737461636b20342065766572",
+            "request_id": "req-d8790618-9bbf-4df0-8af8-fc9e24de29c0",
+            "start_time": "2018-04-25T01:26:33.692125",
+            "updated_at": "2018-04-25T01:26:35.993821",
+            "user_id": "admin"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-limit-resp.json b/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-limit-resp.json
new file mode 100644
index 000000000000..8b2c3bfd8e5c
--- /dev/null
+++ b/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-limit-resp.json
@@ -0,0 +1,20 @@
+{
+    "instanceActions": [
+        {
+            "action": "stop",
+            "instance_uuid": "7a580cc0-3469-441a-9736-d5fce91003f9",
+            "message": null,
+            "project_id": "6f70656e737461636b20342065766572",
+            "request_id": "req-b8ffb713-61a2-4e7c-a705-37052cba9d6e",
+            "start_time": "2018-04-25T01:26:28.955571",
+            "updated_at": "2018-04-25T01:26:29.414973",
+            "user_id": "fake"
+        }
+    ],
+    "links": [
+        {
+            "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/7a580cc0-3469-441a-9736-d5fce91003f9/os-instance-actions?limit=1&marker=req-b8ffb713-61a2-4e7c-a705-37052cba9d6e",
+            "rel": "next"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-marker-resp.json b/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-marker-resp.json
new file mode 100644
index 000000000000..3f6921cb795e
--- /dev/null
+++ b/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-marker-resp.json
@@ -0,0 +1,14 @@
+{
+    "instanceActions": [
+        {
+            "action": "create",
+            "instance_uuid": "9bde1fd5-8435-45c5-afc1-bedd0605275b",
+            "message": null,
+            "project_id": "6f70656e737461636b20342065766572",
+            "request_id": "req-4510fb10-447f-4572-a64d-c2324547d86c",
+            "start_time": "2018-04-25T01:26:33.710291",
+            "updated_at": "2018-04-25T01:26:35.374936",
+            "user_id": "fake"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-timestamp-filter.json b/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-timestamp-filter.json
new file mode 100644
index 000000000000..346c93af7a93
--- /dev/null
+++ b/doc/api_samples/os-instance-actions/v2.62/instance-actions-list-with-timestamp-filter.json
@@ -0,0 +1,14 @@
+{
+    "instanceActions": [
+        {
+            "action": "stop",
+            "instance_uuid": "2150964c-30fe-4214-9547-8822375aa7d0",
+            "message": null,
+            "project_id": "6f70656e737461636b20342065766572",
+            "request_id": "req-0c3b2079-0a44-474d-a5b2-7466d4b4c642",
+            "start_time": "2018-04-25T01:26:29.594237",
+            "updated_at": "2018-04-25T01:26:30.065061",
+            "user_id": "admin"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json
index 6e50403cbabd..a25c2b0a4e25 100644
--- a/doc/api_samples/versions/v21-version-get-resp.json
+++ b/doc/api_samples/versions/v21-version-get-resp.json
@@ -19,7 +19,7 @@
             }
         ],
         "status": "CURRENT",
-        "version": "2.61",
+        "version": "2.62",
         "min_version": "2.1",
         "updated": "2013-07-23T11:33:21Z"
     }
diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json
index 4d3c06a7caff..d4a531ca3503 100644
--- a/doc/api_samples/versions/versions-get-resp.json
+++ b/doc/api_samples/versions/versions-get-resp.json
@@ -22,7 +22,7 @@
                 }
             ],
             "status": "CURRENT",
-            "version": "2.61",
+            "version": "2.62",
             "min_version": "2.1",
             "updated": "2013-07-23T11:33:21Z"
         }
diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py
index 4df2854c3592..12929d4ccc4c 100644
--- a/nova/api/openstack/api_version_request.py
+++ b/nova/api/openstack/api_version_request.py
@@ -146,6 +146,8 @@ REST_API_VERSION_HISTORY = """REST API Version History:
     * 2.61 - Exposes flavor extra_specs in the flavor representation. Flavor
              extra_specs will be included in Response body of GET, POST, PUT
              /flavors APIs.
+    * 2.62 - Add ``host`` and ``hostId`` fields to instance action detail API
+             responses.
 """
 
 # The minimum and maximum versions of the API supported
@@ -154,7 +156,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
 # Note(cyeoh): This only applies for the v2.1 API once microversions
 # support is fully merged. It does not affect the V2 API.
 _MIN_API_VERSION = "2.1"
-_MAX_API_VERSION = "2.61"
+_MAX_API_VERSION = "2.62"
 DEFAULT_API_VERSION = _MIN_API_VERSION
 
 # Almost all proxy APIs which are related to network, images and baremetal
diff --git a/nova/api/openstack/compute/instance_actions.py b/nova/api/openstack/compute/instance_actions.py
index f859131756df..aef7440340ce 100644
--- a/nova/api/openstack/compute/instance_actions.py
+++ b/nova/api/openstack/compute/instance_actions.py
@@ -53,13 +53,20 @@ class InstanceActionsController(wsgi.Controller):
             action[key] = action_raw.get(key)
         return action
 
-    def _format_event(self, event_raw, show_traceback=False):
+    def _format_event(self, event_raw, project_id, show_traceback=False,
+                      show_host=False, show_hostid=False):
         event = {}
         for key in EVENT_KEYS:
             # By default, non-admins are not allowed to see traceback details.
             if key == 'traceback' and not show_traceback:
                 continue
             event[key] = event_raw.get(key)
+        # By default, non-admins are not allowed to see host.
+        if show_host:
+            event['host'] = event_raw['host']
+        if show_hostid:
+            event['hostId'] = utils.generate_hostid(event_raw['host'],
+                                                    project_id)
         return event
 
     @wsgi.Controller.api_version("2.1", "2.20")
@@ -138,18 +145,26 @@ class InstanceActionsController(wsgi.Controller):
         # by default.
         show_events = False
         show_traceback = False
+        show_host = False
         if context.can(ia_policies.POLICY_ROOT % 'events', fatal=False):
             # For all microversions, the user can see all event details
             # including the traceback.
             show_events = show_traceback = True
+            show_host = api_version_request.is_supported(req, '2.62')
         elif api_version_request.is_supported(req, '2.51'):
             # The user is not able to see all event details, but they can at
             # least see the non-traceback event details.
             show_events = True
 
+        # An obfuscated hashed host id is returned since microversion 2.62
+        # for all users.
+        show_hostid = api_version_request.is_supported(req, '2.62')
+
         if show_events:
             events_raw = self.action_api.action_events_get(context, instance,
                                                            action_id)
-            action['events'] = [self._format_event(evt, show_traceback)
-                                for evt in events_raw]
+            action['events'] = [self._format_event(
+                evt, action['project_id'], show_traceback=show_traceback,
+                show_host=show_host, show_hostid=show_hostid
+            ) for evt in events_raw]
         return {'instanceAction': action}
diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst
index 42ff34513556..b1042f22fb15 100644
--- a/nova/api/openstack/compute/rest_api_version_history.rst
+++ b/nova/api/openstack/compute/rest_api_version_history.rst
@@ -780,3 +780,14 @@ Response body of the following APIs:
 * ``GET /flavors/{flavor_id}``
 * ``POST /flavors``
 * ``PUT /flavors/{flavor_id}``
+
+2.62
+----
+
+Adds ``host`` (hostname) and ``hostId`` (an obfuscated hashed host id string)
+fields to the instance action
+``GET /servers/{server_id}/os-instance-actions/{req_id}`` API. The display of
+the newly added ``host`` field will be controlled via policy rule
+``os_compute_api:os-instance-actions:events``, which is the same policy used
+for the ``events.traceback`` field. If the user is prevented by policy, only
+``hostId`` will be displayed.
diff --git a/nova/policies/instance_actions.py b/nova/policies/instance_actions.py
index 46a1e0df67d9..3d800b2bd62d 100644
--- a/nova/policies/instance_actions.py
+++ b/nova/policies/instance_actions.py
@@ -31,7 +31,10 @@ instance_actions_policies = [
 This check is performed only after the check
 os_compute_api:os-instance-actions passes. Beginning with
 Microversion 2.51, events details are always included; traceback
-information is provided per event if policy enforcement passes.""",
+information is provided per event if policy enforcement passes.
+Beginning with Microversion 2.62, each event includes a hashed
+host identifier and, if policy enforcement passes, the name of
+the host.""",
         [
             {
                 'method': 'GET',
diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-action-get-non-admin-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-action-get-non-admin-resp.json.tpl
new file mode 100644
index 000000000000..7641428d17e6
--- /dev/null
+++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-action-get-non-admin-resp.json.tpl
@@ -0,0 +1,21 @@
+{
+    "instanceAction": {
+        "action": "stop",
+        "instance_uuid": "%(uuid)s",
+        "request_id": "%(request_id)s",
+        "user_id": "%(user_id)s",
+        "project_id": "%(project_id)s",
+        "start_time": "%(strtime)s",
+        "updated_at": "%(strtime)s",
+        "message": null,
+        "events": [
+            {
+                "event": "compute_stop_instance",
+                "start_time": "%(strtime)s",
+                "finish_time": "%(strtime)s",
+                "result": "Success",
+                "hostId": "%(event_hostId)s"
+            }
+        ]
+    }
+}
diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-action-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-action-get-resp.json.tpl
new file mode 100644
index 000000000000..fe3a4eec8fcd
--- /dev/null
+++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-action-get-resp.json.tpl
@@ -0,0 +1,23 @@
+{
+    "instanceAction": {
+        "action": "stop",
+        "instance_uuid": "%(uuid)s",
+        "request_id": "%(request_id)s",
+        "user_id": "%(user_id)s",
+        "project_id": "%(project_id)s",
+        "start_time": "%(strtime)s",
+        "updated_at": "%(strtime)s",
+        "message": null,
+        "events": [
+            {
+                "event": "compute_stop_instance",
+                "start_time": "%(strtime)s",
+                "finish_time": "%(strtime)s",
+                "result": "Success",
+                "traceback": null,
+                "host": "%(event_host)s",
+                "hostId": "%(event_hostId)s"
+            }
+        ]
+    }
+}
diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-resp.json.tpl
new file mode 100644
index 000000000000..19161da37483
--- /dev/null
+++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-resp.json.tpl
@@ -0,0 +1,24 @@
+{
+    "instanceActions": [
+        {
+            "action": "stop",
+            "instance_uuid": "%(uuid)s",
+            "request_id": "%(request_id)s",
+            "user_id": "%(user_id)s",
+            "project_id": "%(project_id)s",
+            "start_time": "%(strtime)s",
+            "updated_at": "%(strtime)s",
+            "message": null
+        },
+        {
+            "action": "create",
+            "instance_uuid": "%(uuid)s",
+            "request_id": "%(request_id)s",
+            "user_id": "%(user_id)s",
+            "project_id": "%(project_id)s",
+            "start_time": "%(strtime)s",
+            "updated_at": "%(strtime)s",
+            "message": null
+        }
+    ]
+}
diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-limit-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-limit-resp.json.tpl
new file mode 100644
index 000000000000..dedbd8d8addc
--- /dev/null
+++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-limit-resp.json.tpl
@@ -0,0 +1,20 @@
+{
+    "instanceActions": [
+        {
+            "action": "stop",
+            "instance_uuid": "%(uuid)s",
+            "request_id": "%(request_id)s",
+            "user_id": "%(user_id)s",
+            "project_id": "%(project_id)s",
+            "start_time": "%(strtime)s",
+            "updated_at": "%(strtime)s",
+            "message": null
+        }
+    ],
+    "links": [
+        {
+            "href": "%(versioned_compute_endpoint)s/servers/%(uuid)s/os-instance-actions?limit=1&marker=%(request_id)s",
+            "rel": "next"
+        }
+    ]
+}
diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-marker-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-marker-resp.json.tpl
new file mode 100644
index 000000000000..3e1f1efa8bb0
--- /dev/null
+++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-marker-resp.json.tpl
@@ -0,0 +1,14 @@
+{
+    "instanceActions": [
+        {
+            "action": "create",
+            "instance_uuid": "%(uuid)s",
+            "request_id": "%(request_id)s",
+            "user_id": "%(user_id)s",
+            "project_id": "%(project_id)s",
+            "start_time": "%(strtime)s",
+            "updated_at": "%(strtime)s",
+            "message": null
+        }
+    ]
+}
diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-timestamp-filter.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-timestamp-filter.json.tpl
new file mode 100644
index 000000000000..4b81af86b045
--- /dev/null
+++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.62/instance-actions-list-with-timestamp-filter.json.tpl
@@ -0,0 +1,14 @@
+{
+    "instanceActions": [
+        {
+            "action": "stop",
+            "instance_uuid": "%(uuid)s",
+            "request_id": "%(request_id)s",
+            "user_id": "%(user_id)s",
+            "project_id": "%(project_id)s",
+            "start_time": "%(strtime)s",
+            "updated_at": "%(strtime)s",
+            "message": null
+        }
+    ]
+}
diff --git a/nova/tests/functional/api_sample_tests/test_instance_actions.py b/nova/tests/functional/api_sample_tests/test_instance_actions.py
index c36c62ceadc6..44b6c44be93c 100644
--- a/nova/tests/functional/api_sample_tests/test_instance_actions.py
+++ b/nova/tests/functional/api_sample_tests/test_instance_actions.py
@@ -122,3 +122,20 @@ class ServerActionsV258SampleJsonTest(ServerActionsV251AdminSampleJsonTest):
 
 class ServerActionsV258NonAdminSampleJsonTest(ServerActionsV258SampleJsonTest):
     ADMIN_API = False
+
+
+class ServerActionsV262SampleJsonTest(ServerActionsV258SampleJsonTest):
+    microversion = '2.62'
+    scenarios = [('v2_62', {'api_major_version': 'v2.1'})]
+
+    def _get_subs(self):
+        return {
+            'uuid': self.uuid,
+            'project_id': self.action_stop['project_id'],
+            'event_host': r'\w+',
+            'event_hostId': '[a-f0-9]+'
+        }
+
+
+class ServerActionsV262NonAdminSampleJsonTest(ServerActionsV262SampleJsonTest):
+    ADMIN_API = False
diff --git a/nova/tests/unit/api/openstack/compute/test_instance_actions.py b/nova/tests/unit/api/openstack/compute/test_instance_actions.py
index 861baeb682e0..5b70f99787c6 100644
--- a/nova/tests/unit/api/openstack/compute/test_instance_actions.py
+++ b/nova/tests/unit/api/openstack/compute/test_instance_actions.py
@@ -38,7 +38,8 @@ FAKE_EVENT_ID = fake_server_actions.FAKE_ACTION_ID1
 FAKE_REQUEST_NOTFOUND_ID = 'req-' + uuids.req_not_found
 
 
-def format_action(action, expect_traceback=True):
+def format_action(action, expect_traceback=True, expect_host=False,
+                  expect_hostId=False):
     '''Remove keys that aren't serialized.'''
     to_delete = ('id', 'finish_time', 'created_at', 'updated_at', 'deleted_at',
                  'deleted')
@@ -49,11 +50,14 @@ def format_action(action, expect_traceback=True):
         # NOTE(danms): Without WSGI above us, these will be just stringified
         action['start_time'] = str(action['start_time'].replace(tzinfo=None))
     for event in action.get('events', []):
-        format_event(event, expect_traceback)
+        format_event(event, action.get('project_id'),
+                     expect_traceback=expect_traceback,
+                     expect_host=expect_host, expect_hostId=expect_hostId)
     return action
 
 
-def format_event(event, expect_traceback=True, expect_host=False):
+def format_event(event, project_id, expect_traceback=True, expect_host=False,
+                 expect_hostId=False):
     '''Remove keys that aren't serialized.'''
     to_delete = ['id', 'created_at', 'updated_at', 'deleted_at', 'deleted',
                  'action_id']
@@ -61,6 +65,8 @@ def format_event(event, expect_traceback=True, expect_host=False):
         to_delete.append('traceback')
     if not expect_host:
         to_delete.append('host')
+    if not expect_hostId:
+        to_delete.append('hostId')
     for key in to_delete:
         if key in event:
             del(event[key])
@@ -116,6 +122,8 @@ class InstanceActionsTestV21(test.NoDBTestCase):
     instance_actions = instance_actions_v21
     wsgi_api_version = os_wsgi.DEFAULT_API_VERSION
     expect_events_non_admin = False
+    expect_event_hostId = False
+    expect_event_host = False
 
     def fake_get(self, context, instance_uuid, expected_attrs=None):
         return objects.Instance(uuid=instance_uuid)
@@ -181,8 +189,12 @@ class InstanceActionsTestV21(test.NoDBTestCase):
         fake_action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID]
         fake_events = self.fake_events[fake_action['id']]
         fake_action['events'] = fake_events
-        self.assertEqual(format_action(fake_action),
-                         format_action(res_dict['instanceAction']))
+        self.assertEqual(format_action(fake_action,
+                                       expect_host=self.expect_event_host,
+                                       expect_hostId=self.expect_event_hostId),
+                         format_action(res_dict['instanceAction'],
+                                       expect_host=self.expect_event_host,
+                                       expect_hostId=self.expect_event_hostId))
 
     def test_get_action_with_events_not_allowed(self):
         def fake_get_action(context, uuid, request_id):
@@ -201,10 +213,16 @@ class InstanceActionsTestV21(test.NoDBTestCase):
         if self.expect_events_non_admin:
             fake_event = fake_server_actions.FAKE_EVENTS[FAKE_EVENT_ID]
             fake_action['events'] = copy.deepcopy(fake_event)
-        # By default, non-admins are not allowed to see traceback details.
-        self.assertEqual(format_action(fake_action, expect_traceback=False),
+        # By default, non-admins are not allowed to see traceback details
+        # and event host.
+        self.assertEqual(format_action(fake_action,
+                                       expect_traceback=False,
+                                       expect_host=False,
+                                       expect_hostId=self.expect_event_hostId),
                          format_action(res_dict['instanceAction'],
-                                       expect_traceback=False))
+                                       expect_traceback=False,
+                                       expect_host=False,
+                                       expect_hostId=self.expect_event_hostId))
 
     def test_action_not_found(self):
         def fake_no_action(context, uuid, action_id):
@@ -294,3 +312,9 @@ class InstanceActionsTestV258(InstanceActionsTestV251):
                                self.controller.index, req)
         self.assertIn('Invalid input for query parameters marker',
                       six.text_type(ex))
+
+
+class InstanceActionsTestV262(InstanceActionsTestV251):
+    wsgi_api_version = "2.62"
+    expect_event_hostId = True
+    expect_event_host = True
diff --git a/nova/tests/unit/fake_server_actions.py b/nova/tests/unit/fake_server_actions.py
index e3ddee228941..5193f32c1b0a 100644
--- a/nova/tests/unit/fake_server_actions.py
+++ b/nova/tests/unit/fake_server_actions.py
@@ -20,6 +20,8 @@ FAKE_REQUEST_ID1 = 'req-3293a3f1-b44c-4609-b8d2-d81b105636b8'
 FAKE_REQUEST_ID2 = 'req-25517360-b757-47d3-be45-0e8d2a01b36a'
 FAKE_ACTION_ID1 = 123
 FAKE_ACTION_ID2 = 456
+FAKE_HOST_ID1 = '74824069503a752aaa3abf194f73200fcdd117ef70ab28b576e5bf7a'
+FAKE_HOST_ID2 = '858f5ed465b4967dd1306a38078e9b83b8705bdedfa7f16f898119b4'
 
 FAKE_ACTIONS = {
     FAKE_UUID: {
@@ -70,7 +72,8 @@ FAKE_EVENTS = {
                        'updated_at': None,
                        'deleted_at': None,
                        'deleted': False,
-                       'host': 'host1'
+                       'host': 'host1',
+                       'hostId': FAKE_HOST_ID1
                       },
                       {'id': 2,
                        'action_id': FAKE_ACTION_ID1,
@@ -85,7 +88,8 @@ FAKE_EVENTS = {
                        'updated_at': None,
                        'deleted_at': None,
                        'deleted': False,
-                       'host': 'host1'
+                       'host': 'host1',
+                       'hostId': FAKE_HOST_ID1
                        }
     ],
     FAKE_ACTION_ID2: [{'id': 3,
@@ -101,7 +105,8 @@ FAKE_EVENTS = {
                        'updated_at': None,
                        'deleted_at': None,
                        'deleted': False,
-                       'host': 'host2'
+                       'host': 'host2',
+                       'hostId': FAKE_HOST_ID2
                        }
    ]
 }
diff --git a/releasenotes/notes/add-host-to-instance-action-events-aad2cc18fe191afa.yaml b/releasenotes/notes/add-host-to-instance-action-events-aad2cc18fe191afa.yaml
new file mode 100644
index 000000000000..037abcf03367
--- /dev/null
+++ b/releasenotes/notes/add-host-to-instance-action-events-aad2cc18fe191afa.yaml
@@ -0,0 +1,10 @@
+---
+features:
+  - |
+    The microversion 2.62 adds ``host`` (hostname) and ``hostId`` (an
+    obfuscated hashed host id string) fields to the instance action
+    ``GET /servers/{server_id}/os-instance-actions/{req_id}`` API. The display
+    of the newly added ``host`` field will be controlled via policy rule
+    ``os_compute_api:os-instance-actions:events``, which is the same policy
+    used for the ``events.traceback`` field. If the user is prevented by
+    policy, only ``hostId`` will be displayed.