Fixing DB issue with auto-generation of flavors
- Inlcude keystone authentication - Updated and included unit tests Change-Id: I8ad4790db585b274293ac34e7d0ba7ec00f61df1 Signed-off-by: Helena McGough <helena.mcgough@intel.com>
This commit is contained in:
parent
97ab8e835e
commit
cb6f9fae17
@ -4,4 +4,3 @@
|
||||
- openstack-python36-jobs
|
||||
- check-requirements
|
||||
- openstack-cover-jobs
|
||||
- openstack-lower-constraints-jobs
|
||||
|
@ -35,6 +35,11 @@ function configure_nova_rsd {
|
||||
iniset $NOVA_CONF rsd podm_user ${PODM_USER}
|
||||
iniset $NOVA_CONF rsd podm_password ${PODM_PASSWD}
|
||||
iniset $NOVA_CONF rsd podm_port ${PODM_PORT}
|
||||
iniset $NOVA_CONF rsd auth_password ${OS_PASSWORD}
|
||||
iniset $NOVA_CONF rsd auth_url ${OS_AUTH_URL}
|
||||
iniset $NOVA_CONF rsd identity_version ${OS_IDENTITY_API_VERSION}
|
||||
iniset $NOVA_CONF rsd tenant_name ${OS_PROJECT_NAME}
|
||||
iniset $NOVA_CONF rsd username ${OS_USERNAME}
|
||||
}
|
||||
|
||||
# disabling ERROR_NO_CLONE to allow this plugin work with devstack-gate
|
||||
|
@ -1,6 +1,10 @@
|
||||
# This driver is enabled in override-defaults with:
|
||||
# VIRT_DRIVER=${VIRT_DRIVER:-rsd}
|
||||
|
||||
# Auth info
|
||||
OS_AUTH_URL="$KEYSTONE_SERVICE_URI/v$IDENTITY_API_VERSION"
|
||||
OS_IDENTITY_API_VERSION=${IDENTITY_API_VERSION:-3}
|
||||
|
||||
if [ "$VERBOSE" == "False" ]; then
|
||||
# allow local debugging
|
||||
set -o xtrace
|
||||
|
145
rsd_virt_for_nova/conf/keystone_light.py
Normal file
145
rsd_virt_for_nova/conf/keystone_light.py
Normal file
@ -0,0 +1,145 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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.
|
||||
""" Lightweight (keystone) client for the OpenStack Identity API """
|
||||
|
||||
import logging
|
||||
import requests
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KeystoneException(Exception):
|
||||
def __init__(self, message, exc=None, response=None):
|
||||
if exc:
|
||||
message += "\nReason: %s" % exc
|
||||
super(KeystoneException, self).__init__(message)
|
||||
|
||||
self.response = response
|
||||
self.exception = exc
|
||||
|
||||
|
||||
class InvalidResponse(KeystoneException):
|
||||
def __init__(self, exc, response):
|
||||
super(InvalidResponse, self).__init__(
|
||||
"Invalid response from ident", exc, response)
|
||||
|
||||
|
||||
class MissingServices(KeystoneException):
|
||||
def __init__(self, message, exc, response):
|
||||
super(MissingServices, self).__init__(
|
||||
"MissingServices: " + message, exc, response)
|
||||
|
||||
|
||||
class ClientV3(object):
|
||||
"""Light weight client for the OpenStack Identity API V3.
|
||||
|
||||
:param string username: Username for authentication.
|
||||
:param string password: Password for authentication.
|
||||
:param string tenant_name: Tenant name.
|
||||
:param string auth_url: Keystone service endpoint for authorization.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, auth_url, username, password, tenant_name):
|
||||
"""Initialize a new client"""
|
||||
|
||||
self.auth_url = auth_url
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.tenant_name = tenant_name
|
||||
self._auth_token = ''
|
||||
self._services = ()
|
||||
self._services_by_name = {}
|
||||
|
||||
@property
|
||||
def auth_token(self):
|
||||
"""Return token string usable for X-Auth-Token """
|
||||
# actualize token
|
||||
self.refresh()
|
||||
return self._auth_token
|
||||
|
||||
@property
|
||||
def services(self):
|
||||
"""Return list of services retrieved from identity server """
|
||||
return self._services
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh token and services list."""
|
||||
headers = {'Accept': 'application/json'}
|
||||
url = self.auth_url.rstrip('/') + '/auth/tokens'
|
||||
params = {
|
||||
'auth': {
|
||||
'identity': {
|
||||
'methods': ['password'],
|
||||
'password': {
|
||||
'user': {
|
||||
'name': self.username,
|
||||
'domain': {'id': 'default'},
|
||||
'password': self.password
|
||||
}
|
||||
}
|
||||
},
|
||||
'scope': {
|
||||
'project': {
|
||||
'name': self.tenant_name,
|
||||
'domain': {'id': 'default'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp = requests.post(url, json=params, headers=headers)
|
||||
resp_data = None
|
||||
# processing response
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
resp_data = resp.json()['token']
|
||||
self._services = tuple(resp_data['catalog'])
|
||||
self._services_by_name = {
|
||||
service['name']: service for service in self._services
|
||||
}
|
||||
self._auth_token = resp.headers['X-Subject-Token']
|
||||
except (TypeError, KeyError, ValueError,
|
||||
requests.exceptions.HTTPError) as e:
|
||||
LOG.exception("Error processing response from keystone")
|
||||
raise InvalidResponse(e, resp_data)
|
||||
return resp_data
|
||||
|
||||
def get_service_endpoint(self, name, urlkey="public", region=None):
|
||||
"""Return url endpoint of service
|
||||
|
||||
possible values of urlkey = 'adminURL' | 'publicURL' | 'internalURL'
|
||||
provide region if more endpoints are available
|
||||
"""
|
||||
|
||||
try:
|
||||
endpoints = self._services_by_name[name]['endpoints']
|
||||
if not endpoints:
|
||||
raise MissingServices("Missing name '%s' in received services"
|
||||
% name,
|
||||
None, self._services)
|
||||
|
||||
if region:
|
||||
for ep in endpoints:
|
||||
if ep['region'] == region and ep['interface'] in urlkey:
|
||||
return ep["url"].rstrip('/')
|
||||
else:
|
||||
for ep in endpoints:
|
||||
if ep['interface'] in urlkey:
|
||||
return ep["url"].rstrip('/')
|
||||
raise MissingServices("No valid endpoints found")
|
||||
except (KeyError, ValueError) as e:
|
||||
LOG.exception("Error while processing endpoints")
|
||||
raise MissingServices("Missing data in received services",
|
||||
e, self._services)
|
@ -39,7 +39,22 @@ rsd_opts = [
|
||||
'PODM. '),
|
||||
cfg.IntOpt('podm_port',
|
||||
default=8443,
|
||||
help='Specifying port on PODM for communication. ')
|
||||
help='Specifying port on PODM for communication. '),
|
||||
cfg.StrOpt('auth_password',
|
||||
default='',
|
||||
help='Password required to authenticate to keystone. '),
|
||||
cfg.StrOpt('auth_url',
|
||||
default='',
|
||||
help='URL require to authenticate to keystone. '),
|
||||
cfg.IntOpt('identity_version',
|
||||
default=3,
|
||||
help='Keystone version. '),
|
||||
cfg.StrOpt('tenant_name',
|
||||
default='',
|
||||
help='Name of the openstack tenant. '),
|
||||
cfg.StrOpt('username',
|
||||
default='',
|
||||
help='OpenStack username to authenticate to keystone. ')
|
||||
]
|
||||
|
||||
STATIC_OPTIONS = (rsd_opts)
|
||||
|
@ -40,3 +40,13 @@ class TestConf(test.NoDBTestCase):
|
||||
self.assertEqual('admin', CONF.rsd.podm_password)
|
||||
# PODM port
|
||||
self.assertEqual(8443, CONF.rsd.podm_port)
|
||||
# auth password
|
||||
self.assertEqual('', CONF.rsd.auth_password)
|
||||
# auth url
|
||||
self.assertEqual('', CONF.rsd.auth_url)
|
||||
# keystone version
|
||||
self.assertEqual(3, CONF.rsd.identity_version)
|
||||
# Tenant Name
|
||||
self.assertEqual('', CONF.rsd.tenant_name)
|
||||
# Auth username
|
||||
self.assertEqual('', CONF.rsd.username)
|
||||
|
211
rsd_virt_for_nova/tests/conf/test_keystone_light.py
Normal file
211
rsd_virt_for_nova/tests/conf/test_keystone_light.py
Normal file
@ -0,0 +1,211 @@
|
||||
# 2017 - 2018 Intel Corporation. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Tests for the RSD keystone_light authentication."""
|
||||
|
||||
import mock
|
||||
|
||||
import requests
|
||||
|
||||
from nova import test
|
||||
|
||||
from rsd_virt_for_nova.conf.keystone_light import ClientV3
|
||||
from rsd_virt_for_nova.conf.keystone_light import MissingServices
|
||||
|
||||
|
||||
class TestClientV3(test.NoDBTestCase):
|
||||
"""Test class for configurations."""
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize configuration test class."""
|
||||
super(TestClientV3, self).setUp()
|
||||
|
||||
self.client = ClientV3("my_auth_url", "user", "pass", "tenant")
|
||||
self.test_authtoken = "c5bbb1c9a27e470fb482de2a718e08c2"
|
||||
self.test_public_endpoint = "http://public_endpoint"
|
||||
self.test_internal_endpoint = "http://internal_endpoint"
|
||||
self.test_region = "RegionOne"
|
||||
|
||||
response = {"token": {
|
||||
"is_domain": 'false',
|
||||
"methods": [
|
||||
"password"
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"id": "eacf519eb1264cba9ad645355ce1f6ec",
|
||||
"name": "ResellerAdmin"
|
||||
},
|
||||
{
|
||||
"id": "63e481b5d5f545ecb8947072ff34f10d",
|
||||
"name": "admin"
|
||||
}
|
||||
],
|
||||
"is_admin_project": 'false',
|
||||
"project": {
|
||||
"domain": {
|
||||
"id": "default",
|
||||
"name": "Default"
|
||||
},
|
||||
"id": "97467f21efb2493c92481429a04df7bd",
|
||||
"name": "service"
|
||||
},
|
||||
"catalog": [
|
||||
{
|
||||
"endpoints": [
|
||||
{
|
||||
"url": self.test_public_endpoint + '/',
|
||||
"interface": "public",
|
||||
"region": self.test_region,
|
||||
"region_id": self.test_region,
|
||||
"id": "5e1d9a45d7d442ca8971a5112b2e89b5"
|
||||
},
|
||||
{
|
||||
"url": "http://127.0.0.1:8777",
|
||||
"interface": "admin",
|
||||
"region": self.test_region,
|
||||
"region_id": self.test_region,
|
||||
"id": "5e8b536fde6049d381ee540c018905d1"
|
||||
},
|
||||
{
|
||||
"url": self.test_internal_endpoint + '/',
|
||||
"interface": "internal",
|
||||
"region": self.test_region,
|
||||
"region_id": self.test_region,
|
||||
"id": "db90c733ddd9466696bc5aaec43b18d0"
|
||||
}
|
||||
],
|
||||
"type": "compute",
|
||||
"id": "f6c15a041d574bc190c70815a14ab851",
|
||||
"name": "nova"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
self.mock_response = mock.Mock()
|
||||
self.mock_response.json.return_value = response
|
||||
self.mock_response.headers = {
|
||||
'X-Subject-Token': "c5bbb1c9a27e470fb482de2a718e08c2"
|
||||
}
|
||||
|
||||
def test_ClientV3_init(self):
|
||||
"""Test initialising keystone clientv3."""
|
||||
self.assertEqual(self.client.auth_url, "my_auth_url")
|
||||
self.assertEqual(self.client.username, "user")
|
||||
self.assertEqual(self.client.password, "pass")
|
||||
self.assertEqual(self.client.tenant_name, "tenant")
|
||||
self.assertEqual(self.client._auth_token, '')
|
||||
self.assertEqual(self.client._services, ())
|
||||
self.assertEqual(self.client._services_by_name, {})
|
||||
|
||||
@mock.patch.object(requests, "post")
|
||||
def test_refresh(self, post_req):
|
||||
"""Test the refresh function for auth tokens."""
|
||||
resp = self.client.refresh()
|
||||
|
||||
url = self.client.auth_url.rstrip('/') + '/auth/tokens'
|
||||
params = {
|
||||
'auth': {
|
||||
'identity': {
|
||||
'methods': ['password'],
|
||||
'password': {
|
||||
'user': {
|
||||
'name': self.client.username,
|
||||
'domain': {'id': 'default'},
|
||||
'password': self.client.password
|
||||
}
|
||||
}
|
||||
},
|
||||
'scope': {
|
||||
'project': {
|
||||
'name': self.client.tenant_name,
|
||||
'domain': {'id': 'default'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
headers = {'Accept': 'application/json'}
|
||||
post_req.assert_called_once_with(url, json=params, headers=headers)
|
||||
|
||||
self.assertEqual(resp, post_req.return_value.json()['token'])
|
||||
|
||||
@mock.patch.object(requests, 'post')
|
||||
def test_getservice_endpoint(self, mock_post):
|
||||
"""Test get_service_endpoint"""
|
||||
|
||||
mock_post.return_value = self.mock_response
|
||||
|
||||
client = ClientV3("test_auth_url", "test_username",
|
||||
"test_password", "test_tenant")
|
||||
client.refresh()
|
||||
|
||||
endpoint = client.get_service_endpoint('nova')
|
||||
self.assertEqual(endpoint, self.test_public_endpoint)
|
||||
|
||||
self.assertRaises(MissingServices,
|
||||
client.get_service_endpoint, 'badname')
|
||||
|
||||
@mock.patch.object(requests, 'post')
|
||||
def test_getservice_endpoint_error(self, mock_post):
|
||||
"""Test get service endpoint error"""
|
||||
|
||||
response = {"token": {
|
||||
"is_domain": 'false',
|
||||
"methods": [
|
||||
"password"
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"id": "eacf519eb1264cba9ad645355ce1f6ec",
|
||||
"name": "ResellerAdmin"
|
||||
},
|
||||
{
|
||||
"id": "63e481b5d5f545ecb8947072ff34f10d",
|
||||
"name": "admin"
|
||||
}
|
||||
],
|
||||
"is_admin_project": 'false',
|
||||
"project": {
|
||||
"domain": {
|
||||
"id": "default",
|
||||
"name": "Default"
|
||||
},
|
||||
"id": "97467f21efb2493c92481429a04df7bd",
|
||||
"name": "service"
|
||||
},
|
||||
"catalog": [
|
||||
{
|
||||
"endpoints": [],
|
||||
"type": "compute",
|
||||
"id": "f6c15a041d574bc190c70815a14ab851",
|
||||
"name": "badname"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
self.mock_response = mock.Mock()
|
||||
self.mock_response.json.return_value = response
|
||||
self.mock_response.headers = {
|
||||
'X-Subject-Token': "c5bbb1c9a27e470fb482de2a718e08c2"
|
||||
}
|
||||
mock_post.return_value = self.mock_response
|
||||
|
||||
client = ClientV3("test_auth_url", "test_username",
|
||||
"test_password", "test_tenant")
|
||||
|
||||
client.refresh()
|
||||
|
||||
self.assertRaises(MissingServices, client.get_service_endpoint, 'nova')
|
@ -28,13 +28,9 @@ from nova import rc_fields as fields
|
||||
from nova.compute import power_state
|
||||
from nova.compute import provider_tree
|
||||
|
||||
from nova.objects import flavor
|
||||
|
||||
from nova.virt import fake
|
||||
from nova.virt import hardware
|
||||
|
||||
from rsd_virt_for_nova.conf import rsd as cfg
|
||||
|
||||
from rsd_virt_for_nova.virt import rsd
|
||||
from rsd_virt_for_nova.virt.rsd import driver
|
||||
|
||||
@ -54,8 +50,6 @@ from rsd_lib.resources.v2_3.node import node as v2_3_node
|
||||
|
||||
from sushy import connector
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class FakeInstance(object):
|
||||
"""A class to fake out nova instances."""
|
||||
@ -703,108 +697,3 @@ class TestRSDDriver(base.BaseTestCase):
|
||||
'/redfish/v1/Systems/System2',
|
||||
'/redfish/v1/Systems/System3',
|
||||
'/redfish/v1/Systems/System4'])
|
||||
|
||||
@mock.patch.object(driver.RSDDriver, 'check_chassis_systems')
|
||||
@mock.patch.object(context, 'get_admin_context')
|
||||
@mock.patch.object(flavor.Flavor, '_flavor_get_by_flavor_id_from_db')
|
||||
@mock.patch.object(flavor, '_flavor_create')
|
||||
@mock.patch.object(fields.ResourceClass, 'normalize_name')
|
||||
@mock.patch.object(driver.RSDDriver, 'conv_GiB_to_MiB')
|
||||
def test_create_flavors_success(self, conv_mem, norm_name, flav_create,
|
||||
get_flav, admin_context, check_chas):
|
||||
"""Test creation of new flavors for a System, success."""
|
||||
# Set up the mock objects for a sucessful creation test
|
||||
sys_col = self.RSD.driver.PODM.get_system_collection.return_value
|
||||
chas_col = self.RSD.driver.PODM.get_chassis_collection.return_value
|
||||
chas_col.members_identities = ['/redfish/v1/Chassis/Chassis1']
|
||||
sys_col.members_identities = ['/redfish/v1/Systems/System1']
|
||||
sys_col.get_member.return_value = self.system_inst
|
||||
chas_col.get_member.return_value = self.chassis_inst
|
||||
check_chas.return_value = ['/redfish/v1/Systems/System1']
|
||||
spec = 'resources:' + norm_name.return_value
|
||||
mem = conv_mem.return_value - 512
|
||||
proc = self.system_inst.processors.summary.count
|
||||
flav_id = str(mem) + 'MB-' + str(proc) + 'vcpus'
|
||||
# Run test
|
||||
self.RSD._create_flavors()
|
||||
|
||||
# Check the function calls for the test
|
||||
self.RSD.driver.PODM.get_system_collection.assert_called_once()
|
||||
self.RSD.driver.PODM.get_chassis_collection.assert_called()
|
||||
chas_col.get_member.assert_called_with('/redfish/v1/Chassis/Chassis1')
|
||||
check_chas.assert_called()
|
||||
sys_col.get_member.assert_called_with('/redfish/v1/Systems/System1')
|
||||
conv_mem.assert_called_with(self.system_inst.memory_summary.size_gib)
|
||||
norm_name.assert_called_with(flav_id)
|
||||
admin_context.assert_called()
|
||||
|
||||
# Flavor creation call check
|
||||
flav_create.assert_called_once_with(
|
||||
admin_context.return_value,
|
||||
{'name': 'RSD-' + flav_id,
|
||||
'flavorid': flav_id,
|
||||
'memory_mb': mem,
|
||||
'vcpus': self.system_inst.processors.summary.count,
|
||||
'root_gb': 0,
|
||||
'extra_specs': {spec: '1'}})
|
||||
get_flav.assert_not_called()
|
||||
|
||||
@mock.patch.object(flavor.Flavor, '_flavor_get_by_flavor_id_from_db')
|
||||
@mock.patch.object(flavor, '_flavor_create')
|
||||
@mock.patch.object(fields.ResourceClass, 'normalize_name')
|
||||
@mock.patch.object(driver.RSDDriver, 'conv_GiB_to_MiB')
|
||||
def test_create_flavors_failure(self, conv_mem, norm_name, flav_create,
|
||||
flav_from_db):
|
||||
"""Test failing the creation of new flavors for a System."""
|
||||
# Setup for a failed flavor creation test
|
||||
sys_col = self.RSD.driver.PODM.get_system_collection.return_value
|
||||
self.RSD._create_flavors()
|
||||
|
||||
# Verification of flavor creation failure and invalid function calls
|
||||
self.RSD.driver.PODM.get_system_collection.assert_called_once()
|
||||
sys_col.get_member.assert_not_called()
|
||||
conv_mem.assert_not_called()
|
||||
norm_name.assert_not_called()
|
||||
flav_create.assert_not_called()
|
||||
flav_from_db.assert_not_called()
|
||||
|
||||
@mock.patch.object(objects.FlavorList, 'get_all')
|
||||
@mock.patch.object(context, 'get_admin_context')
|
||||
@mock.patch.object(flavor, '_flavor_destroy')
|
||||
def test_check_flavors_failure(self, flav_destroy, get_context, flav_list):
|
||||
"""Test for failing to check existing flavors."""
|
||||
# Setup for the test to fail to check flavors
|
||||
sys_col = self.RSD.driver.PODM.get_system_collection.return_value
|
||||
self.RSD.check_flavors(self.system_col, [])
|
||||
|
||||
# Confirm that checking the available flavors failed
|
||||
get_context.assert_called()
|
||||
flav_list.assert_called_with(get_context.return_value)
|
||||
sys_col.get_member.assert_not_called()
|
||||
flav_destroy.assert_not_called()
|
||||
|
||||
@mock.patch.object(objects.FlavorList, 'get_all')
|
||||
@mock.patch.object(context, 'get_admin_context')
|
||||
@mock.patch.object(flavor, '_flavor_destroy')
|
||||
def test_check_flavors_success(self, flav_destroy, get_context, flav_list):
|
||||
"""Test for successfully checking existing flavors."""
|
||||
# Setup for check flavor that exists
|
||||
sys_col = self.RSD.driver.PODM.get_system_collection.return_value
|
||||
sys_col.get_member.return_value = self.system_inst
|
||||
self.RSD.rsd_flavors = {
|
||||
'mock_flav_id': {
|
||||
'id': 'flav_id',
|
||||
'rsd_systems': {
|
||||
'/redfish/v1/Chassis/Chassis1':
|
||||
self.system_inst.identity
|
||||
}
|
||||
}
|
||||
}
|
||||
self.RSD.check_flavors(sys_col, ['/redfish/v1/Systems/System1'])
|
||||
|
||||
# Confirm the list of available flavors
|
||||
# No flavors need to be deleted
|
||||
get_context.assert_called()
|
||||
flav_list.assert_called_with(get_context.return_value)
|
||||
sys_col.get_member.assert_called_with('/redfish/v1/Systems/System1')
|
||||
flav_destroy.assert_called()
|
||||
|
108
rsd_virt_for_nova/tests/virt/rsd/test_flavor_management.py
Normal file
108
rsd_virt_for_nova/tests/virt/rsd/test_flavor_management.py
Normal file
@ -0,0 +1,108 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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.
|
||||
"""Unit tests for the RSD flavor management class."""
|
||||
|
||||
import mock
|
||||
|
||||
from rsd_virt_for_nova.conf import rsd as cfg
|
||||
|
||||
from rsd_virt_for_nova.virt.rsd import flavor_management
|
||||
from rsd_virt_for_nova.conf import keystone_light
|
||||
|
||||
from oslotest import base
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class TestFlavorManager(base.BaseTestCase):
|
||||
"""A test class for the flavor manager class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Initial setup of mocks for all of the unit tests."""
|
||||
super(TestFlavorManager, self).setUp()
|
||||
self.flav_man = flavor_management.FlavorManager()
|
||||
|
||||
def test_init(self):
|
||||
"""Test the initialisation of a flavor manager instance."""
|
||||
self.assertEqual(self.flav_man._url_base, None)
|
||||
self.assertEqual(self.flav_man._keystone, None)
|
||||
self.assertEqual(self.flav_man._auth_token, None)
|
||||
self.assertEqual(self.flav_man.headers, None)
|
||||
|
||||
def test_keystone_req(self):
|
||||
"""Test a successful keystone request."""
|
||||
# TODO(helenam100): write successful and failed versions of test
|
||||
|
||||
@mock.patch.object(flavor_management.FlavorManager, "_get_endpoint")
|
||||
def test_get_base_url(self, get_endpoint):
|
||||
"""Test authentication functionality."""
|
||||
url = self.flav_man._get_base_url()
|
||||
|
||||
get_endpoint.assert_called_once_with("nova")
|
||||
self.assertEqual(self.flav_man._url_base,
|
||||
"{}/flavors".format(get_endpoint.return_value))
|
||||
self.assertEqual(url, self.flav_man._url_base)
|
||||
|
||||
@mock.patch.object(keystone_light, "ClientV3")
|
||||
def test_get_endpoint_success(self, client):
|
||||
"""Test getting a valid endpoint for flavor creation."""
|
||||
self.flav_man._keystone = client.return_value
|
||||
endpoint = self.flav_man._get_endpoint("nova")
|
||||
|
||||
self.flav_man._keystone.get_service_endpoint.assert_called_with("nova")
|
||||
self.assertEqual(endpoint,
|
||||
self.flav_man._keystone.get_service_endpoint.return_value)
|
||||
|
||||
@mock.patch.object(keystone_light.ClientV3, "get_service_endpoint")
|
||||
def test_get_endpoint_failure(self, serv_endpoint):
|
||||
"""Failed test for getting an endpoint for flavor create."""
|
||||
# No valid keystone test
|
||||
self.assertRaises(AttributeError, self.flav_man._get_endpoint, "nova")
|
||||
|
||||
@mock.patch.object(flavor_management.FlavorManager, "_get_endpoint")
|
||||
def test_create_request_url_delete(self, get_end):
|
||||
"""Testing creation of a request url for flavor management."""
|
||||
url = self.flav_man._create_request_url("flav_id", "delete")
|
||||
|
||||
get_end.assert_called_once_with("nova")
|
||||
self.assertEquals(url,
|
||||
"{}/flavors/flav_id".format(get_end.return_value))
|
||||
|
||||
@mock.patch.object(flavor_management.FlavorManager, "_get_endpoint")
|
||||
def test_create_request_url_update(self, get_end):
|
||||
"""Testing creation of a request url for flavor management."""
|
||||
url = self.flav_man._create_request_url("flav_id", "update")
|
||||
|
||||
get_end.assert_called_once_with("nova")
|
||||
self.assertEquals(url,
|
||||
"{}/flavors/flav_id/os-extra_specs".format(
|
||||
get_end.return_value))
|
||||
|
||||
@mock.patch.object(flavor_management.FlavorManager, "_get_endpoint")
|
||||
def test_create_request_url_invalid(self, get_end):
|
||||
"""Testing creation of a request url for flavor management."""
|
||||
url = self.flav_man._create_request_url("flav_id", "invalid")
|
||||
|
||||
get_end.assert_called_once_with("nova")
|
||||
self.assertEquals(url, '')
|
||||
|
||||
def test_get_headers(self):
|
||||
"""Testing getting headers for requests."""
|
||||
headers = self.flav_man.get_headers("my_auth_token")
|
||||
|
||||
self.assertEquals(headers, self.flav_man.headers)
|
||||
self.assertEquals(headers,
|
||||
{'X-Auth-Token': "my_auth_token",
|
||||
'Content-type': 'application/json'})
|
@ -23,31 +23,26 @@ import copy
|
||||
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from nova import context
|
||||
|
||||
from nova import exception
|
||||
|
||||
from nova import objects
|
||||
|
||||
from nova import rc_fields as fields
|
||||
|
||||
from nova.compute import power_state
|
||||
from nova.objects import fields as obj_fields
|
||||
from nova.objects import flavor
|
||||
from nova.virt import driver
|
||||
from nova.virt import hardware
|
||||
|
||||
from rsd_virt_for_nova.conf import rsd as cfg
|
||||
from rsd_virt_for_nova.virt import rsd
|
||||
from rsd_virt_for_nova.virt.rsd import flavor_management
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from oslo_utils import versionutils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
PODM_NODE = ()
|
||||
@ -72,6 +67,7 @@ class RSDDriver(driver.ComputeDriver):
|
||||
"""Initialize the RSDDriver."""
|
||||
super(RSDDriver, self).__init__(virtapi)
|
||||
# Initializes vairables to track compute nodes and instances
|
||||
self.flavor_manager = flavor_management.FlavorManager()
|
||||
self.driver = rsd.PODM_connection()
|
||||
self.instances = OrderedDict()
|
||||
self.rsd_flavors = OrderedDict()
|
||||
@ -86,21 +82,16 @@ class RSDDriver(driver.ComputeDriver):
|
||||
nodes = []
|
||||
CHASSIS_COL = self.driver.PODM.get_chassis_collection()
|
||||
for c in CHASSIS_COL.members_identities:
|
||||
try:
|
||||
chas = CHASSIS_COL.get_member(c)
|
||||
cha_sys = self.check_chassis_systems(chas)
|
||||
if cha_sys != []:
|
||||
nodes.append(c)
|
||||
except Exception as c_ex:
|
||||
LOG.warn("Failed to get chassis information: %s", c_ex)
|
||||
nodes = []
|
||||
|
||||
set_nodes(nodes)
|
||||
return copy.copy(PODM_NODE)
|
||||
|
||||
def init_host(self, host):
|
||||
"""Initialize anything that is necessary for the driver to function."""
|
||||
self._nodes = self._init_nodes()
|
||||
return host
|
||||
|
||||
def get_info(self, instance):
|
||||
@ -197,15 +188,15 @@ class RSDDriver(driver.ComputeDriver):
|
||||
cpu_info = ''
|
||||
cha_sys = []
|
||||
|
||||
if nodename not in self._nodes:
|
||||
return {}
|
||||
|
||||
SYSTEM_COL = self.driver.PODM.get_system_collection()
|
||||
members = SYSTEM_COL.members_identities
|
||||
|
||||
CHASSIS_COL = self.driver.PODM.get_chassis_collection()
|
||||
try:
|
||||
chas = CHASSIS_COL.get_member(nodename)
|
||||
cha_sys = self.check_chassis_systems(chas)
|
||||
except Exception as ex:
|
||||
LOG.warn("Failed to retrieve chassis information:%s", ex)
|
||||
|
||||
# Check if all flavors are valid
|
||||
self.check_flavors(SYSTEM_COL, members)
|
||||
@ -447,38 +438,40 @@ class RSDDriver(driver.ComputeDriver):
|
||||
proc = sys.processors.summary.count
|
||||
flav_id = str(mem) + 'MB-' + str(proc) + 'vcpus'
|
||||
res = fields.ResourceClass.normalize_name(flav_id)
|
||||
spec = 'resources:' + res
|
||||
values = {
|
||||
spec = str('resources:' + res)
|
||||
payload = {
|
||||
"flavor": {
|
||||
'name': 'RSD-' + flav_id,
|
||||
'flavorid': flav_id,
|
||||
'memory_mb': mem,
|
||||
'id': flav_id,
|
||||
'ram': mem,
|
||||
'vcpus': proc,
|
||||
'root_gb': 0,
|
||||
'disk': 0
|
||||
}
|
||||
}
|
||||
if flav_id not in self.rsd_flavors.keys():
|
||||
try:
|
||||
requests.post(
|
||||
self._url_base, data=json.dumps(payload),
|
||||
headers=self.headers)
|
||||
except Exception as ex:
|
||||
LOG.warn("Failed to create the new flavor: %s", ex)
|
||||
try:
|
||||
extra_pay = {
|
||||
'extra_specs': {
|
||||
spec: '1'}
|
||||
}
|
||||
if sys.identity not in self.rsd_flavors:
|
||||
try:
|
||||
LOG.debug("New flavor for system: %s", sys.identity)
|
||||
flavor._flavor_create(
|
||||
context.get_admin_context(), values)
|
||||
self.chas_systems[str(chas.path)] = [str(sys.identity)]
|
||||
update_url = self.flavor_manager._create_request_url(
|
||||
flav_id, 'update')
|
||||
requests.post(
|
||||
update_url, data=json.dumps(extra_pay),
|
||||
headers=self.headers)
|
||||
self.rsd_flavors[flav_id] = {
|
||||
'rsd_systems': self.chas_systems
|
||||
'id': flav_id,
|
||||
'rsd_systems': {
|
||||
str(chas.path): str(sys.identity)}
|
||||
}
|
||||
self._nodes = self._init_nodes()
|
||||
except Exception as ex:
|
||||
LOG.debug(
|
||||
"A flavor already exists for this system: %s", ex)
|
||||
flavor.Flavor._flavor_get_by_flavor_id_from_db(
|
||||
context.get_admin_context(), flav_id)
|
||||
if flav_id not in self.rsd_flavors.keys():
|
||||
self.chas_systems[str(chas.path)] = [
|
||||
str(sys.identity)]
|
||||
self.rsd_flavors[flav_id] = {
|
||||
'rsd_systems': self.chas_systems
|
||||
}
|
||||
|
||||
LOG.warn("Failed to add extra_specs:%s", ex)
|
||||
else:
|
||||
chassis_ = self.rsd_flavors[flav_id]['rsd_systems']
|
||||
if str(chas.path) not in chassis_.keys():
|
||||
@ -510,30 +503,34 @@ class RSDDriver(driver.ComputeDriver):
|
||||
flav_id = str(mem) + 'MB-' + str(proc) + 'vcpus'
|
||||
flav_ids.append(flav_id)
|
||||
|
||||
f_list = objects.FlavorList.get_all(context.get_admin_context())
|
||||
self._keystone = self.flavor_manager.keystone_req()
|
||||
self._auth_token = self._keystone.auth_token
|
||||
|
||||
self._url_base = self.flavor_manager._get_base_url()
|
||||
self.headers = self.flavor_manager.get_headers(self._auth_token)
|
||||
|
||||
req = requests.get(self._url_base, headers=self.headers)
|
||||
f_list = json.loads(req.text)['flavors']
|
||||
for f in f_list:
|
||||
if 'RSD' in f.name:
|
||||
if f.flavorid not in flav_ids:
|
||||
try:
|
||||
flavor._flavor_destroy(
|
||||
context.get_admin_context(),
|
||||
flavor_id=f.flavorid)
|
||||
except exception.FlavorNotFound as ex:
|
||||
LOG.warn("Flavor not found exception: %s", ex)
|
||||
if 'RSD' in f['name']:
|
||||
if f['id'] not in flav_ids:
|
||||
del_url = self.flavor_manager._create_request_url(
|
||||
f['id'], 'delete')
|
||||
fla_del = requests.delete(del_url, headers=self.headers)
|
||||
|
||||
for k in list(self.rsd_flavors):
|
||||
if k in self.rsd_flavors.keys():
|
||||
chas_list = self.rsd_flavors[k]['rsd_systems'].keys()
|
||||
for c in chas_list:
|
||||
sys_list = self.rsd_flavors[k]['rsd_systems'][c]
|
||||
sys_list = self.rsd_flavors[k]['rsd_systems'].values()
|
||||
for s in sys_list:
|
||||
if s not in sys_ids:
|
||||
try:
|
||||
rsd_id = self.rsd_flavors[k]['id']
|
||||
flavor._flavor_destroy(
|
||||
context.get_admin_context(), rsd_id)
|
||||
LOG.debug("Deleting flavor: %s", k)
|
||||
del_url = self.flavor_manager._create_request_url(
|
||||
rsd_id, 'delete')
|
||||
try:
|
||||
LOG.debug("Deleting flavor for removed systems: %s", k)
|
||||
fla_del = requests.delete(del_url,
|
||||
headers=self.headers)
|
||||
del self.rsd_flavors[k]
|
||||
except KeyError as k_ex:
|
||||
LOG.warn("Flavor doesn't exist:%s", k_ex)
|
||||
except Exception as ex:
|
||||
LOG.warn("Failed to delete flavor: %s, %s",
|
||||
json.loads(fla_del.text), ex)
|
||||
return
|
||||
|
88
rsd_virt_for_nova/virt/rsd/flavor_management.py
Normal file
88
rsd_virt_for_nova/virt/rsd/flavor_management.py
Normal file
@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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.
|
||||
"""
|
||||
A class + functions for managing RSD flavors
|
||||
|
||||
Requires the authentication to keystone to perform request to the nova-api's.
|
||||
This allows the management and creation of RSD specific flavors.
|
||||
"""
|
||||
|
||||
from rsd_virt_for_nova.conf import rsd as cfg
|
||||
|
||||
from rsd_virt_for_nova.conf.keystone_light import ClientV3
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FlavorManager(object):
|
||||
"""Implementation of nova compute driver to compose RSD nodes from nova."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the RSDDriver."""
|
||||
self._url_base = None
|
||||
self._keystone = None
|
||||
self._auth_token = None
|
||||
self.headers = None
|
||||
|
||||
def keystone_req(self):
|
||||
"""Authenticate to keystone."""
|
||||
keystone_url = ''
|
||||
OS_USERNAME = CONF.rsd.username
|
||||
OS_PASSWORD = CONF.rsd.auth_password
|
||||
OS_TENANT_NAME = CONF.rsd.tenant_name
|
||||
OS_AUTH_URL = CONF.rsd.auth_url
|
||||
OS_IDENTITY_VERSION = CONF.rsd.identity_version
|
||||
|
||||
keystone_url = OS_AUTH_URL + '/v' + str(OS_IDENTITY_VERSION)
|
||||
|
||||
self._keystone = ClientV3(
|
||||
auth_url=str(keystone_url),
|
||||
username=OS_USERNAME,
|
||||
password=OS_PASSWORD,
|
||||
tenant_name=OS_TENANT_NAME
|
||||
)
|
||||
self._auth_token = self._keystone.auth_token
|
||||
|
||||
return self._keystone
|
||||
|
||||
def _get_base_url(self):
|
||||
# get the uri of service endpoint
|
||||
endpoint = self._get_endpoint("nova")
|
||||
|
||||
self._url_base = "{}/flavors".format(endpoint)
|
||||
|
||||
return self._url_base
|
||||
|
||||
def _get_endpoint(self, service):
|
||||
# get the uri of service endpoint
|
||||
endpoint = self._keystone.get_service_endpoint(service)
|
||||
return endpoint
|
||||
|
||||
def _create_request_url(self, flavorid, req_type):
|
||||
endpoint = self._get_endpoint("nova")
|
||||
url = ''
|
||||
if req_type == 'delete':
|
||||
url = "{}/flavors/%s".format(endpoint) % (flavorid)
|
||||
elif req_type == 'update':
|
||||
url = "{}/flavors/%s/os-extra_specs".format(endpoint) % (flavorid)
|
||||
return url
|
||||
|
||||
def get_headers(self, auth_token):
|
||||
self.headers = {'X-Auth-Token': auth_token,
|
||||
'Content-type': 'application/json'}
|
||||
return self.headers
|
4
tox.ini
4
tox.ini
@ -5,7 +5,7 @@
|
||||
|
||||
[tox]
|
||||
minversion = 2.0
|
||||
envlist = py27,py36,pycodestyle
|
||||
envlist = py27,py36,pep8
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
@ -23,7 +23,7 @@ deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands = stestr run {posargs}
|
||||
|
||||
[testenv:pycodestyle]
|
||||
[testenv:pep8]
|
||||
commands =
|
||||
flake8 {posargs}
|
||||
pycodestyle {posargs}
|
||||
|
Loading…
Reference in New Issue
Block a user