Merge "Support description for instance update/rebuild"

This commit is contained in:
Zuul 2018-04-03 12:33:28 +00:00 committed by Gerrit Code Review
commit 28251b789e
5 changed files with 192 additions and 19 deletions

View File

@ -626,9 +626,12 @@ def server_reboot(request, instance_id, soft_reboot=False):
@profiler.trace
def server_rebuild(request, instance_id, image_id, password=None,
disk_config=None):
return novaclient(request).servers.rebuild(instance_id, image_id,
password, disk_config)
disk_config=None, description=None):
kwargs = {}
if description:
kwargs['description'] = description
return get_novaclient_with_instance_desc(request).servers.rebuild(
instance_id, image_id, password, disk_config, **kwargs)
@profiler.trace

View File

@ -56,9 +56,17 @@ class RebuildInstanceForm(forms.SelfHandlingForm):
widget=forms.PasswordInput(render_value=False))
disk_config = forms.ThemableChoiceField(label=_("Disk Partition"),
required=False)
description = forms.CharField(
label=_("Description"),
widget=forms.Textarea(attrs={'rows': 4}),
max_length=255,
required=False
)
def __init__(self, request, *args, **kwargs):
super(RebuildInstanceForm, self).__init__(request, *args, **kwargs)
if not api.nova.is_feature_available(request, "instance_description"):
del self.fields['description']
instance_id = kwargs.get('initial', {}).get('instance_id')
self.fields['instance_id'].initial = instance_id
@ -105,9 +113,10 @@ class RebuildInstanceForm(forms.SelfHandlingForm):
image = data.get('image')
password = data.get('password') or None
disk_config = data.get('disk_config', None)
description = data.get('description', None)
try:
api.nova.server_rebuild(request, instance, image, password,
disk_config)
disk_config, description=description)
messages.info(request, _('Rebuilding instance %s.') % instance)
except Exception:
redirect = reverse('horizon:project:instances:index')

View File

@ -1778,7 +1778,7 @@ class InstanceTests(InstanceTestBase):
helpers.IsHttpRequest(), server.id)
instance_update_get_stubs = {
api.nova: ('server_get',),
api.nova: ('server_get', 'is_feature_available'),
api.neutron: ('security_group_list',
'server_security_groups',)}
@ -1789,6 +1789,7 @@ class InstanceTests(InstanceTestBase):
self.mock_server_get.return_value = server
self.mock_security_group_list.return_value = []
self.mock_server_security_groups.return_value = []
self.mock_is_feature_available.return_value = False
url = reverse('horizon:project:instances:update', args=[server.id])
res = self.client.get(url)
@ -1799,6 +1800,9 @@ class InstanceTests(InstanceTestBase):
helpers.IsHttpRequest(), server.id)
self.mock_security_group_list(helpers.IsHttpRequest(), tenant_id=None)
self.mock_server_security_groups(helpers.IsHttpRequest(), server.id)
self.mock_is_feature_available.assert_called_once_with(
helpers.IsHttpRequest(), "instance_description"
)
@helpers.create_mocks(instance_update_get_stubs)
def test_instance_update_get_server_get_exception(self):
@ -1825,7 +1829,7 @@ class InstanceTests(InstanceTestBase):
return self.client.post(url, formData)
instance_update_post_stubs = {
api.nova: ('server_get', 'server_update'),
api.nova: ('server_get', 'server_update', 'is_feature_available'),
api.neutron: ('security_group_list',
'server_security_groups',
'server_update_security_groups')}
@ -1839,6 +1843,7 @@ class InstanceTests(InstanceTestBase):
wanted_groups = [secgroups[1].id, secgroups[2].id]
self.mock_server_get.return_value = server
self.mock_is_feature_available.return_value = False
self.mock_security_group_list.return_value = secgroups
self.mock_server_security_groups.return_value = server_groups
self.mock_server_update.return_value = server
@ -1855,15 +1860,54 @@ class InstanceTests(InstanceTestBase):
self.mock_server_security_groups.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self.mock_server_update.assert_called_once_with(
helpers.IsHttpRequest(), server.id, server.name)
helpers.IsHttpRequest(), server.id, server.name, description=None)
self.mock_server_update_security_groups.assert_called_once_with(
helpers.IsHttpRequest(), server.id, wanted_groups)
self.mock_is_feature_available.assert_called_once_with(
helpers.IsHttpRequest(), "instance_description"
)
@helpers.create_mocks(instance_update_post_stubs)
def test_instance_update_post_with_desc(self):
server = self.servers.first()
secgroups = self.security_groups.list()[:3]
server_groups = [secgroups[0], secgroups[1]]
test_description = 'test description'
self.mock_server_get.return_value = server
self.mock_is_feature_available.return_value = True
self.mock_security_group_list.return_value = secgroups
self.mock_server_security_groups.return_value = server_groups
self.mock_server_update.return_value = server
formData = {'name': server.name,
'description': test_description}
url = reverse('horizon:project:instances:update',
args=[server.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self.mock_security_group_list.assert_called_once_with(
helpers.IsHttpRequest(), tenant_id=None)
self.mock_server_security_groups.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self.mock_server_update.assert_called_once_with(
helpers.IsHttpRequest(), server.id, server.name,
description=test_description)
self.mock_is_feature_available.assert_called_once_with(
helpers.IsHttpRequest(), "instance_description"
)
@helpers.create_mocks(instance_update_post_stubs)
def test_instance_update_post_api_exception(self):
server = self.servers.first()
self.mock_server_get.return_value = server
self.mock_is_feature_available.return_value = False
self.mock_security_group_list.return_value = []
self.mock_server_security_groups.return_value = []
self.mock_server_update.side_effect = self.exceptions.nova
@ -1879,15 +1923,19 @@ class InstanceTests(InstanceTestBase):
self.mock_server_security_groups.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self.mock_server_update.assert_called_once_with(
helpers.IsHttpRequest(), server.id, server.name)
helpers.IsHttpRequest(), server.id, server.name, description=None)
self.mock_server_update_security_groups.assert_called_once_with(
helpers.IsHttpRequest(), server.id, [])
self.mock_is_feature_available.assert_called_once_with(
helpers.IsHttpRequest(), "instance_description"
)
@helpers.create_mocks(instance_update_post_stubs)
def test_instance_update_post_secgroup_api_exception(self):
server = self.servers.first()
self.mock_server_get.return_value = server
self.mock_is_feature_available.return_value = False
self.mock_security_group_list.return_value = []
self.mock_server_security_groups.return_value = []
self.mock_server_update.return_value = server
@ -1904,9 +1952,12 @@ class InstanceTests(InstanceTestBase):
self.mock_server_security_groups.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self.mock_server_update.assert_called_once_with(
helpers.IsHttpRequest(), server.id, server.name)
helpers.IsHttpRequest(), server.id, server.name, description=None)
self.mock_server_update_security_groups.assert_called_once_with(
helpers.IsHttpRequest(), server.id, [])
self.mock_is_feature_available.assert_called_once_with(
helpers.IsHttpRequest(), "instance_description"
)
class InstanceLaunchInstanceTests(InstanceTestBase,
@ -4316,12 +4367,15 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
helpers.IsHttpRequest(), server.id, flavor.id, 'AUTO')
@helpers.create_mocks({api.glance: ('image_list_detailed',),
api.nova: ('extension_supported',
api.nova: ('server_get',
'extension_supported',
'is_feature_available',)})
def test_rebuild_instance_get(self, expect_password_fields=True):
server = self.servers.first()
self._mock_glance_image_list_detailed(self.images.list())
self.mock_extension_supported.return_value = True
self.mock_is_feature_available.return_value = False
self.mock_server_get.return_value = server
url = reverse('horizon:project:instances:rebuild', args=[server.id])
res = self.client.get(url)
@ -4334,9 +4388,14 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
else:
self.assertNotContains(res, password_field_label)
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_is_feature_available.assert_called_once_with(
helpers.IsHttpRequest(), "instance_description"
)
@django.test.utils.override_settings(
OPENSTACK_HYPERVISOR_FEATURES={'can_set_password': False})
@ -4358,7 +4417,8 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
return self.client.post(url, form_data)
instance_rebuild_post_stubs = {
api.nova: ('server_rebuild',
api.nova: ('server_get',
'server_rebuild',
'extension_supported',
'is_feature_available',),
api.glance: ('image_list_detailed',)}
@ -4369,9 +4429,11 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
image = self.images.first()
password = u'testpass'
self.mock_server_get.return_value = server
self._mock_glance_image_list_detailed(self.images.list())
self.mock_extension_supported.return_value = True
self.mock_server_rebuild.return_value = []
self.mock_is_feature_available.return_value = False
res = self._instance_rebuild_post(server.id, image.id,
password=password,
@ -4380,20 +4442,28 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_server_rebuild.assert_called_once_with(
helpers.IsHttpRequest(), server.id, image.id, password, 'AUTO')
helpers.IsHttpRequest(), server.id, image.id, password, 'AUTO',
description=None)
self.mock_is_feature_available.assert_called_once_with(
helpers.IsHttpRequest(), "instance_description"
)
@helpers.create_mocks(instance_rebuild_post_stubs)
def test_rebuild_instance_post_with_password_equals_none(self):
server = self.servers.first()
image = self.images.first()
self.mock_server_get.return_value = server
self._mock_glance_image_list_detailed(self.images.list())
self.mock_extension_supported.return_value = True
self.mock_server_rebuild.side_effect = self.exceptions.nova
self.mock_is_feature_available.return_value = False
res = self._instance_rebuild_post(server.id, image.id,
password=None,
@ -4401,11 +4471,17 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
disk_config='AUTO')
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_server_rebuild.assert_called_once_with(
helpers.IsHttpRequest(), server.id, image.id, None, 'AUTO')
helpers.IsHttpRequest(), server.id, image.id, None, 'AUTO',
description=None)
self.mock_is_feature_available.assert_called_once_with(
helpers.IsHttpRequest(), "instance_description"
)
@helpers.create_mocks(instance_rebuild_post_stubs)
def test_rebuild_instance_post_password_do_not_match(self):
@ -4414,8 +4490,10 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
pass1 = u'somepass'
pass2 = u'notsomepass'
self.mock_server_get.return_value = server
self._mock_glance_image_list_detailed(self.images.list())
self.mock_extension_supported.return_value = True
self.mock_is_feature_available.return_value = False
res = self._instance_rebuild_post(server.id, image.id,
password=pass1,
@ -4431,19 +4509,26 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
else:
image_list_count = 3
ext_count = 1
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=image_list_count)
self.assert_mock_multiple_calls_with_same_arguments(
self.mock_extension_supported, ext_count,
mock.call('DiskConfig', helpers.IsHttpRequest()))
self.assert_mock_multiple_calls_with_same_arguments(
self.mock_is_feature_available, 2,
mock.call(helpers.IsHttpRequest(), 'instance_description'))
@helpers.create_mocks(instance_rebuild_post_stubs)
def test_rebuild_instance_post_with_empty_string(self):
server = self.servers.first()
image = self.images.first()
self.mock_server_get.return_value = server
self._mock_glance_image_list_detailed(self.images.list())
self.mock_extension_supported.return_value = True
self.mock_server_rebuild.return_value = []
self.mock_is_feature_available.return_value = False
res = self._instance_rebuild_post(server.id, image.id,
password=u'',
@ -4452,11 +4537,50 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_server_rebuild.assert_called_once_with(
helpers.IsHttpRequest(), server.id, image.id, None, 'AUTO')
helpers.IsHttpRequest(), server.id, image.id, None, 'AUTO',
description=None)
self.mock_is_feature_available.assert_called_once_with(
helpers.IsHttpRequest(), "instance_description"
)
@helpers.create_mocks(instance_rebuild_post_stubs)
def test_rebuild_instance_post_with_desc(self):
server = self.servers.first()
image = self.images.first()
test_description = 'test description'
self.mock_server_get.return_value = server
self._mock_glance_image_list_detailed(self.images.list())
self.mock_extension_supported.return_value = True
self.mock_server_rebuild.return_value = []
self.mock_is_feature_available.return_value = True
form_data = {'instance_id': server.id,
'image': image.id,
'description': test_description}
url = reverse('horizon:project:instances:rebuild',
args=[server.id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_server_rebuild.assert_called_once_with(
helpers.IsHttpRequest(), server.id, image.id, None, '',
description=test_description)
self.mock_is_feature_available.assert_called_once_with(
helpers.IsHttpRequest(), "instance_description"
)
@helpers.create_mocks(instance_rebuild_post_stubs)
def test_rebuild_instance_post_api_exception(self):
@ -4464,9 +4588,11 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
image = self.images.first()
password = u'testpass'
self.mock_server_get.return_value = server
self._mock_glance_image_list_detailed(self.images.list())
self.mock_extension_supported.return_value = True
self.mock_server_rebuild.side_effect = self.exceptions.nova
self.mock_is_feature_available.return_value = False
res = self._instance_rebuild_post(server.id, image.id,
password=password,
@ -4474,11 +4600,17 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
disk_config='AUTO')
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_server_rebuild.assert_called_once_with(
helpers.IsHttpRequest(), server.id, image.id, password, 'AUTO')
helpers.IsHttpRequest(), server.id, image.id, password, 'AUTO',
description=None)
self.mock_is_feature_available.assert_called_once_with(
helpers.IsHttpRequest(), "instance_description"
)
@django.test.utils.override_settings(API_RESULT_PAGE_SIZE=2)
@helpers.create_mocks({

View File

@ -334,8 +334,10 @@ class UpdateView(workflows.WorkflowView):
def get_initial(self):
initial = super(UpdateView, self).get_initial()
instance = self.get_object()
initial.update({'instance_id': self.kwargs['instance_id'],
'name': getattr(self.get_object(), 'name', '')})
'name': getattr(instance, 'name', ''),
'description': getattr(instance, 'description', '')})
return initial
@ -352,8 +354,21 @@ class RebuildView(forms.ModalFormView):
context['can_set_server_password'] = api.nova.can_set_server_password()
return context
@memoized.memoized_method
def get_object(self, *args, **kwargs):
instance_id = self.kwargs['instance_id']
try:
return api.nova.server_get(self.request, instance_id)
except Exception:
redirect = reverse("horizon:project:instances:index")
msg = _('Unable to retrieve instance details.')
exceptions.handle(self.request, msg, redirect=redirect)
def get_initial(self):
return {'instance_id': self.kwargs['instance_id']}
instance = self.get_object()
initial = {'instance_id': self.kwargs['instance_id'],
'description': getattr(instance, 'description', '')}
return initial
class DecryptPasswordView(forms.ModalFormView):

View File

@ -128,12 +128,26 @@ class UpdateInstanceSecurityGroups(BaseSecurityGroups):
class UpdateInstanceInfoAction(workflows.Action):
name = forms.CharField(label=_("Name"),
max_length=255)
description = forms.CharField(
label=_("Description"),
widget=forms.Textarea(attrs={'rows': 4}),
max_length=255,
required=False
)
def __init__(self, request, *args, **kwargs):
super(UpdateInstanceInfoAction, self).__init__(request,
*args,
**kwargs)
if not api.nova.is_feature_available(request, "instance_description"):
del self.fields["description"]
def handle(self, request, data):
try:
api.nova.server_update(request,
data['instance_id'],
data['name'])
data['name'],
description=data.get('description'))
except Exception:
exceptions.handle(request, ignore=True)
return False
@ -148,7 +162,7 @@ class UpdateInstanceInfoAction(workflows.Action):
class UpdateInstanceInfo(workflows.Step):
action_class = UpdateInstanceInfoAction
depends_on = ("instance_id",)
contributes = ("name",)
contributes = ("name", "description")
class UpdateInstance(workflows.Workflow):