Resizing a server by means of changing its flavor
Sometime we need resize the server when it is launched, such as the number of vcpu, the size of memory and the size of disk. We can achieve this by means of changing its flavor. Change-Id: I1ab6b61f286e951b644a2e66383ac62c6a6f887e Implements: blueprint resize-server
This commit is contained in:
parent
c5f968afee
commit
8770b32dfa
@ -463,6 +463,10 @@ def server_migrate(request, instance_id):
|
||||
novaclient(request).servers.migrate(instance_id)
|
||||
|
||||
|
||||
def server_resize(request, instance_id, flavor, **kwargs):
|
||||
novaclient(request).servers.resize(instance_id, flavor, **kwargs)
|
||||
|
||||
|
||||
def server_confirm_resize(request, instance_id):
|
||||
novaclient(request).servers.confirm_resize(instance_id)
|
||||
|
||||
|
@ -272,6 +272,26 @@ class LogLink(tables.LinkAction):
|
||||
return "?".join([base_url, tab_query_string])
|
||||
|
||||
|
||||
class ResizeLink(tables.LinkAction):
|
||||
name = "resize"
|
||||
verbose_name = _("Resize Instance")
|
||||
url = "horizon:project:instances:resize"
|
||||
classes = ("ajax-modal", "btn-resize")
|
||||
|
||||
def get_link_url(self, project):
|
||||
return self._get_link_url(project, 'flavor_choice')
|
||||
|
||||
def _get_link_url(self, project, step_slug):
|
||||
base_url = urlresolvers.reverse(self.url, args=[project.id])
|
||||
param = urlencode({"step": step_slug})
|
||||
return "?".join([base_url, param])
|
||||
|
||||
def allowed(self, request, instance):
|
||||
return ((instance.status in ACTIVE_STATES
|
||||
or instance.status == 'SHUTOFF')
|
||||
and not is_deleting(instance))
|
||||
|
||||
|
||||
class ConfirmResize(tables.Action):
|
||||
name = "confirm"
|
||||
verbose_name = _("Confirm Resize/Migrate")
|
||||
@ -498,5 +518,5 @@ class InstancesTable(tables.DataTable):
|
||||
SimpleAssociateIP, AssociateIP,
|
||||
SimpleDisassociateIP, EditInstance,
|
||||
EditInstanceSecurityGroups, ConsoleLink, LogLink,
|
||||
TogglePause, ToggleSuspend, SoftRebootInstance,
|
||||
RebootInstance, TerminateInstance)
|
||||
TogglePause, ToggleSuspend, ResizeLink,
|
||||
SoftRebootInstance, RebootInstance, TerminateInstance)
|
||||
|
47
openstack_dashboard/dashboards/project/instances/templates/instances/_resize_instance_help.html
Normal file
47
openstack_dashboard/dashboards/project/instances/templates/instances/_resize_instance_help.html
Normal file
@ -0,0 +1,47 @@
|
||||
{% load i18n horizon humanize %}
|
||||
|
||||
<h4>{% trans "Flavor Details" %}</h4>
|
||||
<table class="flavor_table table-striped">
|
||||
<tbody>
|
||||
<tr><td class="flavor_name">{% trans "Name" %}</td><td><span id="flavor_name"></span></td></tr>
|
||||
<tr><td class="flavor_name">{% trans "VCPUs" %}</td><td><span id="flavor_vcpus"></span></td></tr>
|
||||
<tr><td class="flavor_name">{% trans "Root Disk" %}</td><td><span id="flavor_disk"> </span> {% trans "GB" %}</td></tr>
|
||||
<tr><td class="flavor_name">{% trans "Ephemeral Disk" %}</td><td><span id="flavor_ephemeral"></span> {% trans "GB" %}</td></tr>
|
||||
<tr><td class="flavor_name">{% trans "Total Disk" %}</td><td><span id="flavor_disk_total"></span> {% trans "GB" %}</td></tr>
|
||||
<tr><td class="flavor_name">{% trans "RAM" %}</td><td><span id="flavor_ram"></span> {% trans "MB" %}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="quota-dynamic">
|
||||
<h4>{% trans "Project Quotas" %}</h4>
|
||||
<div class="quota_title clearfix">
|
||||
<strong>{% trans "Number of Instances" %} <span>({{ usages.instances.used|intcomma }})</span></strong>
|
||||
<p>{{ usages.instances.available|quota|intcomma }}</p>
|
||||
</div>
|
||||
<div id="quota_instances" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.instances.quota }}" data-quota-used="{{ usages.instances.used }}">
|
||||
</div>
|
||||
|
||||
<div class="quota_title clearfix">
|
||||
<strong>{% trans "Number of VCPUs" %} <span>({{ usages.cores.used|intcomma }})</span></strong>
|
||||
<p>{{ usages.cores.available|quota|intcomma }}</p>
|
||||
</div>
|
||||
<div id="quota_vcpus" class="quota_bar" data-progress-indicator-flavor data-quota-limit="{{ usages.cores.quota }}" data-quota-used="{{ usages.cores.used }}">
|
||||
</div>
|
||||
|
||||
<div class="quota_title clearfix">
|
||||
<strong>{% trans "Total RAM" %} <span>({{ usages.ram.used|intcomma }} {% trans "MB" %})</span></strong>
|
||||
<p>{{ usages.ram.available|quota:_("MB")|intcomma }}</p>
|
||||
</div>
|
||||
<div id="quota_ram" data-progress-indicator-flavor data-quota-limit="{{ usages.ram.quota }}" data-quota-used="{{ usages.ram.used }}" class="quota_bar">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
if(typeof horizon.Quota !== 'undefined') {
|
||||
horizon.Quota.initWithFlavors({{ flavors|safe|default:"{}" }});
|
||||
} else {
|
||||
addHorizonLoadEvent(function() {
|
||||
horizon.Quota.initWithFlavors({{ flavors|safe|default:"{}" }});
|
||||
});
|
||||
}
|
||||
</script>
|
@ -1578,3 +1578,105 @@ class InstanceTests(test.TestCase):
|
||||
res = self.client.post(INDEX_URL, formData)
|
||||
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({api.nova: ('server_get',
|
||||
'flavor_list',),
|
||||
quotas: ('tenant_quota_usages',)})
|
||||
def test_instance_resize_get(self):
|
||||
server = self.servers.first()
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id) \
|
||||
.AndReturn(server)
|
||||
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.flavors.list())
|
||||
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.flavors.list())
|
||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.quota_usages.first())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:instances:resize', args=[server.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertTemplateUsed(res, WorkflowView.template_name)
|
||||
|
||||
@test.create_stubs({api.nova: ('server_get',
|
||||
'flavor_list',)})
|
||||
def test_instance_resize_get_server_get_exception(self):
|
||||
server = self.servers.first()
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id) \
|
||||
.AndRaise(self.exceptions.nova)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:instances:resize',
|
||||
args=[server.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({api.nova: ('server_get',
|
||||
'flavor_list',)})
|
||||
def test_instance_resize_get_flavor_list_exception(self):
|
||||
server = self.servers.first()
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id) \
|
||||
.AndReturn(server)
|
||||
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
||||
.AndRaise(self.exceptions.nova)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:instances:resize',
|
||||
args=[server.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
def _instance_resize_post(self, server_id, flavor_id):
|
||||
formData = {'flavor': flavor_id,
|
||||
'default_role': 'member'}
|
||||
url = reverse('horizon:project:instances:resize',
|
||||
args=[server_id])
|
||||
return self.client.post(url, formData)
|
||||
|
||||
instance_resize_post_stubs = {
|
||||
api.nova: ('server_get', 'server_resize',
|
||||
'flavor_list', 'flavor_get')}
|
||||
|
||||
@test.create_stubs(instance_resize_post_stubs)
|
||||
def test_instance_resize_post(self):
|
||||
server = self.servers.first()
|
||||
flavor = self.flavors.first()
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id) \
|
||||
.AndReturn(server)
|
||||
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.flavors.list())
|
||||
api.nova.server_resize(IsA(http.HttpRequest), server.id, flavor.id) \
|
||||
.AndReturn([])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self._instance_resize_post(server.id, flavor.id)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs(instance_resize_post_stubs)
|
||||
def test_instance_resize_post_api_exception(self):
|
||||
server = self.servers.first()
|
||||
flavor = self.flavors.first()
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id) \
|
||||
.AndReturn(server)
|
||||
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.flavors.list())
|
||||
api.nova.server_resize(IsA(http.HttpRequest), server.id, flavor.id) \
|
||||
.AndRaise(self.exceptions.nova)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self._instance_resize_post(server.id, flavor.id)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
@ -21,6 +21,7 @@
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
from .views import IndexView, UpdateView, DetailView, LaunchInstanceView
|
||||
from .views import ResizeView
|
||||
|
||||
|
||||
INSTANCES = r'^(?P<instance_id>[^/]+)/%s$'
|
||||
@ -35,4 +36,5 @@ urlpatterns = patterns(VIEW_MOD,
|
||||
url(INSTANCES % 'console', 'console', name='console'),
|
||||
url(INSTANCES % 'vnc', 'vnc', name='vnc'),
|
||||
url(INSTANCES % 'spice', 'spice', name='spice'),
|
||||
url(INSTANCES % 'resize', ResizeView.as_view(), name='resize'),
|
||||
)
|
||||
|
@ -38,7 +38,7 @@ from horizon import workflows
|
||||
from openstack_dashboard import api
|
||||
from .tabs import InstanceDetailTabs
|
||||
from .tables import InstancesTable
|
||||
from .workflows import LaunchInstance, UpdateInstance
|
||||
from .workflows import LaunchInstance, UpdateInstance, ResizeInstance
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -203,3 +203,53 @@ class DetailView(tabs.TabView):
|
||||
def get_tabs(self, request, *args, **kwargs):
|
||||
instance = self.get_data()
|
||||
return self.tab_group_class(request, instance=instance, **kwargs)
|
||||
|
||||
|
||||
class ResizeView(workflows.WorkflowView):
|
||||
workflow_class = ResizeInstance
|
||||
success_url = reverse_lazy("horizon:project:instances:index")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ResizeView, self).get_context_data(**kwargs)
|
||||
context["instance_id"] = self.kwargs['instance_id']
|
||||
return context
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
if not hasattr(self, "_object"):
|
||||
instance_id = self.kwargs['instance_id']
|
||||
try:
|
||||
self._object = api.nova.server_get(self.request, instance_id)
|
||||
flavor_id = self._object.flavor['id']
|
||||
flavors = self.get_flavors()
|
||||
if flavor_id in flavors:
|
||||
self._object.flavor_name = flavors[flavor_id].name
|
||||
else:
|
||||
flavor = api.nova.flavor_get(self.request, flavor_id)
|
||||
self._object.flavor_name = flavor.name
|
||||
except:
|
||||
redirect = reverse("horizon:project:instances:index")
|
||||
msg = _('Unable to retrieve instance details.')
|
||||
exceptions.handle(self.request, msg, redirect=redirect)
|
||||
return self._object
|
||||
|
||||
def get_flavors(self, *args, **kwargs):
|
||||
if not hasattr(self, "_flavors"):
|
||||
try:
|
||||
flavors = api.nova.flavor_list(self.request)
|
||||
self._flavors = SortedDict([(str(flavor.id), flavor)
|
||||
for flavor in flavors])
|
||||
except:
|
||||
redirect = reverse("horizon:project:instances:index")
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve flavors.'), redirect=redirect)
|
||||
return self._flavors
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(ResizeView, self).get_initial()
|
||||
_object = self.get_object()
|
||||
if _object:
|
||||
initial.update({'instance_id': self.kwargs['instance_id'],
|
||||
'old_flavor_id': _object.flavor['id'],
|
||||
'old_flavor_name': getattr(_object, 'flavor_name', ''),
|
||||
'flavors': self.get_flavors()})
|
||||
return initial
|
||||
|
@ -1,2 +1,3 @@
|
||||
from create_instance import *
|
||||
from update_instance import *
|
||||
from resize_instance import *
|
||||
|
@ -0,0 +1,113 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 CentRin Data, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
import logging
|
||||
import json
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.views.decorators.debug import sensitive_variables
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import workflows
|
||||
from horizon import forms
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.usage import quotas
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SetFlavorChoiceAction(workflows.Action):
|
||||
old_flavor_id = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
old_flavor_name = forms.CharField(label=_("Old Flavor"),
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={'readonly': 'readonly'}
|
||||
))
|
||||
flavor = forms.ChoiceField(label=_("New Flavor"),
|
||||
required=True,
|
||||
help_text=_("Choose the flavor to launch."))
|
||||
|
||||
class Meta:
|
||||
name = _("Flavor Choice")
|
||||
slug = 'flavor_choice'
|
||||
help_text_template = ("project/instances/"
|
||||
"_resize_instance_help.html")
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(SetFlavorChoiceAction, self).clean()
|
||||
flavor = cleaned_data.get('flavor', None)
|
||||
|
||||
if flavor is None or flavor == cleaned_data['old_flavor_id']:
|
||||
raise forms.ValidationError(_('Please choose a new flavor that '
|
||||
'can not be same as the old one.'))
|
||||
return cleaned_data
|
||||
|
||||
def populate_flavor_choices(self, request, context):
|
||||
flavors = context.get('flavors')
|
||||
flavor_list = [(flavor.id, '%s' % flavor.name)
|
||||
for flavor in flavors.values()]
|
||||
if flavor_list:
|
||||
flavor_list.insert(0, ("", _("Select an New Flavor")))
|
||||
else:
|
||||
flavor_list.insert(0, ("", _("No flavors available.")))
|
||||
return sorted(flavor_list)
|
||||
|
||||
def get_help_text(self):
|
||||
extra = {}
|
||||
try:
|
||||
extra['usages'] = quotas.tenant_quota_usages(self.request)
|
||||
extra['usages_json'] = json.dumps(extra['usages'])
|
||||
flavors = json.dumps([f._info for f in
|
||||
api.nova.flavor_list(self.request)])
|
||||
extra['flavors'] = flavors
|
||||
except:
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve quota information."))
|
||||
return super(SetFlavorChoiceAction, self).get_help_text(extra)
|
||||
|
||||
|
||||
class SetFlavorChoice(workflows.Step):
|
||||
action_class = SetFlavorChoiceAction
|
||||
depends_on = ("instance_id",)
|
||||
contributes = ("old_flavor_id", "old_flavor_name", "flavors", "flavor")
|
||||
|
||||
|
||||
class ResizeInstance(workflows.Workflow):
|
||||
slug = "resize_instance"
|
||||
name = _("Resize Instance")
|
||||
finalize_button_name = _("Resize")
|
||||
success_message = _('Resized instance "%s".')
|
||||
failure_message = _('Unable to resize instance "%s".')
|
||||
success_url = "horizon:project:instances:index"
|
||||
default_steps = (SetFlavorChoice,)
|
||||
|
||||
def format_status_message(self, message):
|
||||
return message % self.context.get('name', 'unknown instance')
|
||||
|
||||
@sensitive_variables('context')
|
||||
def handle(self, request, context):
|
||||
instance_id = context.get('instance_id', None)
|
||||
flavor = context.get('flavor', None)
|
||||
try:
|
||||
api.nova.server_resize(request, instance_id, flavor)
|
||||
return True
|
||||
except:
|
||||
exceptions.handle(request)
|
||||
return False
|
Loading…
x
Reference in New Issue
Block a user