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:
parent
bb75d1d06b
commit
d2d80fa9ef
@ -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')
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user