Merge "Plumbing required in servers ViewBuilder to construct partial results"
This commit is contained in:
commit
a2d3ca91c3
|
@ -270,12 +270,15 @@ class ServersController(wsgi.Controller):
|
|||
if is_detail:
|
||||
instance_list._context = context
|
||||
instance_list.fill_faults()
|
||||
response = self._view_builder.detail(req, instance_list)
|
||||
response = self._view_builder.detail(
|
||||
req, instance_list, cell_down_support=cell_down_support)
|
||||
else:
|
||||
response = self._view_builder.index(req, instance_list)
|
||||
response = self._view_builder.index(
|
||||
req, instance_list, cell_down_support=cell_down_support)
|
||||
return response
|
||||
|
||||
def _get_server(self, context, req, instance_uuid, is_detail=False):
|
||||
def _get_server(self, context, req, instance_uuid, is_detail=False,
|
||||
cell_down_support=False):
|
||||
"""Utility function for looking up an instance by uuid.
|
||||
|
||||
:param context: request context for auth
|
||||
|
@ -283,6 +286,10 @@ class ServersController(wsgi.Controller):
|
|||
:param instance_uuid: UUID of the server instance to get
|
||||
:param is_detail: True if you plan on showing the details of the
|
||||
instance in the response, False otherwise.
|
||||
:param cell_down_support: True if the API (and caller) support
|
||||
returning a minimal instance
|
||||
construct if the relevant cell is
|
||||
down.
|
||||
"""
|
||||
expected_attrs = ['flavor', 'numa_topology']
|
||||
if is_detail:
|
||||
|
@ -295,7 +302,7 @@ class ServersController(wsgi.Controller):
|
|||
instance = common.get_instance(self.compute_api, context,
|
||||
instance_uuid,
|
||||
expected_attrs=expected_attrs,
|
||||
cell_down_support=False)
|
||||
cell_down_support=cell_down_support)
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
|
@ -389,8 +396,12 @@ class ServersController(wsgi.Controller):
|
|||
"""Returns server details by server id."""
|
||||
context = req.environ['nova.context']
|
||||
context.can(server_policies.SERVERS % 'show')
|
||||
instance = self._get_server(context, req, id, is_detail=True)
|
||||
return self._view_builder.show(req, instance)
|
||||
# TODO(tssurya): enable cell_down_support after bumping the
|
||||
# microversion.
|
||||
instance = self._get_server(
|
||||
context, req, id, is_detail=True, cell_down_support=False)
|
||||
return self._view_builder.show(
|
||||
req, instance, cell_down_support=False)
|
||||
|
||||
@wsgi.response(202)
|
||||
@wsgi.expected_errors((400, 403, 409))
|
||||
|
|
|
@ -93,8 +93,23 @@ class ViewBuilder(common.ViewBuilder):
|
|||
|
||||
def basic(self, request, instance, show_extra_specs=False,
|
||||
show_extended_attr=None, show_host_status=None,
|
||||
show_sec_grp=None, bdms=None):
|
||||
show_sec_grp=None, bdms=None, cell_down_support=False):
|
||||
"""Generic, non-detailed view of an instance."""
|
||||
if cell_down_support and 'display_name' not in instance:
|
||||
# NOTE(tssurya): If the microversion is >= 2.69, this boolean will
|
||||
# be true in which case we check if there are instances from down
|
||||
# cells (by checking if their objects have missing keys like
|
||||
# `display_name`) and return partial constructs based on the
|
||||
# information available from the nova_api database.
|
||||
return {
|
||||
"server": {
|
||||
"id": instance.uuid,
|
||||
"status": "UNKNOWN",
|
||||
"links": self._get_links(request,
|
||||
instance.uuid,
|
||||
self._collection_name),
|
||||
},
|
||||
}
|
||||
return {
|
||||
"server": {
|
||||
"id": instance["uuid"],
|
||||
|
@ -125,16 +140,51 @@ class ViewBuilder(common.ViewBuilder):
|
|||
# results.
|
||||
return sorted(list(set(self._show_expected_attrs + expected_attrs)))
|
||||
|
||||
def _show_from_down_cell(self, request, instance, show_extra_specs):
|
||||
"""Function that constructs the partial response for the instance."""
|
||||
ret = {
|
||||
"server": {
|
||||
"id": instance.uuid,
|
||||
"status": "UNKNOWN",
|
||||
"tenant_id": instance.project_id,
|
||||
"created": utils.isotime(instance.created_at),
|
||||
},
|
||||
}
|
||||
if 'flavor' in instance:
|
||||
# If the key 'flavor' is present for an instance from a down cell
|
||||
# it means that the request is ``GET /servers/{server_id}`` and
|
||||
# thus we include the information from the request_spec of the
|
||||
# instance like its flavor, image, avz, and user_id in addition to
|
||||
# the basic information from its instance_mapping.
|
||||
# If 'flavor' key is not present for an instance from a down cell
|
||||
# down cell it means the request is ``GET /servers/detail`` and we
|
||||
# do not expose the flavor in the response when listing servers
|
||||
# with details for performance reasons of fetching it from the
|
||||
# request specs table for the whole list of instances.
|
||||
ret["server"]["image"] = self._get_image(request, instance)
|
||||
ret["server"]["flavor"] = self._get_flavor(request, instance,
|
||||
show_extra_specs)
|
||||
# in case availability zone was not requested by the user during
|
||||
# boot time, return UNKNOWN.
|
||||
avz = instance.availability_zone or "UNKNOWN"
|
||||
ret["server"]["OS-EXT-AZ:availability_zone"] = avz
|
||||
ret["server"]["OS-EXT-STS:power_state"] = instance.power_state
|
||||
# in case its an old request spec which doesn't have the user_id
|
||||
# data migrated, return UNKNOWN.
|
||||
ret["server"]["user_id"] = instance.user_id or "UNKNOWN"
|
||||
else:
|
||||
# GET /servers/detail includes links for GET /servers/{server_id}.
|
||||
ret['server']["links"] = self._get_links(
|
||||
request, instance.uuid, self._collection_name)
|
||||
return ret
|
||||
|
||||
def show(self, request, instance, extend_address=True,
|
||||
show_extra_specs=None, show_AZ=True, show_config_drive=True,
|
||||
show_extended_attr=None, show_host_status=None,
|
||||
show_keypair=True, show_srv_usg=True, show_sec_grp=True,
|
||||
show_extended_status=True, show_extended_volumes=True,
|
||||
bdms=None):
|
||||
bdms=None, cell_down_support=False):
|
||||
"""Detailed view of a single instance."""
|
||||
ip_v4 = instance.get('access_ip_v4')
|
||||
ip_v6 = instance.get('access_ip_v6')
|
||||
|
||||
if show_extra_specs is None:
|
||||
# detail will pre-calculate this for us. If we're doing show,
|
||||
# then figure it out here.
|
||||
|
@ -144,6 +194,17 @@ class ViewBuilder(common.ViewBuilder):
|
|||
show_extra_specs = context.can(
|
||||
fes_policies.POLICY_ROOT % 'index', fatal=False)
|
||||
|
||||
if cell_down_support and 'display_name' not in instance:
|
||||
# NOTE(tssurya): If the microversion is >= 2.69, this boolean will
|
||||
# be true in which case we check if there are instances from down
|
||||
# cells (by checking if their objects have missing keys like
|
||||
# `display_name`) and return partial constructs based on the
|
||||
# information available from the nova_api database.
|
||||
return self._show_from_down_cell(
|
||||
request, instance, show_extra_specs)
|
||||
ip_v4 = instance.get('access_ip_v4')
|
||||
ip_v6 = instance.get('access_ip_v6')
|
||||
|
||||
server = {
|
||||
"server": {
|
||||
"id": instance["uuid"],
|
||||
|
@ -280,13 +341,13 @@ class ViewBuilder(common.ViewBuilder):
|
|||
|
||||
return server
|
||||
|
||||
def index(self, request, instances):
|
||||
def index(self, request, instances, cell_down_support=False):
|
||||
"""Show a list of servers without many details."""
|
||||
coll_name = self._collection_name
|
||||
return self._list_view(self.basic, request, instances, coll_name,
|
||||
False)
|
||||
False, cell_down_support=cell_down_support)
|
||||
|
||||
def detail(self, request, instances):
|
||||
def detail(self, request, instances, cell_down_support=False):
|
||||
"""Detailed view of a list of instance."""
|
||||
coll_name = self._collection_name + '/detail'
|
||||
context = request.environ['nova.context']
|
||||
|
@ -318,7 +379,8 @@ class ViewBuilder(common.ViewBuilder):
|
|||
show_extended_attr=show_extended_attr,
|
||||
show_host_status=show_host_status,
|
||||
show_sec_grp=False,
|
||||
bdms=bdms)
|
||||
bdms=bdms,
|
||||
cell_down_support=cell_down_support)
|
||||
|
||||
self._add_security_grps(request, list(servers_dict["servers"]),
|
||||
instances)
|
||||
|
@ -326,7 +388,7 @@ class ViewBuilder(common.ViewBuilder):
|
|||
|
||||
def _list_view(self, func, request, servers, coll_name, show_extra_specs,
|
||||
show_extended_attr=None, show_host_status=None,
|
||||
show_sec_grp=False, bdms=None):
|
||||
show_sec_grp=False, bdms=None, cell_down_support=False):
|
||||
"""Provide a view for a list of servers.
|
||||
|
||||
:param func: Function used to format the server data
|
||||
|
@ -341,13 +403,18 @@ class ViewBuilder(common.ViewBuilder):
|
|||
:param show_sec_grp: If the security group should be included in
|
||||
the response dict.
|
||||
:param bdms: Instances bdms info from multiple cells.
|
||||
:param cell_down_support: True if the API (and caller) support
|
||||
returning a minimal instance
|
||||
construct if the relevant cell is
|
||||
down.
|
||||
:returns: Server data in dictionary format
|
||||
"""
|
||||
server_list = [func(request, server,
|
||||
show_extra_specs=show_extra_specs,
|
||||
show_extended_attr=show_extended_attr,
|
||||
show_host_status=show_host_status,
|
||||
show_sec_grp=show_sec_grp, bdms=bdms)["server"]
|
||||
show_sec_grp=show_sec_grp, bdms=bdms,
|
||||
cell_down_support=cell_down_support)["server"]
|
||||
for server in servers]
|
||||
servers_links = self._get_collection_links(request,
|
||||
servers,
|
||||
|
|
|
@ -7339,6 +7339,307 @@ class ServersViewBuilderTest(test.TestCase):
|
|||
self.assertThat(output, matchers.DictMatches(expected_server))
|
||||
|
||||
|
||||
class ServersViewBuilderTestV269(ServersViewBuilderTest):
|
||||
"""Server ViewBuilder test for microversion 2.69
|
||||
|
||||
The intent here is simply to verify that when showing server details
|
||||
after microversion 2.69 the response could have missing keys for those
|
||||
servers from the down cells.
|
||||
"""
|
||||
wsgi_api_version = '2.69'
|
||||
|
||||
def setUp(self):
|
||||
super(ServersViewBuilderTestV269, self).setUp()
|
||||
self.view_builder = views.servers.ViewBuilder()
|
||||
self.ctxt = context.RequestContext('fake', 'fake')
|
||||
|
||||
def fake_is_supported(req, min_version="2.1", max_version="2.69"):
|
||||
return (fakes.api_version.APIVersionRequest(max_version) >=
|
||||
req.api_version_request >=
|
||||
fakes.api_version.APIVersionRequest(min_version))
|
||||
self.stub_out('nova.api.openstack.api_version_request.is_supported',
|
||||
fake_is_supported)
|
||||
|
||||
def req(self, url, use_admin_context=False):
|
||||
return fakes.HTTPRequest.blank(url,
|
||||
use_admin_context=use_admin_context,
|
||||
version=self.wsgi_api_version)
|
||||
|
||||
def test_get_server_list_detail_with_down_cells(self):
|
||||
# Fake out 1 partially constructued instance and one full instance.
|
||||
self.instances = [
|
||||
self.instance,
|
||||
objects.Instance(
|
||||
context=self.ctxt,
|
||||
uuid=uuids.fake1,
|
||||
project_id='fake',
|
||||
created_at=datetime.datetime(1955, 11, 5)
|
||||
)
|
||||
]
|
||||
|
||||
req = self.req('/fake/servers/detail')
|
||||
output = self.view_builder.detail(req, self.instances, True)
|
||||
|
||||
self.assertEqual(2, len(output['servers']))
|
||||
image_bookmark = "http://localhost/fake/images/5"
|
||||
expected = {
|
||||
"servers": [{
|
||||
"id": self.uuid,
|
||||
"user_id": "fake_user",
|
||||
"tenant_id": "fake_project",
|
||||
"updated": "2010-11-11T11:00:00Z",
|
||||
"created": "2010-10-10T12:00:00Z",
|
||||
"progress": 0,
|
||||
"name": "test_server",
|
||||
"status": "ACTIVE",
|
||||
"hostId": '',
|
||||
"image": {
|
||||
"id": "5",
|
||||
"links": [
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": image_bookmark,
|
||||
},
|
||||
],
|
||||
},
|
||||
"flavor": {
|
||||
'disk': 1,
|
||||
'ephemeral': 1,
|
||||
'vcpus': 1,
|
||||
'ram': 256,
|
||||
'original_name': 'flavor1',
|
||||
'extra_specs': {},
|
||||
'swap': 0
|
||||
},
|
||||
"addresses": {
|
||||
'test1': [
|
||||
{'version': 4, 'addr': '192.168.1.100',
|
||||
'OS-EXT-IPS:type': 'fixed',
|
||||
'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
|
||||
{'version': 6, 'addr': '2001:db8:0:1::1',
|
||||
'OS-EXT-IPS:type': 'fixed',
|
||||
'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'},
|
||||
{'version': 4, 'addr': '192.168.2.100',
|
||||
'OS-EXT-IPS:type': 'fixed',
|
||||
'OS-EXT-IPS-MAC:mac_addr': 'bb:bb:bb:bb:bb:bb'}
|
||||
],
|
||||
'test2': [
|
||||
{'version': 4, 'addr': '192.168.3.100',
|
||||
'OS-EXT-IPS:type': 'fixed',
|
||||
'OS-EXT-IPS-MAC:mac_addr': 'cc:cc:cc:cc:cc:cc'},
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"tags": [],
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": self.self_link,
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": self.bookmark_link,
|
||||
},
|
||||
],
|
||||
"OS-DCF:diskConfig": "MANUAL",
|
||||
"OS-EXT-SRV-ATTR:root_device_name": None,
|
||||
"accessIPv4": '',
|
||||
"accessIPv6": '',
|
||||
"host_status": '',
|
||||
"OS-EXT-SRV-ATTR:user_data": None,
|
||||
"trusted_image_certificates": None,
|
||||
"OS-EXT-AZ:availability_zone": "nova",
|
||||
"OS-EXT-SRV-ATTR:kernel_id": '',
|
||||
"OS-EXT-SRV-ATTR:reservation_id": '',
|
||||
"config_drive": None,
|
||||
"OS-EXT-SRV-ATTR:host": None,
|
||||
"OS-EXT-SRV-ATTR:hypervisor_hostname": None,
|
||||
"OS-EXT-SRV-ATTR:hostname": 'test_server',
|
||||
"OS-EXT-SRV-ATTR:instance_name": "instance-00000001",
|
||||
"key_name": '',
|
||||
"locked": False,
|
||||
"description": None,
|
||||
"OS-SRV-USG:launched_at": None,
|
||||
"OS-SRV-USG:terminated_at": None,
|
||||
"security_groups": [{'name': 'fake-0-0'},
|
||||
{'name': 'fake-0-1'}],
|
||||
"OS-EXT-STS:task_state": None,
|
||||
"OS-EXT-STS:vm_state": vm_states.ACTIVE,
|
||||
"OS-EXT-STS:power_state": 1,
|
||||
"OS-EXT-SRV-ATTR:launch_index": 0,
|
||||
"OS-EXT-SRV-ATTR:ramdisk_id": '',
|
||||
"os-extended-volumes:volumes_attached": [
|
||||
{'id': 'some_volume_1', 'delete_on_termination': True},
|
||||
{'id': 'some_volume_2', 'delete_on_termination': False},
|
||||
]
|
||||
},
|
||||
{
|
||||
'created': '1955-11-05T00:00:00Z',
|
||||
'id': uuids.fake1,
|
||||
'tenant_id': 'fake',
|
||||
"status": "UNKNOWN",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v2/fake/servers/%s" %
|
||||
uuids.fake1,
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/fake/servers/%s" %
|
||||
uuids.fake1,
|
||||
},
|
||||
],
|
||||
}]
|
||||
}
|
||||
self.assertThat(output, matchers.DictMatches(expected))
|
||||
|
||||
def test_get_server_list_with_down_cells(self):
|
||||
# Fake out 1 partially constructued instance and one full instance.
|
||||
self.instances = [
|
||||
self.instance,
|
||||
objects.Instance(
|
||||
context=self.ctxt,
|
||||
uuid=uuids.fake1,
|
||||
project_id='fake',
|
||||
created_at=datetime.datetime(1955, 11, 5)
|
||||
)
|
||||
]
|
||||
|
||||
req = self.req('/fake/servers')
|
||||
output = self.view_builder.index(req, self.instances, True)
|
||||
|
||||
self.assertEqual(2, len(output['servers']))
|
||||
|
||||
expected = {
|
||||
"servers": [{
|
||||
"id": self.uuid,
|
||||
"name": "test_server",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": self.self_link,
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": self.bookmark_link,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
'id': uuids.fake1,
|
||||
"status": "UNKNOWN",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v2/fake/servers/%s" %
|
||||
uuids.fake1,
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/fake/servers/%s" %
|
||||
uuids.fake1,
|
||||
},
|
||||
],
|
||||
}]
|
||||
}
|
||||
|
||||
self.assertThat(output, matchers.DictMatches(expected))
|
||||
|
||||
def test_get_server_with_down_cells(self):
|
||||
# Fake out 1 partially constructued instance.
|
||||
self.instance = objects.Instance(
|
||||
context=self.ctxt,
|
||||
uuid=self.uuid,
|
||||
project_id=self.instance.project_id,
|
||||
created_at=datetime.datetime(1955, 11, 5),
|
||||
user_id=self.instance.user_id,
|
||||
image_ref=self.instance.image_ref,
|
||||
power_state=0,
|
||||
flavor=self.instance.flavor,
|
||||
availability_zone=self.instance.availability_zone
|
||||
)
|
||||
|
||||
req = self.req('/fake/servers/%s' % FAKE_UUID)
|
||||
output = self.view_builder.show(req, self.instance,
|
||||
cell_down_support=True)
|
||||
# nine fields from request_spec and instance_mapping
|
||||
self.assertEqual(9, len(output['server']))
|
||||
image_bookmark = "http://localhost/fake/images/5"
|
||||
expected = {
|
||||
"server": {
|
||||
"id": self.uuid,
|
||||
"user_id": "fake_user",
|
||||
"tenant_id": "fake_project",
|
||||
"created": '1955-11-05T00:00:00Z',
|
||||
"status": "UNKNOWN",
|
||||
"image": {
|
||||
"id": "5",
|
||||
"links": [
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": image_bookmark,
|
||||
},
|
||||
],
|
||||
},
|
||||
"flavor": {
|
||||
'disk': 1,
|
||||
'ephemeral': 1,
|
||||
'vcpus': 1,
|
||||
'ram': 256,
|
||||
'original_name': 'flavor1',
|
||||
'extra_specs': {},
|
||||
'swap': 0
|
||||
},
|
||||
"OS-EXT-AZ:availability_zone": "nova",
|
||||
"OS-EXT-STS:power_state": 0
|
||||
}
|
||||
}
|
||||
self.assertThat(output, matchers.DictMatches(expected))
|
||||
|
||||
def test_get_server_without_image_avz_user_id_set_from_down_cells(self):
|
||||
# Fake out 1 partially constructued instance.
|
||||
self.instance = objects.Instance(
|
||||
context=self.ctxt,
|
||||
uuid=self.uuid,
|
||||
project_id=self.instance.project_id,
|
||||
created_at=datetime.datetime(1955, 11, 5),
|
||||
user_id=None,
|
||||
image_ref=None,
|
||||
power_state=0,
|
||||
flavor=self.instance.flavor,
|
||||
availability_zone=None
|
||||
)
|
||||
|
||||
req = self.req('/fake/servers/%s' % FAKE_UUID)
|
||||
output = self.view_builder.show(req, self.instance,
|
||||
cell_down_support=True)
|
||||
# nine fields from request_spec and instance_mapping
|
||||
self.assertEqual(9, len(output['server']))
|
||||
expected = {
|
||||
"server": {
|
||||
"id": self.uuid,
|
||||
"user_id": "UNKNOWN",
|
||||
"tenant_id": "fake_project",
|
||||
"created": '1955-11-05T00:00:00Z',
|
||||
"status": "UNKNOWN",
|
||||
"image": "",
|
||||
"flavor": {
|
||||
'disk': 1,
|
||||
'ephemeral': 1,
|
||||
'vcpus': 1,
|
||||
'ram': 256,
|
||||
'original_name': 'flavor1',
|
||||
'extra_specs': {},
|
||||
'swap': 0
|
||||
},
|
||||
"OS-EXT-AZ:availability_zone": "UNKNOWN",
|
||||
"OS-EXT-STS:power_state": 0
|
||||
}
|
||||
}
|
||||
self.assertThat(output, matchers.DictMatches(expected))
|
||||
|
||||
|
||||
class ServersAllExtensionsTestCase(test.TestCase):
|
||||
"""Servers tests using default API router with all extensions enabled.
|
||||
|
||||
|
|
Loading…
Reference in New Issue