diff --git a/circle/circle/settings/base.py b/circle/circle/settings/base.py index e029355..09619c8 100644 --- a/circle/circle/settings/base.py +++ b/circle/circle/settings/base.py @@ -368,9 +368,9 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': from shutilwhich import which from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT - # INSTALLED_APPS += ( # needed only for testing djangosaml2 - # 'djangosaml', - # ) + INSTALLED_APPS += ( + 'djangosaml2', + ) AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'djangosaml2.backends.Saml2Backend', diff --git a/circle/dashboard/forms.py b/circle/dashboard/forms.py index c9fcf44..e4f7228 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -634,12 +634,8 @@ class LeaseForm(forms.ModelForm): Field('name'), Field("suspend_interval_seconds", type="hidden", value="0"), Field("delete_interval_seconds", type="hidden", value="0"), + HTML(string_concat("<label>", _("Suspend in"), "</label>")), Div( - Div( - HTML(_("Suspend in")), - css_class="input-group-addon", - style="width: 100px;", - ), NumberField("suspend_hours", css_class="form-control"), Div( HTML(_("hours")), @@ -662,12 +658,8 @@ class LeaseForm(forms.ModelForm): ), css_class="input-group interval-input", ), + HTML(string_concat("<label>", _("Delete in"), "</label>")), Div( - Div( - HTML(_("Delete in")), - css_class="input-group-addon", - style="width: 100px;", - ), NumberField("delete_hours", css_class="form-control"), Div( HTML(_("hours")), @@ -691,7 +683,7 @@ class LeaseForm(forms.ModelForm): css_class="input-group interval-input", ) ) - helper.add_input(Submit("submit", "Save changes")) + helper.add_input(Submit("submit", _("Save changes"))) return helper class Meta: @@ -703,6 +695,8 @@ class VmRenewForm(forms.Form): force = forms.BooleanField(required=False, label=_( "Set expiration times even if they are shorter than " "the current value.")) + save = forms.BooleanField(required=False, label=_( + "Save selected lease.")) def __init__(self, *args, **kwargs): choices = kwargs.pop('choices') @@ -714,6 +708,32 @@ class VmRenewForm(forms.Form): empty_label=None, label=_('Length'))) if len(choices) < 2: self.fields['lease'].widget = HiddenInput() + self.fields['save'].widget = HiddenInput() + + @property + def helper(self): + helper = FormHelper(self) + helper.form_tag = False + return helper + + +class VmStateChangeForm(forms.Form): + + interrupt = forms.BooleanField(required=False, label=_( + "Forcibly interrupt all running activities."), + help_text=_("Set all activities to finished state, " + "but don't interrupt any tasks.")) + new_state = forms.ChoiceField(Instance.STATUS, label=_( + "New status")) + + def __init__(self, *args, **kwargs): + show_interrupt = kwargs.pop('show_interrupt') + status = kwargs.pop('status') + super(VmStateChangeForm, self).__init__(*args, **kwargs) + + if not show_interrupt: + self.fields['interrupt'].widget = HiddenInput() + self.fields['new_state'].initial = status @property def helper(self): @@ -1141,9 +1161,9 @@ class VmResourcesForm(forms.ModelForm): vm_search_choices = ( - (0, _("owned")), - (1, _("shared")), - (2, _("all")), + ("owned", _("owned")), + ("shared", _("shared")), + ("all", _("all")), ) @@ -1162,5 +1182,5 @@ class VmListSearchForm(forms.Form): # set initial value, otherwise it would be overwritten by request.GET if not self.data.get("stype"): data = self.data.copy() - data['stype'] = 2 + data['stype'] = "all" self.data = data diff --git a/circle/dashboard/static/dashboard/dashboard.js b/circle/dashboard/static/dashboard/dashboard.js index 98cdede..f7e5448 100644 --- a/circle/dashboard/static/dashboard/dashboard.js +++ b/circle/dashboard/static/dashboard/dashboard.js @@ -262,7 +262,7 @@ $(function () { $("#dashboard-vm-search-form").submit(function() { var vm_list_items = $("#dashboard-vm-list .list-group-item"); - if(vm_list_items.length == 1) { + if(vm_list_items.length == 1 && vm_list_items.first().prop("href")) { window.location.href = vm_list_items.first().prop("href"); return false; } diff --git a/circle/dashboard/static/dashboard/group-list.js b/circle/dashboard/static/dashboard/group-list.js index fbbd76e..d5cb480 100644 --- a/circle/dashboard/static/dashboard/group-list.js +++ b/circle/dashboard/static/dashboard/group-list.js @@ -1,83 +1,4 @@ -var ctrlDown, shiftDown = false; -var ctrlKey = 17; -var shiftKey = 16; -var selected = []; - $(function() { - $(document).keydown(function(e) { - if (e.keyCode == ctrlKey) ctrlDown = true; - if (e.keyCode == shiftKey) shiftDown = true; - }).keyup(function(e) { - if (e.keyCode == ctrlKey) ctrlDown = false; - if (e.keyCode == shiftKey) shiftDown = false; - }); - - $('.group-list-table tbody').find('tr').mousedown(function() { - var retval = true; - if (ctrlDown) { - setRowColor($(this)); - if(!$(this).hasClass('group-list-selected')) { - selected.splice(selected.indexOf($(this).index()), 1); - } else { - selected.push($(this).index()); - } - retval = false; - } else if(shiftDown) { - if(selected.length > 0) { - start = selected[selected.length - 1] + 1; - end = $(this).index(); - - if(start > end) { - var tmp = start - 1; start = end; end = tmp - 1; - } - - for(var i = start; i <= end; i++) { - if(selected.indexOf(i) < 0) { - selected.push(i); - setRowColor($('.group-list-table tbody tr').eq(i)); - } - } - } - retval = false; - } else { - $('.group-list-selected').removeClass('group-list-selected'); - $(this).addClass('group-list-selected'); - selected = [$(this).index()]; - } - - // reset btn disables - $('.group-list-table tbody tr .btn').attr('disabled', false); - // show/hide group controls - if(selected.length > 1) { - $('.group-list-group-control a').attr('disabled', false); - for(var i = 0; i < selected.length; i++) { - $('.group-list-table tbody tr').eq(selected[i]).find('.btn').attr('disabled', true); - } - } else { - $('.group-list-group-control a').attr('disabled', true); - } - return retval; - }); - - $('#group-list-group-migrate').click(function() { - console.log(collectIds(selected)); - }); - - - $('tbody a').mousedown(function(e) { - // parent tr doesn't get selected when clicked - e.stopPropagation(); - }); - - $('tbody a').click(function(e) { - // browser doesn't jump to top when clicked the buttons - - if(!$(this).hasClass('real-link')) { - return false; - } - }); - - /* rename */ $("#group-list-rename-button, .group-details-rename-button").click(function() { $("#group-list-column-name", $(this).closest("tr")).hide(); @@ -113,51 +34,4 @@ $(function() { return false; }); - - /* group actions */ - - /* select all */ - $('#group-list-group-select-all').click(function() { - $('.group-list-table tbody tr').each(function() { - var index = $(this).index(); - if(selected.indexOf(index) < 0) { - selected.push(index); - $(this).addClass('group-list-selected'); - } - }); - if(selected.length > 0) - $('.group-list-group-control a').attr('disabled', false); - return false; - }); - - /* mass vm delete */ - $('#group-list-group-delete').click(function() { - addModalConfirmation(massDeleteVm, - { - 'url': '/dashboard/group/mass-delete/', - 'data': { - 'selected': selected, - 'v': collectIds(selected) - } - } - ); - return false; - }); }); - -function collectIds(rows) { - var ids = []; - for(var i = 0; i < rows.length; i++) { - var div = $('td:first-child div', $('.group-list-table tbody tr').eq(rows[i])); - ids.push(div.prop('id').replace('node-', '')); - } - return ids; -} - -function setRowColor(row) { - if(!row.hasClass('group-list-selected')) { - row.addClass('group-list-selected'); - } else { - row.removeClass('group-list-selected'); - } -} diff --git a/circle/dashboard/templates/dashboard/_vm-create-2.html b/circle/dashboard/templates/dashboard/_vm-create-2.html index 3d3a62c..53c9fde 100644 --- a/circle/dashboard/templates/dashboard/_vm-create-2.html +++ b/circle/dashboard/templates/dashboard/_vm-create-2.html @@ -7,7 +7,6 @@ {% csrf_token %} {{ vm_create_form.template }} -{{ vm_create_form.customized }} <div class="row"> <div class="col-sm-12"> @@ -23,6 +22,8 @@ </div> </div> +{% if perms.vm.set_resources %} +{{ vm_create_form.customized }} <div class="row"> <div class="col-sm-10"> <div class="form-group"> @@ -85,6 +86,7 @@ </div><!-- .no-js-hidden --> </div><!-- .col-sm-8 --> </div><!-- .row --> +{% endif %} </form> <script> diff --git a/circle/dashboard/templates/dashboard/group-list.html b/circle/dashboard/templates/dashboard/group-list.html index 0719e1a..92dfaa5 100644 --- a/circle/dashboard/templates/dashboard/group-list.html +++ b/circle/dashboard/templates/dashboard/group-list.html @@ -6,63 +6,24 @@ {% block content %} -<div class="alert alert-info"> - Tip #1: you can select multiple vm instances while holding down the <strong>CTRL</strong> key! -</div> - -<div class="alert alert-info"> - Tip #2: if you want to select multiple instances by one click select an instance then hold down <strong>SHIFT</strong> key and select another one! -</div> - <div class="row"> <div class="col-md-12"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="no-margin"><i class="fa fa-group"></i> Your groups</h3> </div> - <div class="panel-body group-list-group-control"> - <p> - <strong>Group actions</strong> - <button id="group-list-group-select-all" class="btn btn-info btn-xs">Select all</button> - <a id="group-list-group-delete" disabled href="#" class="btn btn-danger btn-xs"><i class="fa fa-times"></i> Discard</a> - </p> - </div> + <div class="panel-body"> <div id="table_container"> - - <div id="rendered_table" class="panel-body"> - {% render_table table %} + <div id="rendered_table" class="panel-body"> + {% render_table table %} + </div> </div> - </div> + </div><!-- .panel-body --> </div> </div> </div> -<style> - .popover { - max-width: 600px; - } - - .group-list-selected, .group-list-selected td { - background-color: #e8e8e8 !important; - } - - .group-list-selected:hover, .group-list-selected:hover td { - background-color: #d0d0d0 !important; - } - - .group-list-selected td:first-child { - font-weight: bold; - } - - .group-list-table-thin { - width: 10px; - } - - .group-list-table-admin { - width: 130px; - } -</style> {% endblock %} {% block extra_js %} -<script src="{{ STATIC_URL}}dashboard/group-list.js"></script> + <script src="{{ STATIC_URL}}dashboard/group-list.js"></script> {% endblock %} diff --git a/circle/dashboard/templates/dashboard/group-list/column-actions.html b/circle/dashboard/templates/dashboard/group-list/column-actions.html index 9529c06..3ad9ef6 100644 --- a/circle/dashboard/templates/dashboard/group-list/column-actions.html +++ b/circle/dashboard/templates/dashboard/group-list/column-actions.html @@ -1 +1,5 @@ - <a data-group-pk="{{ record.pk }}" class="btn btn-danger btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=record.pk %}?next={{ request.path }}"><i class="fa fa-trash"></i></a> +<a data-group-pk="{{ record.pk }}" + class="btn btn-danger btn-xs real-link group-delete" + href="{% url "dashboard.views.delete-group" pk=record.pk %}?next={{ request.path }}"> + <i class="fa fa-trash-o"></i> +</a> diff --git a/circle/dashboard/templates/dashboard/index-nodes.html b/circle/dashboard/templates/dashboard/index-nodes.html index 0573cde..e6ecb49 100644 --- a/circle/dashboard/templates/dashboard/index-nodes.html +++ b/circle/dashboard/templates/dashboard/index-nodes.html @@ -3,8 +3,10 @@ <div class="panel-heading"> <div class="pull-right toolbar"> <div class="btn-group"> - <a href="#index-graph-view" data-index-box="node" class="btn btn-default btn-xs"><i class="fa fa-dashboard"></i></a> - <a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled"><i class="fa fa-list"></i></a> + <a href="#index-graph-view" data-index-box="node" class="btn btn-default btn-xs" + data-container="body"><i class="fa fa-dashboard"></i></a> + <a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled" + data-container="body"><i class="fa fa-list"></i></a> </div> <span class="btn btn-default btn-xs infobtn" title="{% trans "List of compute nodes, also called worker nodes or hypervisors, which run the virtual machines." %}"><i class="fa fa-info-circle"></i></span> diff --git a/circle/dashboard/templates/dashboard/index-vm.html b/circle/dashboard/templates/dashboard/index-vm.html index 66622e5..8815e1b 100644 --- a/circle/dashboard/templates/dashboard/index-vm.html +++ b/circle/dashboard/templates/dashboard/index-vm.html @@ -3,10 +3,12 @@ <div class="panel-heading"> <div class="pull-right toolbar"> <div class="btn-group"> - <a href="#index-graph-view" data-index-box="vm" class="btn - btn-default btn-xs" title="{% trans "summary view" %}"><i class="fa fa-dashboard"></i></a> - <a href="#index-list-view" data-index-box="vm" class="btn - btn-default btn-xs disabled" title="{% trans "list view" %}"><i class="fa fa-list"></i></a> + <a href="#index-graph-view" data-index-box="vm" class="btn btn-default btn-xs" + data-container="body" + title="{% trans "summary view" %}"><i class="fa fa-dashboard"></i></a> + <a href="#index-list-view" data-index-box="vm" class="btn btn-default btn-xs disabled" + data-container="body" + title="{% trans "list view" %}"><i class="fa fa-list"></i></a> </div> <span class="btn btn-default btn-xs infobtn" title="{% trans "List of your current virtual machines. Favourited ones are ahead of others." %}"><i class="fa fa-info-circle"></i></span> </div> diff --git a/circle/dashboard/templates/dashboard/vm-detail/_operations.html b/circle/dashboard/templates/dashboard/vm-detail/_operations.html index f4161d7..7e799c9 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/_operations.html +++ b/circle/dashboard/templates/dashboard/vm-detail/_operations.html @@ -7,7 +7,7 @@ <span class="operation operation-{{op.op}} btn btn-default disabled btn-xs"> {% else %} <a href="{{op.get_url}}" class="operation operation-{{op.op}} btn - btn-{{op.effect}} btn-xs" title="{{op.name|capfirst}}: {{op.description|truncatewords:20}}"> + btn-{{op.effect}} btn-xs" title="{{op.name|capfirst}}: {{op.description|truncatewords:15}}"> {% endif %} <i class="fa fa-{{op.icon}}"></i> <span{% if not op.is_preferred %} class="sr-only"{% endif %}>{{op.name}}</span> diff --git a/circle/dashboard/views.py b/circle/dashboard/views.py index 0ba79d8..0116282 100644 --- a/circle/dashboard/views.py +++ b/circle/dashboard/views.py @@ -67,7 +67,7 @@ from .forms import ( CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm, NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm, UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm, - VmSaveForm, UserKeyForm, VmRenewForm, + VmSaveForm, UserKeyForm, VmRenewForm, VmStateChangeForm, CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm, TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm, VmResourcesForm, VmAddInterfaceForm, VmListSearchForm @@ -936,6 +936,24 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView): return extra +class VmStateChangeView(FormOperationMixin, VmOperationView): + op = 'emergency_change_state' + icon = 'legal' + effect = 'danger' + show_in_toolbar = True + form_class = VmStateChangeForm + wait_for_result = 0.5 + + def get_form_kwargs(self): + inst = self.get_op().instance + active_activities = InstanceActivity.objects.filter( + finished__isnull=True, instance=inst) + show_interrupt = active_activities.exists() + val = super(VmStateChangeView, self).get_form_kwargs() + val.update({'show_interrupt': show_interrupt, 'status': inst.status}) + return val + + vm_ops = OrderedDict([ ('deploy', VmOperationView.factory( op='deploy', icon='play', effect='success')), @@ -956,8 +974,7 @@ vm_ops = OrderedDict([ op='shut_off', icon='ban', effect='warning')), ('recover', VmOperationView.factory( op='recover', icon='medkit', effect='warning')), - ('nostate', VmOperationView.factory( - op='emergency_change_state', icon='legal', effect='danger')), + ('nostate', VmStateChangeView), ('destroy', VmOperationView.factory( extra_bases=[TokenOperationView], op='destroy', icon='times', effect='danger')), @@ -1764,10 +1781,10 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView): def create_default_queryset(self): cleaned_data = self.search_form.cleaned_data - stype = cleaned_data.get('stype', 2) - superuser = stype == 2 - shared = stype == 1 - level = "owner" if stype == 0 else "user" + stype = cleaned_data.get('stype', "all") + superuser = stype == "all" + shared = stype == "shared" + level = "owner" if stype == "owned" else "user" queryset = Instance.get_objects_with_level( level, self.request.user, group_also=shared, disregard_superuser=not superuser, @@ -2048,8 +2065,10 @@ class VmCreate(LoginRequiredMixin, TemplateView): if not template.has_level(request.user, 'user'): raise PermissionDenied() - instances = [Instance.create_from_template( - template=template, owner=user)] + args = {"template": template, "owner": user} + if "name" in request.POST: + args["name"] = request.POST.get("name") + instances = [Instance.create_from_template(**args)] return self.__deploy(request, instances) def __create_customized(self, request, *args, **kwargs): @@ -2075,6 +2094,7 @@ class VmCreate(LoginRequiredMixin, TemplateView): 'num_cores': post['cpu_count'], 'ram_size': post['ram_size'], 'priority': post['cpu_priority'], + 'max_ram_size': post['ram_size'], } networks = [InterfaceTemplate(vlan=l, managed=l.managed) for l in post['networks']] diff --git a/circle/firewall/models.py b/circle/firewall/models.py index 8fab47a..5081059 100644 --- a/circle/firewall/models.py +++ b/circle/firewall/models.py @@ -203,12 +203,6 @@ class Rule(models.Model): elif self.firewall_id: return 'INPUT' if self.direction == 'in' else 'OUTPUT' - def get_dport_sport(self): - if self.direction == 'in': - return self.dport, self.sport - else: - return self.sport, self.dport - def get_ipt_rules(self, host=None): # action action = 'LOG_ACC' if self.action == 'accept' else 'LOG_DROP' @@ -235,9 +229,6 @@ class Rule(models.Model): if vlan and not vlan.managed: return retval - # src and dst ports - dport, sport = self.get_dport_sport() - # process foreign vlans for foreign_vlan in self.foreign_network.vlans.all(): if not foreign_vlan.managed: @@ -246,7 +237,7 @@ class Rule(models.Model): r = IptRule(priority=self.weight, action=action, proto=self.proto, extra=self.extra, comment='Rule #%s' % self.pk, - src=src, dst=dst, dport=dport, sport=sport) + src=src, dst=dst, dport=self.dport, sport=self.sport) chain_name = self.get_chain_name(local=vlan, remote=foreign_vlan) retval[chain_name] = r diff --git a/circle/vm/operations.py b/circle/vm/operations.py index b85019d..32a70c9 100644 --- a/circle/vm/operations.py +++ b/circle/vm/operations.py @@ -745,7 +745,7 @@ class RenewOperation(InstanceOperation): required_perms = () concurrency_check = False - def _operation(self, activity, lease=None, force=False): + def _operation(self, activity, lease=None, force=False, save=False): suspend, delete = self.instance.get_renew_times(lease) if (not force and suspend and self.instance.time_of_suspend and suspend < self.instance.time_of_suspend): @@ -759,6 +759,8 @@ class RenewOperation(InstanceOperation): "in its delete time get earlier than before.")) self.instance.time_of_suspend = suspend self.instance.time_of_delete = delete + if save: + self.instance.lease = lease self.instance.save() activity.result = create_readable(ugettext_noop( "Renewed to suspend at %(suspend)s and destroy at %(delete)s."), @@ -779,9 +781,17 @@ class ChangeStateOperation(InstanceOperation): "resources.") acl_level = "owner" required_perms = ('vm.emergency_change_state', ) + concurrency_check = False - def _operation(self, user, activity, new_state="NOSTATE"): + def _operation(self, user, activity, new_state="NOSTATE", interrupt=False): activity.resultant_state = new_state + if interrupt: + msg_txt = ugettext_noop("Activity is forcibly interrupted.") + message = create_readable(msg_txt, msg_txt) + for i in InstanceActivity.objects.filter( + finished__isnull=True, instance=self.instance): + i.finish(False, result=message) + logger.error('Forced finishing activity %s', i) register_operation(ChangeStateOperation) diff --git a/circle/vm/tasks/local_agent_tasks.py b/circle/vm/tasks/local_agent_tasks.py index e7ef122..27b471b 100644 --- a/circle/vm/tasks/local_agent_tasks.py +++ b/circle/vm/tasks/local_agent_tasks.py @@ -86,7 +86,7 @@ def agent_started(vm, version=None): if version and version != settings.AGENT_VERSION: try: - update_agent(vm, instance, act) + update_agent(instance, act) except TimeoutError: pass else: @@ -134,9 +134,9 @@ def agent_stopped(vm): pass -def update_agent(instance, vm, act=None): +def update_agent(instance, act=None): if act: - act.sub_activity( + act = act.sub_activity( 'update', readable_name=create_readable( ugettext_noop('update to %(version)s'), @@ -150,5 +150,6 @@ def update_agent(instance, vm, act=None): version=settings.AGENT_VERSION)) with act: queue = instance.get_remote_queue_name("agent") - update.apply_async(queue=queue, - args=(vm, create_agent_tar())).get(timeout=10) + update.apply_async( + queue=queue, + args=(instance.vm_name, create_agent_tar())).get(timeout=10)