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..718d0a3 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.
{{n.get_status_display}}
{% if current == n.pk %}
{% trans "current" %}
{% endif %}
{% if recommended == n.pk %}{% trans "recommended" %}
{% endif %}
+ {% if n.pk not in nodes_w_traits %}
+
+
+ {% trans "missing traits" %}
+ {% endif %}
+ {% if n.pk not in nodes_w_traits %}
+
+ {% trans "Node traits" %}:
+ {% if n.traits.all %}
+ {{ n.traits.all|join:", " }}
+ {% else %}
+ -
+ {% endif %}
+
+
+ {% trans "Required traits" %}:
+ {% if object.req_traits.all %}
+ {{ object.req_traits.all|join:", " }}
+ {% else %}
+ -
+ {% endif %}
+
+
+ {% endif %}
{% trans "CPU load" %}: {{ n.cpu_usage }}
{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}
diff --git a/circle/dashboard/templates/dashboard/node-detail.html b/circle/dashboard/templates/dashboard/node-detail.html
index 586cd74..c4d04ef 100644
--- a/circle/dashboard/templates/dashboard/node-detail.html
+++ b/circle/dashboard/templates/dashboard/node-detail.html
@@ -73,7 +73,7 @@
-
{% trans "Virtual Machines" %}
diff --git a/circle/dashboard/views/node.py b/circle/dashboard/views/node.py
index 269eb96..19bcbe4 100644
--- a/circle/dashboard/views/node.py
+++ b/circle/dashboard/views/node.py
@@ -143,8 +143,13 @@ class NodeDetailView(LoginRequiredMixin,
def __remove_trait(self, request):
try:
to_remove = request.POST.get('to_remove')
- self.object = self.get_object()
- self.object.traits.remove(to_remove)
+ trait = Trait.objects.get(pk=to_remove)
+ node = self.get_object()
+ node.traits.remove(to_remove)
+
+ if not trait.in_use:
+ trait.delete()
+
message = u"Success"
except: # note this won't really happen
message = u"Not success"
@@ -155,7 +160,7 @@ class NodeDetailView(LoginRequiredMixin,
content_type="application/json"
)
else:
- return redirect(self.object.get_absolute_url())
+ return redirect(node.get_absolute_url())
class NodeList(LoginRequiredMixin, GraphMixin, SingleTableView):
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
diff --git a/circle/vm/models/common.py b/circle/vm/models/common.py
index c86c0b1..fd3efa2 100644
--- a/circle/vm/models/common.py
+++ b/circle/vm/models/common.py
@@ -170,3 +170,10 @@ class Trait(Model):
def __unicode__(self):
return self.name
+
+ @property
+ def in_use(self):
+ return (
+ self.instance_set.exists() or self.node_set.exists()
+ or self.instancetemplate_set.exists()
+ )