Flavor Plugin

This patch adds support to index Flavors of Nova.
Implements: blueprint nova-flavor-plugin
Change-Id: I190c37a7cce3e0f4d80a994315db7545eb4b6fb6
This commit is contained in:
Geetika Batra 2016-05-12 13:55:24 +05:30 committed by Steve McLellan
parent af80169f13
commit f7d1a48428
15 changed files with 270 additions and 1 deletions

View File

@ -197,6 +197,9 @@ enabled = True
[resource_plugin:os_nova_hypervisor]
enabled = True
[resource_plugin:os_nova_flavor]
enabled = True
[resource_plugin:os_glance_image]
enabled = True

View File

@ -85,6 +85,9 @@ Please read the rest of the guide for detailed information.::
[resource_plugin:os_nova_hypervisor]
enabled = True
[resource_plugin:os_nova_flavor]
enabled = True
[resource_plugin:os_glance_image]
enabled = True

View File

@ -73,6 +73,19 @@ Plugin: OS::Nova::Hypervisor
putting it to its own resource group and scheduling a cron job to re-sync
with little overhead.
Plugin: OS::Nova::Flavor
^^^^^^^^^^^^^^^^^^^^^^^^
::
[resource_plugin:os_nova_flavor]
enabled = true
.. note::
There are no notifications for flavor from nova yet, so we recommend
putting it in its own resource group and scheduling a cron job to re-sync
with little overhead.
Nova Configuration
==================

View File

@ -10,6 +10,7 @@
"resource:OS::Glance::Metadef:allow": "",
"resource:OS::Nova::Server:allow": "",
"resource:OS::Nova::Hypervisor:allow": "role:admin",
"resource:OS::Nova::Flavor:allow": "",
"resource:OS::Cinder::Volume:allow": "",
"resource:OS::Cinder::Snapshot:allow": "",
"resource:OS::Designate::Zone:allow": "",

View File

@ -0,0 +1,7 @@
---
features:
- Adds nova plugin for flavors.
issues:
- There are no notifications for flavors from nova yet, so we recommend
putting it to its own resource group and scheduling a cron job to re-sync
with little overhead.

View File

@ -16,16 +16,32 @@
import copy
import json
import logging
import novaclient.exceptions
import six
from searchlight.elasticsearch.plugins import openstack_clients
from searchlight.elasticsearch.plugins import utils
from searchlight.i18n import _LW
LOG = logging.getLogger(__name__)
# All 'links' will also be removed
BLACKLISTED_FIELDS = set((u'progress', u'links'))
FLAVOR_ACCESS_FIELD = 'tenant_access'
def _get_flavor_access(flavor):
if flavor.is_public:
return None
try:
n_client = openstack_clients.get_novaclient()
return [access.tenant_id for access in
n_client.flavor_access.list(flavor=flavor)] or None
except novaclient.exceptions.Unauthorized:
LOG.warning(_LW("Could not return tenant for %s; forbidden") %
flavor)
return None
def serialize_nova_server(server):
@ -78,6 +94,19 @@ def serialize_nova_hypervisor(hypervisor, updated_at=None):
return serialized
def serialize_nova_flavor(flavor, updated_at=None):
serialized = {k: v for k, v in six.iteritems(flavor.to_dict())
if k not in ("links")}
serialized["extra_specs"] = flavor.get_keys()
serialized[FLAVOR_ACCESS_FIELD] = _get_flavor_access(flavor)
if not getattr(flavor, 'updated_at', None):
serialized['updated_at'] = updated_at or utils.get_now_str()
return serialized
def _format_networks(server, serialized):
networks = []

View File

@ -0,0 +1,102 @@
# 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 searchlight.common import resource_types
from searchlight.elasticsearch.plugins import base
from searchlight.elasticsearch.plugins.nova import FLAVOR_ACCESS_FIELD
from searchlight.elasticsearch.plugins.nova \
import notification_handler
from searchlight.elasticsearch.plugins.nova import serialize_nova_flavor
from searchlight.elasticsearch.plugins import openstack_clients
class FlavorIndex(base.IndexBase):
NotificationHandlerCls = notification_handler.FlavorHandler
@classmethod
def get_document_type(self):
return resource_types.NOVA_FLAVOR
def get_mapping(self):
str_analysis = {'type': 'string', 'index': 'not_analyzed'}
integer = {'type': 'integer'}
return {
'dynamic': True,
'properties': {
'id': str_analysis,
'tenant_id': str_analysis,
'OS-FLV-DISABLED:disabled': {'type': 'boolean'},
'OS-FLV-EXT-DATA:ephemeral': integer,
'disk': integer,
'name': {
'type': 'string',
'fields': {
'raw': str_analysis
}
},
'os-flavor-access:is_public': {'type': 'boolean'},
FLAVOR_ACCESS_FIELD: str_analysis,
'ram': integer,
'rxtx_factor': {'type': 'float'},
'swap': str_analysis,
'vcpus': integer,
'extra-specs': {
'type': 'nested',
'properties': {}
}
}
}
def get_objects(self):
"""Generator that lists all nova Flavors"""
return openstack_clients.get_novaclient().flavors.list(is_public=None)
@property
def facets_with_options(self):
return ('OS-FLV-DISABLED:disabled', 'os-flavor-access:is_public')
def serialize(self, flavor):
return serialize_nova_flavor(flavor)
def _get_rbac_field_filters(self, request_context):
"""Return any RBAC field filters to be injected into an indices
query. Document type will be added to this list.
"""
return [
{
'or': [
{
'term': {
'os-flavor-access:is_public': True
}
},
{
'term': {
FLAVOR_ACCESS_FIELD: request_context.tenant
}
}
]
}
]
def filter_result(self, hit, request_context):
super(FlavorIndex, self).filter_result(hit, request_context)
# Only admins and tenants who the flavor has been granted
# access can see the full list of access.
if not request_context.is_admin:
source = hit['_source']
is_public = source.get('os-flavor-access:is_public', False)
access = source.get(FLAVOR_ACCESS_FIELD, [])
if is_public or request_context.tenant not in access:
source.pop(FLAVOR_ACCESS_FIELD, None)

View File

@ -244,3 +244,18 @@ class HypervisorHandler(base.NotificationBase):
# hypervisor in nova is implemented:
# https://blueprints.launchpad.net/nova/+spec/hypervisor-notification
return {}
class FlavorHandler(base.NotificationBase):
"""Handles nova flavor notifications.
"""
@classmethod
def _get_notification_exchanges(cls):
return []
def get_event_handlers(self):
# TODO(): Currently there is no notification for flavor,
# this needs to be changed once the notification for
# flavor in nova is implemented:
return {}

View File

@ -369,7 +369,8 @@ class FunctionalTest(test_utils.BaseTestCase):
plugin_classes = {
'glance': {'images': 'ImageIndex', 'metadefs': 'MetadefIndex'},
'nova': {'servers': 'ServerIndex',
'hypervisors': 'HypervisorIndex'},
'hypervisors': 'HypervisorIndex',
'flavors': 'FlavorIndex'},
'cinder': {'volumes': 'VolumeIndex', 'snapshots': 'SnapshotIndex'},
'neutron': {'networks': 'NetworkIndex', 'ports': 'PortIndex',
'subnets': 'SubnetIndex', 'routers': 'RouterIndex',
@ -385,6 +386,7 @@ class FunctionalTest(test_utils.BaseTestCase):
plugins = include_plugins or (
('glance', 'images'), ('glance', 'metadefs'),
('nova', 'servers'), ('nova', 'hypervisors'),
('nova', 'flavors'),
('cinder', 'volumes'), ('cinder', 'snapshots'),
('neutron', 'networks'), ('neutron', 'ports'),
('neutron', 'subnets'), ('neutron', 'routers'),

View File

@ -0,0 +1,16 @@
[
{
"name": "m1.tiny",
"tenant_id": "",
"ram": 512,
"extra_spec": {},
"OS-FLV-DISABLED:disabled": false,
"vcpus": 1,
"swap": "",
"os-flavor-access:is_public": true,
"rxtx_factor": 1.0,
"OS-FLV-EXT-DATA:ephemeral": 0,
"disk": 1,
"id": "1"
}
]

View File

@ -22,6 +22,7 @@ METADEFS_FILE = "searchlight/tests/functional/data/load/metadefs.json"
IMAGE_MEMBERS_FILE = \
"searchlight/tests/functional/data/load/image_members.json"
SERVERS_FILE = "searchlight/tests/functional/data/load/servers.json"
FLAVORS_FILE = "searchlight/tests/functional/data/load/flavors.json"
from glanceclient.v2 import client as glance
@ -32,6 +33,15 @@ import novaclient.client
_session = None
def _get_flavor_tenant(flavor):
if flavor.is_public:
return ""
n_client = get_novaclient()
flavor_access = n_client.flavor_access.list(flavor=flavor)[0]
tenant_id = flavor_access.tenant_id
return tenant_id
def _get_session():
global _session
@ -116,10 +126,24 @@ def get_nova_servers_with_pyclient():
f.write(servers_json)
def get_nova_flavors_with_pyclient():
nova_client = get_novaclient()
flavor = nova_client.flavors.list()[0]
flavor_dict = flavor.to_dict()
flavor_dict.pop("links")
flavor_dict.update({"tenant_id": _get_flavor_tenant(flavor)})
flavor_dict.update({"extra_spec": flavor.get_keys()})
flavors_json = json.dumps([flavor_dict], indent=4)
with open(FLAVORS_FILE, "w") as f:
f.write(flavors_json)
def generate():
get_glance_images_and_members_with_pyclient()
get_glance_metadefs_with_pyclient()
get_nova_servers_with_pyclient()
get_nova_flavors_with_pyclient()
if __name__ == "__main__":
generate()

View File

@ -32,6 +32,8 @@ class TestNovaPlugins(functional.FunctionalTest):
self.hyper_objects = self._load_fixture_data('load/hypervisors.json')
self.server_plugin = self.initialized_plugins['OS::Nova::Server']
self.server_objects = self._load_fixture_data('load/servers.json')
self.flavor_plugin = self.initialized_plugins['OS::Nova::Flavor']
self.flavor_objects = self._load_fixture_data('load/flavors.json')
@mock.patch(nova_version_getter, return_value=fake_version_list)
def test_hypervisor_rbac(self, mock_version):
@ -162,3 +164,39 @@ class TestNovaPlugins(functional.FunctionalTest):
actual_sources = [process(hit['_source']) for hit in hits]
self.assertEqual(expected_sources, actual_sources)
def test_flavor_rbac(self):
self._index(self.flavor_plugin,
[utils.FlavorDictObj(**flavor)
for flavor in self.flavor_objects]
)
query = {
"type": ["OS::Nova::Flavor"],
"query": {
"match_all": {}
}
}
response, json_content = self._search_request(query, TENANT1)
expected_sources = [{
u'OS-FLV-DISABLED:disabled': False,
u'OS-FLV-EXT-DATA:ephemeral': 0,
u'disk': 1,
u"tenant_id": u"",
u'extra_specs': {},
u'id': u'1',
u'name': u'm1.tiny',
u'os-flavor-access:is_public': True,
u'ram': 512,
u'rxtx_factor': 1.0,
u'swap': u'',
u'vcpus': 1}]
hits = json_content['hits']['hits']
for hit in hits:
source = hit["_source"]
source.pop("updated_at")
actual_sources = [hit["_source"] for hit in hits]
self.assertEqual(expected_sources, actual_sources)
self.assertEqual(200, response.status)
self.assertEqual(1, json_content['hits']['total'])

View File

@ -294,6 +294,7 @@ class TestSearchDeserializer(test_utils.BaseTestCase):
'OS::Designate::Zone',
'OS::Glance::Image',
'OS::Glance::Metadef',
'OS::Nova::Flavor',
'OS::Nova::Server',
'OS::Nova::Hypervisor',
'OS::Neutron::FloatingIP',

View File

@ -15,6 +15,7 @@
"""Common utilities used in testing"""
import copy
import errno
import functools
import os
@ -566,3 +567,16 @@ class DictObj(object):
class FakeVersion(object):
def __init__(self, version):
self.version = version
class FlavorDictObj(DictObj):
def get_keys(self):
return self.extra_spec
def is_public(self):
return self.is_public
def to_dict(self):
d = copy.deepcopy(super(FlavorDictObj, self).to_dict())
d.pop("extra_spec", None)
return d

View File

@ -34,6 +34,7 @@ searchlight.index_backend =
os_nova_server = searchlight.elasticsearch.plugins.nova.servers:ServerIndex
os_nova_hypervisor = searchlight.elasticsearch.plugins.nova.hypervisors:HypervisorIndex
os_neutron_floatingip = searchlight.elasticsearch.plugins.neutron.floatingips:FloatingIPIndex
os_nova_flavor = searchlight.elasticsearch.plugins.nova.flavors:FlavorIndex
os_neutron_network = searchlight.elasticsearch.plugins.neutron.networks:NetworkIndex
os_neutron_subnet = searchlight.elasticsearch.plugins.neutron.subnets:SubnetIndex
os_neutron_port = searchlight.elasticsearch.plugins.neutron.ports:PortIndex