Manage image ID's for node profile

Kernel and ramdisk image ID's are stored for each node profile
in nova keys and should be managed via UI.

Only image ID is displayed in node profile list. Though
somewhat unfriendly, this is better for now than making
2 calls to Glance for every row. Separate node details page
will be introduced in the next patch to fix this issue.

Also this patch introduce abstraction over flavor to ease
writing code specific to node profiles.

Change-Id: Ie76651536e2d8b90e14c9000da0226743db91e88
This commit is contained in:
Dmitry Tantsur 2014-02-24 05:02:09 -05:00
parent bb75d1d06b
commit d2d80fa9ef
5 changed files with 164 additions and 56 deletions

View File

@ -85,6 +85,58 @@ def image_get(request, image_id):
return image
class NodeProfile(object):
def __init__(self, flavor):
"""Construct node profile by wrapping flavor
:param flavor: Nova flavor
:type flavor: novaclient.v1_1.flavors.Flavor
"""
self._flavor = flavor
def __getattr__(self, name):
return getattr(self._flavor, name)
@cached_property
def extras_dict(self):
"""Return extra parameters of node profile
:return: Nova flavor keys
:rtype: dict
"""
return self._flavor.get_keys()
@property
def cpu_arch(self):
return self.extras_dict.get('cpu_arch', '')
@property
def kernel_image_id(self):
return self.extras_dict.get('baremetal:deploy_kernel_id', '')
@property
def ramdisk_image_id(self):
return self.extras_dict.get('baremetal:deploy_ramdisk_id', '')
@classmethod
def create(cls, request, name, memory, vcpus, disk, cpu_arch,
kernel_image_id, ramdisk_image_id):
extras_dict = {'cpu_arch': cpu_arch,
'baremetal:deploy_kernel_id': kernel_image_id,
'baremetal:deploy_ramdisk_id': ramdisk_image_id}
return cls(nova.flavor_create(request, name, memory, vcpus, disk,
metadata=extras_dict))
@classmethod
def get(cls, request, node_profile_id):
return cls(nova.flavor_get(request, node_profile_id))
@classmethod
def list(cls, request):
return [cls(item) for item in nova.flavor_list(request)]
class Overcloud(base.APIResourceWrapper):
_attrs = ('id', 'stack_id', 'name', 'description', 'counts', 'attributes')

View File

@ -35,14 +35,9 @@ class DeleteNodeProfile(flavor_tables.DeleteFlavor):
self.data_type_plural = _("Node Profiles")
def get_arch(flavor):
extra_specs = flavor.get_keys()
return extra_specs.get('cpu_arch', '')
class NodeProfilesTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Node'))
arch = tables.Column(get_arch, verbose_name=_('Architecture'))
arch = tables.Column('cpu_arch', verbose_name=_('Architecture'))
vcpus = tables.Column('vcpus', verbose_name=_('CPUs'))
ram = tables.Column(flavor_tables.get_size,
verbose_name=_('Memory'),
@ -50,6 +45,11 @@ class NodeProfilesTable(tables.DataTable):
disk = tables.Column(flavor_tables.get_disk_size,
verbose_name=_('Disk'),
attrs={'data-type': 'size'})
# FIXME(dtantsur): would be much better to have names here
kernel_image_id = tables.Column('kernel_image_id',
verbose_name=_('Deploy Kernel Image ID'))
ramdisk_image_id = tables.Column('ramdisk_image_id',
verbose_name=_('Deploy Ramdisk Image ID'))
class Meta:
name = "node_profiles"

View File

@ -18,6 +18,7 @@ from django.core import urlresolvers
from mock import patch, call # noqa
from horizon import exceptions
from openstack_dashboard.test.test_data import utils
from tuskar_ui.test import helpers as test
from tuskar_ui.test.test_data import tuskar_data
@ -34,59 +35,75 @@ CREATE_URL = urlresolvers.reverse(
@contextlib.contextmanager
def _prepare_create():
flavor = TEST_DATA.novaclient_flavors.first()
all_flavors = TEST_DATA.novaclient_flavors.list()
images = TEST_DATA.glanceclient_images.list()
data = {'name': 'foobar',
'vcpus': 3,
'memory_mb': 1024,
'disk_gb': 40,
'arch': 'amd64'}
with patch('openstack_dashboard.api.nova', **{
'spec_set': ['flavor_create', 'flavor_list', 'flavor_get_extras',
'flavor_extra_set'],
'flavor_create.return_value': flavor,
'flavor_list.return_value': TEST_DATA.novaclient_flavors.list(),
'flavor_get_extras.return_value': {},
}) as mock:
yield mock, data
'arch': 'amd64',
'kernel_image_id': images[0].id,
'ramdisk_image_id': images[1].id}
with contextlib.nested(
patch('tuskar_ui.api.NodeProfile.create',
return_value=flavor),
patch('openstack_dashboard.api.glance.image_list_detailed',
return_value=(TEST_DATA.glanceclient_images.list(), False)),
# Inherited code calls this directly
patch('openstack_dashboard.api.nova.flavor_list',
return_value=all_flavors),
) as mocks:
yield mocks[0], data
class NodeProfilesTest(test.BaseAdminViewTests):
def test_index(self):
with patch('openstack_dashboard.api.nova', **{
'spec_set': ['flavor_list'],
'flavor_list.return_value':
TEST_DATA.novaclient_flavors.list(),
}) as mock:
with patch('openstack_dashboard.api.nova.flavor_list',
return_value=TEST_DATA.novaclient_flavors.list()) as mock:
res = self.client.get(INDEX_URL)
self.assertEqual(mock.flavor_list.call_count, 1)
self.assertEqual(mock.call_count, 1)
self.assertTemplateUsed(res,
'infrastructure/node_profiles/index.html')
def test_index_recoverable_failure(self):
with patch('openstack_dashboard.api.nova.flavor_list',
side_effect=exceptions.Conflict):
self.client.get(INDEX_URL)
# FIXME(dtantsur): I expected the following to work:
# self.assertMessageCount(error=1, warning=0)
def test_create_get(self):
res = self.client.get(CREATE_URL)
with patch('openstack_dashboard.api.glance.image_list_detailed',
return_value=([], False)) as mock:
res = self.client.get(CREATE_URL)
self.assertEqual(mock.call_count, 2)
self.assertTemplateUsed(res,
'infrastructure/node_profiles/create.html')
def test_create_get_recoverable_failure(self):
with patch('openstack_dashboard.api.glance.image_list_detailed',
side_effect=exceptions.Conflict):
self.client.get(CREATE_URL)
self.assertMessageCount(error=1, warning=0)
def test_create_post_ok(self):
flavor = TEST_DATA.novaclient_flavors.first()
with _prepare_create() as (nova_mock, data):
images = TEST_DATA.glanceclient_images.list()
with _prepare_create() as (create_mock, data):
res = self.client.post(CREATE_URL, data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
request = nova_mock.flavor_create.call_args_list[0][0][0]
self.assertListEqual(nova_mock.flavor_create.call_args_list, [
call(request, name=u'foobar', memory=1024, vcpu=3, disk=40,
flavorid='auto', ephemeral=0, swap=0, is_public=True)
])
self.assertEqual(nova_mock.flavor_list.call_count, 1)
self.assertListEqual(nova_mock.flavor_extra_set.call_args_list, [
call(request, flavor.id, {'cpu_arch': 'amd64'}),
request = create_mock.call_args_list[0][0][0]
self.assertListEqual(create_mock.call_args_list, [
call(request, name=u'foobar', memory=1024, vcpus=3, disk=40,
cpu_arch='amd64', kernel_image_id=images[0].id,
ramdisk_image_id=images[1].id)
])
def test_create_post_name_exists(self):
flavor = TEST_DATA.novaclient_flavors.first()
with _prepare_create() as (nova_mock, data):
with _prepare_create() as (create_mock, data):
data['name'] = flavor.name
res = self.client.post(CREATE_URL, data)
self.assertFormErrors(res)

View File

@ -12,18 +12,34 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack_dashboard.dashboards.admin.flavors \
import views as flavor_views
from django.utils.translation import ugettext_lazy as _
from tuskar_ui.infrastructure.node_profiles import tables
from tuskar_ui.infrastructure.node_profiles import workflows
from horizon import exceptions
from horizon import tables
from horizon import workflows
from tuskar_ui import api
from tuskar_ui.infrastructure.node_profiles \
import tables as node_profiles_tables
from tuskar_ui.infrastructure.node_profiles \
import workflows as node_profiles_workflows
class IndexView(flavor_views.IndexView):
table_class = tables.NodeProfilesTable
class IndexView(tables.DataTableView):
table_class = node_profiles_tables.NodeProfilesTable
template_name = 'infrastructure/node_profiles/index.html'
def get_data(self):
try:
node_profiles = api.NodeProfile.list(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve node profile list.'))
return []
node_profiles.sort(key=lambda np: (np.vcpus, np.ram, np.disk))
return node_profiles
class CreateView(flavor_views.CreateView):
workflow_class = workflows.CreateNodeProfile
class CreateView(workflows.WorkflowView):
workflow_class = node_profiles_workflows.CreateNodeProfile
template_name = 'infrastructure/node_profiles/create.html'

View File

@ -18,17 +18,40 @@ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.api import glance
from openstack_dashboard.dashboards.admin.flavors \
import workflows as flavor_workflows
from tuskar_ui import api
class CreateNodeProfileAction(flavor_workflows.CreateFlavorInfoAction):
arch = fields.ChoiceField(choices=(('i386', 'i386'), ('amd64', 'amd64')),
label=_("Architecture"))
kernel_image_id = fields.ChoiceField(choices=(),
label=_("Deploy Kernel Image"))
ramdisk_image_id = fields.ChoiceField(choices=(),
label=_("Deploy Ramdisk Image"))
def __init__(self, *args, **kwrds):
super(CreateNodeProfileAction, self).__init__(*args, **kwrds)
try:
kernel_images = glance.image_list_detailed(
self.request,
filters={'disk_format': 'aki'}
)[0]
ramdisk_images = glance.image_list_detailed(
self.request,
filters={'disk_format': 'ari'}
)[0]
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve images list.'))
kernel_images = []
ramdisk_images = []
self.fields['kernel_image_id'].choices = [(img.id, img.name)
for img in kernel_images]
self.fields['ramdisk_image_id'].choices = [(img.id, img.name)
for img in ramdisk_images]
# Delete what is not applicable to hardware
del self.fields['eph_gb']
del self.fields['swap_mb']
@ -51,7 +74,9 @@ class CreateNodeProfileStep(workflows.Step):
"vcpus",
"memory_mb",
"disk_gb",
"arch")
"arch",
"kernel_image_id",
"ramdisk_image_id")
class CreateNodeProfile(flavor_workflows.CreateFlavor):
@ -64,20 +89,18 @@ class CreateNodeProfile(flavor_workflows.CreateFlavor):
default_steps = (CreateNodeProfileStep,)
def handle(self, request, data):
data = dict(data, flavor_access=(), eph_gb=0, swap_mb=0,
flavor_id='auto')
if not super(CreateNodeProfile, self).handle(request, data):
return False
try:
extras_dict = api.nova.flavor_get_extras(request,
self.object.id,
raw=True) or {}
extras_dict['cpu_arch'] = data['arch']
api.nova.flavor_extra_set(request,
self.object.id,
extras_dict)
self.object = api.NodeProfile.create(
request,
name=data['name'],
memory=data['memory_mb'],
vcpus=data['vcpus'],
disk=data['disk_gb'],
cpu_arch=data['arch'],
kernel_image_id=data['kernel_image_id'],
ramdisk_image_id=data['ramdisk_image_id']
)
except Exception:
exceptions.handle(request, ignore=True)
exceptions.handle(request, _("Unable to create node profile"))
return False
return True