diff --git a/circle/dashboard/static/dashboard/vm-list.js b/circle/dashboard/static/dashboard/vm-list.js index 35d7ea6..2df8867 100644 --- a/circle/dashboard/static/dashboard/vm-list.js +++ b/circle/dashboard/static/dashboard/vm-list.js @@ -114,7 +114,7 @@ $(function() { $("body").on("click", "#op-form-send", function() { var url = $(this).closest("form").prop("action"); - $(this).find("i").prop("class", "fa fa-spinner fa-spin"); + $(this).find("i").prop("class", "fa fa-fw fa-spinner fa-spin"); $.ajax({ url: url, @@ -177,7 +177,8 @@ $(function() { function checkStatusUpdate() { - if($("#vm-list-table tbody td.state i").hasClass("fa-spin")) { + icons = $("#vm-list-table tbody td.state i"); + if(icons.hasClass("fa-spin") || icons.hasClass("migrating-icon")) { return true; } } @@ -194,12 +195,20 @@ function updateStatuses(runs) { if(vm in result) { if(result[vm].in_status_change) { if(!status_icon.hasClass("fa-spin")) { - status_icon.prop("class", "fa fa-spinner fa-spin"); + status_icon.prop("class", "fa fa-fw fa-spinner fa-spin"); + } + } + else if(result[vm].status == "MIGRATING") { + if(!status_icon.hasClass("migrating-icon")) { + status_icon.prop("class", "fa fa-fw " + result[vm].icon + " migrating-icon"); } } else { - status_icon.prop("class", "fa " + result[vm].icon); + status_icon.prop("class", "fa fa-fw " + result[vm].icon); } status_text.text(result[vm].status); + if("node" in result[vm]) { + $(this).find(".node").text(result[vm].node); + } } else { $(this).remove(); } diff --git a/circle/dashboard/templates/dashboard/_vm-mass-migrate.html b/circle/dashboard/templates/dashboard/_vm-mass-migrate.html new file mode 100644 index 0000000..2b3125b --- /dev/null +++ b/circle/dashboard/templates/dashboard/_vm-mass-migrate.html @@ -0,0 +1,29 @@ +{% extends "dashboard/mass-operate.html" %} +{% load i18n %} +{% load sizefieldtags %} + +{% block question %} +<p> +{% blocktrans with op=op.name %} +Choose a compute node to migrate the selected VMs to. +{% endblocktrans %} +</p> +<p class="text-info">{{op.name}}: {{op.description}}</p> +{% endblock %} + +{% block formfields %} + <ul id="vm-migrate-node-list" class="list-unstyled"> + {% for n in nodes %} + <li class="panel panel-default"><div class="panel-body"> + <label for="migrate-to-{{n.pk}}"> + <strong>{{ n }}</strong> + </label> + <input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;" checked="checked"> + <span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span> + <span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span> + <div style="clear: both;"></div> + </div></li> + {% endfor %} + </ul> + <hr /> +{% endblock %} diff --git a/circle/dashboard/templates/dashboard/mass-operate.html b/circle/dashboard/templates/dashboard/mass-operate.html index 7306fd1..f43092c 100644 --- a/circle/dashboard/templates/dashboard/mass-operate.html +++ b/circle/dashboard/templates/dashboard/mass-operate.html @@ -10,26 +10,27 @@ Do you want to perform the following operation: <strong>{{op}}</strong>? <p class="text-info">{{op.description}}</p> {% endblock %} <form method="POST" action="{{url}}">{% csrf_token %} - {% for i in instances %} - <div class="panel panel-default mass-op-panel"> - <i class="fa {{ i.get_status_icon }} fa-fw"></i> - {{ i.name }} ({{ i.pk }}) - <div style="float: right;" title="{{ i.disabled }}"> - <i class="fa status-icon - {% if i.disabled %}fa-minus-square minus{% else %}fa-check-square check{% endif %}"> - </i> - </div> + {% block formfields %}{% endblock %} + {% for i in instances %} + <div class="panel panel-default mass-op-panel"> + <i class="fa {{ i.get_status_icon }} fa-fw"></i> + {{ i.name }} ({{ i.pk }}) + <div style="float: right;" title="{{ i.disabled }}"> + <i class="fa status-icon + {% if i.disabled %}fa-minus-square minus{% else %}fa-check-square check{% endif %}"> + </i> </div> - <input type="checkbox" name="vm" value="{{ i.pk }}" {% if not i.disabled %}checked{% endif %} - style="display: none;"/> - {% endfor %} + </div> + <input type="checkbox" name="vm" value="{{ i.pk }}" {% if not i.disabled %}checked{% endif %} + style="display: none;"/> + {% endfor %} <div class="pull-right"> <a class="btn btn-default" href="{% url "dashboard.views.vm-list" %}" - data-dismiss="modal">{% trans "Cancel" %}</a> + data-dismiss="modal">{% trans "Cancel" %}</a> <button class="btn btn-{{ opview.effect }}" type="submit" id="op-form-send"> - {% if opview.icon %}<i class="fa fa-{{opview.icon}}"></i> {% endif %}{{ opview.name|capfirst }} - </button> + {% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ opview.name|capfirst }} + </button> </div> </form> diff --git a/circle/dashboard/templates/dashboard/vm-list.html b/circle/dashboard/templates/dashboard/vm-list.html index f56055c..60d314c 100644 --- a/circle/dashboard/templates/dashboard/vm-list.html +++ b/circle/dashboard/templates/dashboard/vm-list.html @@ -66,7 +66,7 @@ <td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td> <td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td> <td class="state"> - <i class="fa + <i class="fa fa-fw {% if i.is_in_status_change %} fa-spin fa-spinner {% else %} @@ -77,7 +77,9 @@ {% include "dashboard/_display-name.html" with user=i.owner show_org=True %} </td> {% if user.is_superuser %} - <td data-sort-value="{{ i.node.normalized_name }}">{{ i.node.name|default:"-" }}</td> + <td class="node "data-sort-value="{{ i.node.normalized_name }}"> + {{ i.node.name|default:"-" }} + </td> {% endif %} </tr> {% empty %} @@ -109,4 +111,53 @@ {% block extra_js %} <script src="{{ STATIC_URL}}dashboard/vm-list.js"></script> <script src="{{ STATIC_URL}}dashboard/js/stupidtable.min.js"></script> + <style> + #vm-list-table .migrating-icon { + -webkit-animation: passing 2s linear infinite; + animation: passing 2s linear infinite; + } + +@-webkit-keyframes passing { + 0% { + -webkit-transform: translateX(50%); + transform: translateX(50%); + opacity: 0; + } + + 50% { + -webkit-transform: translateX(0%); + transform: translateX(0%); + opacity: 1; + } + + 100% { + -webkit-transform: translateX(-50%); + transform: translateX(-50%); + opacity: 0; + } +} + +@keyframes passing { + 0% { + -webkit-transform: translateX(50%); + -ms-transform: translateX(50%); + transform: translateX(50%); + opacity: 0; + } + + 50% { + -webkit-transform: translateX(0%); + -ms-transform: translateX(0%); + transform: translateX(0%); + opacity: 1; + } + + 100% { + -webkit-transform: translateX(-50%); + -ms-transform: translateX(-50%); + transform: translateX(-50%); + opacity: 0; + } +} + </style> {% endblock %} diff --git a/circle/dashboard/views.py b/circle/dashboard/views.py index 2bf4e0d..71a576e 100644 --- a/circle/dashboard/views.py +++ b/circle/dashboard/views.py @@ -1034,14 +1034,16 @@ class MassOperationView(OperationView): vms.append(i) return vms - def post(self, request, *args, **kwargs): + def post(self, request, extra=None, *args, **kwargs): + if extra is None: + extra = {} user = self.request.user vms = request.POST.getlist("vm") instances = Instance.objects.filter(pk__in=vms) for i in instances: try: op = self._op_checks(i, user) - op.async(user=user) + op.async(user=user, **extra) except HumanReadableException as e: e.send_message(request) except Exception as e: @@ -1067,6 +1069,28 @@ class MassOperationView(OperationView): return op +class MassMigrationView(MassOperationView): + template_name = 'dashboard/_vm-mass-migrate.html' + icon = "info" + op = "migrate" + icon = "truck" + + def get_context_data(self, **kwargs): + ctx = super(MassMigrationView, self).get_context_data(**kwargs) + ctx['nodes'] = [n for n in Node.objects.filter(enabled=True) + if n.state == "ONLINE"] + return ctx + + def post(self, request, extra=None, *args, **kwargs): + if extra is None: + extra = {} + node = self.request.POST.get("node") + if node: + node = get_object_or_404(Node, pk=node) + extra["to_node"] = node + return super(MassMigrationView, self).post(request, extra, *args, + **kwargs) + vm_mass_ops = OrderedDict([ ('deploy', MassOperationView.factory( op='deploy', icon='play', effect='success')), @@ -1074,6 +1098,7 @@ vm_mass_ops = OrderedDict([ op='wake_up', icon='sun-o', effect='success')), ('sleep', MassOperationView.factory( op='sleep', icon='moon-o', effect='info')), + ('migrate', MassMigrationView), ('destroy', MassOperationView.factory( op='destroy', icon='times', effect='danger')), ]) @@ -1671,6 +1696,8 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView): 'icon': i.get_status_icon(), 'in_status_change': i.is_in_status_change(), } + if self.request.user.is_superuser: + statuses[i.pk]['node'] = i.node.name if i.node else "-" return HttpResponse(json.dumps(statuses), content_type="application/json") else: diff --git a/circle/vm/models/instance.py b/circle/vm/models/instance.py index 8ad4ad6..3c552f7 100644 --- a/circle/vm/models/instance.py +++ b/circle/vm/models/instance.py @@ -938,7 +938,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, 'ERROR': 'fa-warning', 'PENDING': 'fa-rocket', 'DESTROYED': 'fa-trash-o', - 'MIGRATING': 'fa-truck'}.get(self.status, 'fa-question') + 'MIGRATING': 'fa-truck migrating-icon' + }.get(self.status, 'fa-question') def get_activities(self, user=None): acts = (self.activity_log.filter(parent=None).