diff --git a/circle/dashboard/forms.py b/circle/dashboard/forms.py index 1bf863b..af2489e 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -41,6 +41,7 @@ from django.forms.widgets import TextInput, HiddenInput from django.template import Context from django.template.loader import render_to_string from django.utils.html import escape, format_html +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from sizefield.widgets import FileSizeWidget from django.core.urlresolvers import reverse_lazy @@ -951,18 +952,45 @@ class VmAddInterfaceForm(OperationForm): self.fields['vlan'] = field +class DeployChoiceField(forms.ModelChoiceField): + def __init__(self, *args, **kwargs): + self.instance = kwargs.pop("instance") + super(DeployChoiceField, self).__init__(*args, **kwargs) + + def label_from_instance(self, obj): + traits = set(obj.traits.all()) + req_traits = set(self.instance.req_traits.all()) + # if the subset is empty the node satisfies the required traits + subset = req_traits - traits + + label = "%s %s" % ( + "" if subset else "", escape(obj.name), + ) + + if subset: + missing_traits = ", ".join(map(lambda x: escape(x.name), subset)) + label += _(" (missing_traits: %s)") % missing_traits + + return mark_safe(label) + + class VmDeployForm(OperationForm): def __init__(self, *args, **kwargs): choices = kwargs.pop('choices', None) + instance = kwargs.pop("instance") super(VmDeployForm, self).__init__(*args, **kwargs) if choices is not None: - self.fields.insert(0, 'node', forms.ModelChoiceField( + self.fields.insert(0, 'node', DeployChoiceField( queryset=choices, required=False, label=_('Node'), help_text=_( "Deploy virtual machine to this node " - "(blank allows scheduling automatically)."))) + "(blank allows scheduling automatically)."), + widget=forms.Select(attrs={ + 'class': "font-awesome-font", + }), instance=instance + )) class VmPortRemoveForm(OperationForm): diff --git a/circle/dashboard/templates/dashboard/_vm-migrate.html b/circle/dashboard/templates/dashboard/_vm-migrate.html index c8024c4..89d3ba7 100644 --- a/circle/dashboard/templates/dashboard/_vm-migrate.html +++ b/circle/dashboard/templates/dashboard/_vm-migrate.html @@ -23,11 +23,35 @@ Choose a compute node to migrate {{obj}} to. <i class="fa {{n.get_status_icon}}"></i> {{n.get_status_display}}</div> {% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %} {% if recommended == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %} + {% if n.pk not in nodes_w_traits %} + <div class="label label-warning"> + <i class="fa fa-warning"></i> + {% trans "missing traits" %}</div> + {% endif %} </label> <input id="migrate-to-{{n.pk}}" type="radio" name="to_node" value="{{ n.pk }}" style="float: right;" {% if current == n.pk %}disabled="disabled"{% endif %} {% if recommended == n.pk %}checked="checked"{% endif %} /> + {% if n.pk not in nodes_w_traits %} + <span class="vm-migrate-node-property"> + {% trans "Node traits" %}: + {% if n.traits.all %} + {{ n.traits.all|join:", " }} + {% else %} + - + {% endif %} + </span> + <span class="vm-migrate-node-property"> + {% trans "Required traits" %}: + {% if object.req_traits.all %} + {{ object.req_traits.all|join:", " }} + {% else %} + - + {% endif %} + </span> + <hr /> + {% endif %} <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> diff --git a/circle/dashboard/views/vm.py b/circle/dashboard/views/vm.py index 2bce624..f916cbd 100644 --- a/circle/dashboard/views/vm.py +++ b/circle/dashboard/views/vm.py @@ -67,6 +67,7 @@ from ..forms import ( VmRemoveInterfaceForm, ) from ..models import Favourite +from manager.scheduler import has_traits logger = logging.getLogger(__name__) @@ -444,6 +445,20 @@ class VmMigrateView(FormOperationMixin, VmOperationView): val.update({'choices': choices, 'default': default}) return val + def get_context_data(self, *args, **kwargs): + ctx = super(VmMigrateView, self).get_context_data(*args, **kwargs) + + inst = self.get_object() + if isinstance(inst, Instance): + nodes_w_traits = [ + n.pk for n in Node.objects.filter(enabled=True) + if n.online and + has_traits(inst.req_traits.all(), n) + ] + ctx['nodes_w_traits'] = nodes_w_traits + + return ctx + class VmPortRemoveView(FormOperationMixin, VmOperationView): @@ -698,6 +713,7 @@ class VmDeployView(FormOperationMixin, VmOperationView): online = (n.pk for n in Node.objects.filter(enabled=True) if n.online) kwargs['choices'] = Node.objects.filter(pk__in=online) + kwargs['instance'] = self.get_object() return kwargs