Merge "Support description for instance update/rebuild"
This commit is contained in:
commit
28251b789e
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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({
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user