Add microversion support for nova plugins

When nova adds new fields to nova resources,
new API microversions will be added, we should
add the ability to set microversions when we
call nova client to make the most use of the
newly added fields.

Implements: blueprint support-microversion-for-nova
Closes-bug: #1585522

Change-Id: I277475615dada98b965b7ff4ff02c12054df3747
This commit is contained in:
Kevin_Zheng 2016-05-28 11:22:34 +08:00
parent 33066df7ca
commit 21042fd7f4
12 changed files with 161 additions and 42 deletions

View File

@ -177,6 +177,9 @@ driver = messaging
#notifications_topic = notifications
#resource_group_name = searchlight
[service_credentials:nova]
compute_api_version = 2.1
[resource_plugin:os_nova_server]
enabled = True
#admin_only_fields = OS-EXT-STS:vm_state

View File

@ -75,6 +75,9 @@ Please read the rest of the guide for detailed information.::
[resource_plugin]
resource_group_name = searchlight
[service_credentials:nova]
compute_api_version = 2.1
[resource_plugin:os_nova_server]
enabled = True
admin_only_fields = OS-EXT-SRV*,OS-EXT-STS:vm_state

View File

@ -39,6 +39,19 @@ general configuration information, and an example complete configuration.
searchlight.conf
----------------
Nova microversions
^^^^^^^^^^^^^^^^^^
::
[service_credentials:nova]
compute_api_version = 2.1
.. note::
Nova adds/removes fields using microversion mechanism, check
http://git.openstack.org/cgit/openstack/nova/tree/nova/api/openstack/rest_api_version_history.rst
for detailed Nova microversion history.
Plugin: OS::Nova::Server
^^^^^^^^^^^^^^^^^^^^^^^^
::

View File

@ -0,0 +1,6 @@
---
features:
- Added microversion support for Nova plugins, the default
API version for Nova plugins is 2.1. User can change the
API microversion for Nova using compute_api_version config
option added in section service_credentials:nova.

View File

@ -121,3 +121,9 @@ class InvalidJsonPatchPath(JsonPatchException):
class IndexingException(SearchlightException):
message = _("An error occurred during index creation or initial loading")
class InvalidAPIVersionProvided(SearchlightException):
message = _("The provided API version is not supported, "
"the current available version range for %(service)s "
"is: from %(min_version)s to %(max_version)s.")

View File

@ -240,8 +240,7 @@ class IndexBase(plugin.Plugin):
# See https://www.elastic.co/guide/en/elasticsearch/
# reference/2.1/mapping-meta-field.html
if facet_name in meta_mapping:
facet['resource_type'] = \
meta_mapping[facet_name]['resource_type']
facet.update(meta_mapping[facet_name])
if (self.get_parent_id_field() and
name == self.get_parent_id_field()):

View File

@ -29,7 +29,7 @@ class ServerIndex(base.IndexBase):
NotificationHandlerCls = notification_handler.InstanceHandler
# Will be combined with 'admin_only_fields' from config
ADMIN_ONLY_FIELDS = ['OS-EXT-SRV-ATTR:*']
ADMIN_ONLY_FIELDS = ['OS-EXT-SRV-ATTR:*', 'host_status']
@classmethod
def get_document_type(self):
@ -95,6 +95,17 @@ class ServerIndex(base.IndexBase):
# maintains compatibility with both
'security_groups': {'type': 'string', 'index': 'not_analyzed'},
'status': {'type': 'string', 'index': 'not_analyzed'},
# Nova adds/removes fields using microversion mechanism, check
# http://git.openstack.org/cgit/openstack/nova/tree/nova/api/openstack/rest_api_version_history.rst
# for detailed Nova microversion history.
# Added in microversion 2.9
'locked': {'type': 'string', 'index': 'not_analyzed'},
# Added in microversion 2.16
'host_status': {'type': 'string', 'index': 'not_analyzed'},
# Added in microversion 2.19
'description': {'type': 'string'},
# Added in microversion 2.26
'tags': {'type': 'string'},
},
"_meta": {
"image.id": {
@ -117,6 +128,18 @@ class ServerIndex(base.IndexBase):
},
"security_groups": {
"resource_type": resource_types.NOVA_SECURITY_GROUP
},
"locked": {
"min_version": "2.9"
},
"host_status": {
"min_version": "2.16"
},
"description": {
"min_version": "2.19"
},
"tags": {
"min_version": "2.26"
}
},
}
@ -131,7 +154,7 @@ class ServerIndex(base.IndexBase):
return ('OS-EXT-AZ:availability_zone',
'status', 'image.id', 'flavor.id', 'networks.name',
'networks.OS-EXT-IPS:type', 'networks.version',
'security_groups')
'security_groups', 'host_status', 'locked')
@property
def facets_excluded(self):
@ -139,7 +162,7 @@ class ServerIndex(base.IndexBase):
fields should not be offered as facet options, or those that should
only be available to administrators.
"""
return {'tenant_id': True, 'project_id': True,
return {'tenant_id': True, 'project_id': True, 'host_status': True,
'created': False, 'updated': False}
def _get_rbac_field_filters(self, request_context):

View File

@ -12,6 +12,7 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from distutils.version import LooseVersion
import os
from cinderclient import client as cinder_client
@ -21,10 +22,11 @@ from keystoneclient import auth as ks_auth
from keystoneclient import session as ks_session
import keystoneclient.v2_0.client as ks_client
import neutronclient.v2_0.client as neutron_client
from novaclient import api_versions
from novaclient import client as nova_client
import swiftclient
from searchlight.common import exception
from oslo_config import cfg
@ -38,10 +40,20 @@ client_opts = [
'use for communication with OpenStack services.'),
]
compute_api_version = cfg.StrOpt(
'compute_api_version',
default='2.1',
help='The compute API (micro)version, the provided '
'compute API (micro)version should be not smaller '
'than 2.1 and not larger than the max supported '
'Compute API microversion. The current supported '
'Compute API versions can be checked using: '
'nova version-list.')
GROUP = "service_credentials"
cfg.CONF.register_opts(client_opts, group=GROUP)
cfg.CONF.register_opt(compute_api_version, group="service_credentials:nova")
ks_session.Session.register_conf_options(cfg.CONF, GROUP)
@ -49,6 +61,8 @@ ks_auth.register_conf_options(cfg.CONF, GROUP)
_session = None
NOVA_MIN_API_VERSION = '2.1'
def _get_session():
global _session
@ -73,14 +87,26 @@ def get_glanceclient():
def get_novaclient():
session = _get_session()
return nova_client.Client(
version=api_versions.APIVersion('2.1'),
session=session,
region_name=cfg.CONF.service_credentials.os_region_name,
endpoint_type=cfg.CONF.service_credentials.os_endpoint_type
)
def do_get_client(api_version=2.1):
session = _get_session()
return nova_client.Client(
version=api_version,
session=session,
region_name=cfg.CONF.service_credentials.os_region_name,
endpoint_type=cfg.CONF.service_credentials.os_endpoint_type
)
version = cfg.CONF["service_credentials:nova"].compute_api_version
# Check whether Nova can support the provided microversion.
max_version = do_get_client().versions.list()[-1].version
if LooseVersion(version) > LooseVersion(max_version) or \
LooseVersion(version) < LooseVersion(NOVA_MIN_API_VERSION):
raise exception.InvalidAPIVersionProvided(
service='compute service', min_version=NOVA_MIN_API_VERSION,
max_version=max_version)
return do_get_client(version)
def get_designateclient():

View File

@ -14,6 +14,7 @@
# limitations under the License.
import json
import mock
import six
import time
import uuid
@ -28,6 +29,11 @@ TENANT3 = str(uuid.uuid4())
USER1 = str(uuid.uuid4())
fake_version_list = [test_utils.FakeVersion('2.1'),
test_utils.FakeVersion('2.1')]
nova_version_getter = 'novaclient.v2.client.versions.VersionManager.list'
MATCH_ALL = {"query": {"match_all": {}}, "sort": [{"name": {"order": "asc"}}]}
EMPTY_RESPONSE = {"hits": {"hits": [], "total": 0, "max_score": 0.0},
"_shards": {"successful": 0, "failed": 0, "total": 0},
@ -231,10 +237,12 @@ class TestSearchApi(functional.FunctionalTest):
u'updated_at': u'2016-04-07T15:51:35Z',
u'user_id': u'27f4d76b-be62-4e4e-aa33bb11cc55'
}
self._index(
servers_plugin,
[test_utils.DictObj(**server1), test_utils.DictObj(**server2),
test_utils.DictObj(**server3)])
with mock.patch(nova_version_getter, return_value=fake_version_list):
self._index(
servers_plugin,
[test_utils.DictObj(**server1),
test_utils.DictObj(**server2),
test_utils.DictObj(**server3)])
response, json_content = self._facet_request(
TENANT1,
@ -308,9 +316,11 @@ class TestSearchApi(functional.FunctionalTest):
u'user_id': u'27f4d76b-be62-4e4e-aa33bb11cc55'
}
self._index(
servers_plugin,
[test_utils.DictObj(**server1), test_utils.DictObj(**server2)])
with mock.patch(nova_version_getter, return_value=fake_version_list):
self._index(
servers_plugin,
[test_utils.DictObj(**server1),
test_utils.DictObj(**server2)])
response, json_content = self._facet_request(
TENANT1,
@ -360,9 +370,10 @@ class TestSearchApi(functional.FunctionalTest):
}
servers_plugin = self.initialized_plugins['OS::Nova::Server']
self._index(
servers_plugin,
[test_utils.DictObj(**s1)])
with mock.patch(nova_version_getter, return_value=fake_version_list):
self._index(
servers_plugin,
[test_utils.DictObj(**s1)])
response, json_content = self._search_request(MATCH_ALL,
TENANT1,
@ -413,9 +424,10 @@ class TestSearchApi(functional.FunctionalTest):
}
servers_plugin = self.initialized_plugins['OS::Nova::Server']
self._index(
servers_plugin,
[test_utils.DictObj(**s1)])
with mock.patch(nova_version_getter, return_value=fake_version_list):
self._index(
servers_plugin,
[test_utils.DictObj(**s1)])
# For each of these queries (which are really looking for the same
# thing) we expect a result for an admin, and no result for a user
@ -484,7 +496,8 @@ class TestSearchApi(functional.FunctionalTest):
"created_at": "2016-04-06T12:48:18Z"
}
self._index(servers_plugin, [test_utils.DictObj(**server_doc)])
with mock.patch(nova_version_getter, return_value=fake_version_list):
self._index(servers_plugin, [test_utils.DictObj(**server_doc)])
self._index(images_plugin, [image_doc])
# Modify the policy file to disallow some things

View File

@ -11,6 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from searchlight.tests import functional
from searchlight.tests import utils
@ -18,6 +19,11 @@ from searchlight.tests import utils
TENANT1 = u"1816a16093df465dbc609cf638422a05"
TENANT_ID = u"1dd2c5280b4e45fc9d7d08a81228c891"
fake_version_list = [utils.FakeVersion('2.1'),
utils.FakeVersion('2.1')]
nova_version_getter = 'novaclient.v2.client.versions.VersionManager.list'
class TestNovaPlugins(functional.FunctionalTest):
def setUp(self):
@ -27,7 +33,8 @@ class TestNovaPlugins(functional.FunctionalTest):
self.server_plugin = self.initialized_plugins['OS::Nova::Server']
self.server_objects = self._load_fixture_data('load/servers.json')
def test_hypervisor_rbac(self):
@mock.patch(nova_version_getter, return_value=fake_version_list)
def test_hypervisor_rbac(self, mock_version):
self._index(self.hyper_plugin,
[utils.DictObj(**hyper) for hyper in self.hyper_objects])
response, json_content = self._search_request(
@ -88,7 +95,8 @@ class TestNovaPlugins(functional.FunctionalTest):
actual_sources = [process(hit['_source']) for hit in hits]
self.assertEqual(expected_sources, actual_sources)
def _index_data(self):
@mock.patch(nova_version_getter, return_value=fake_version_list)
def _index_data(self, mock_version):
self._index(self.server_plugin,
[utils.DictObj(**server) for server in self.server_objects]

View File

@ -90,6 +90,10 @@ net_ip4_6 = {
}
]
}
fake_version_list = [test_utils.FakeVersion('2.1'),
test_utils.FakeVersion('2.1')]
net_ipv4 = {u'net4': [dict(net_ip4_6[u'net4'][0])]}
_now = datetime.datetime.utcnow()
@ -98,6 +102,7 @@ created_now = _five_minutes_ago.strftime('%Y-%m-%dT%H:%M:%SZ')
updated_now = _now.strftime('%Y-%m-%dT%H:%M:%SZ')
nova_server_getter = 'novaclient.v2.client.servers.ServerManager.get'
nova_version_getter = 'novaclient.v2.client.versions.VersionManager.list'
def _instance_fixture(instance_id, name, tenant_id, **kwargs):
@ -197,7 +202,8 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
def test_document_type(self):
self.assertEqual('OS::Nova::Server', self.plugin.get_document_type())
def test_serialize(self):
@mock.patch(nova_version_getter, return_value=fake_version_list)
def test_serialize(self, mock_version):
expected = {
u'OS-DCF:diskConfig': u'MANUAL',
u'OS-EXT-AZ:availability_zone': u'az1',
@ -258,7 +264,8 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
self.assertEqual(expected, serialized)
def test_serialize_no_image(self):
@mock.patch(nova_version_getter, return_value=fake_version_list)
def test_serialize_no_image(self, mock_version):
instance = _instance_fixture(
ID3, u'instance3', tenant_id=TENANT1,
flavor=flavor1, image='', addresses=net_ipv4,
@ -341,9 +348,10 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
network_facets = ('name', 'version', 'ipv6_addr', 'ipv4_addr',
'OS-EXT-IPS-MAC:mac_addr', 'OS-EXT-IPS:type')
expected_facet_names = [
'OS-EXT-AZ:availability_zone', 'created_at', 'flavor.id', 'id',
'image.id', 'name', 'owner', 'security_groups', 'status',
'updated_at', 'user_id']
'OS-EXT-AZ:availability_zone', 'created_at', 'description',
'flavor.id', 'id', 'image.id', 'locked', 'name',
'owner', 'security_groups', 'status', 'tags', 'updated_at',
'user_id']
expected_facet_names.extend(['networks.' + f for f in network_facets])
self.assertEqual(set(expected_facet_names), set(facet_names))
@ -356,7 +364,8 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
for name in complex_facet_option_fields)
simple_facet_option_fields = (
'status', 'OS-EXT-AZ:availability_zone', 'security_groups'
'status', 'OS-EXT-AZ:availability_zone', 'security_groups',
'locked'
)
aggs.update(dict(unit_test_utils.simple_facet_field_agg(name)
for name in simple_facet_option_fields))
@ -397,9 +406,10 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
network_facets = ('name', 'version', 'ipv6_addr', 'ipv4_addr',
'OS-EXT-IPS-MAC:mac_addr', 'OS-EXT-IPS:type')
expected_facet_names = [
'OS-EXT-AZ:availability_zone', 'created_at', 'flavor.id', 'id',
'image.id', 'name', 'owner', 'project_id', 'security_groups',
'status', 'tenant_id', 'updated_at', 'user_id']
'OS-EXT-AZ:availability_zone', 'created_at', 'description',
'flavor.id', 'host_status', 'id', 'image.id', 'locked',
'name', 'owner', 'project_id', 'security_groups', 'status',
'tags', 'tenant_id', 'updated_at', 'user_id']
expected_facet_names.extend(['networks.' + f for f in network_facets])
self.assertEqual(set(expected_facet_names), set(facet_names))
@ -411,7 +421,8 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
for name in complex_facet_option_fields)
simple_facet_option_fields = (
'status', 'OS-EXT-AZ:availability_zone', 'security_groups'
'status', 'OS-EXT-AZ:availability_zone', 'security_groups',
'host_status', 'locked'
)
aggs.update(dict(unit_test_utils.simple_facet_field_agg(name)
for name in simple_facet_option_fields))
@ -514,7 +525,8 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
self.assertEqual(expected_status, status_facet)
self.assertEqual(expected_image, image_facet)
def test_created_at_updated_at(self):
@mock.patch(nova_version_getter, return_value=fake_version_list)
def test_created_at_updated_at(self, mock_version):
self.assertTrue('created_at' not in self.instance1.to_dict())
self.assertTrue('updated_at' not in self.instance1.to_dict())
@ -524,7 +536,8 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
self.assertEqual(serialized['created_at'], created_now)
self.assertEqual(serialized['updated_at'], updated_now)
def test_update_404_deletes(self):
@mock.patch(nova_version_getter, return_value=fake_version_list)
def test_update_404_deletes(self, mock_version):
"""Test that if a server is missing on a notification event, it
gets deleted from the index
"""
@ -570,7 +583,8 @@ class TestServerLoaderPlugin(test_utils.BaseTestCase):
mock_update.assert_called_with(vol_payload, "a", 1234)
def test_filter_result(self):
@mock.patch(nova_version_getter, return_value=fake_version_list)
def test_filter_result(self, mock_version):
"""We reformat outgoing results so that security group looks like the
response we get from the nova API.
"""

View File

@ -561,3 +561,8 @@ class DictObj(object):
def to_dict(self):
return self.__dict__
class FakeVersion(object):
def __init__(self, version):
self.version = version