Merge branch 'master' of github.com:open-cloud/xos
diff --git a/xos/core/admin.py b/xos/core/admin.py
index 69cae61..1ded815 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -57,6 +57,31 @@
flatatt(final_attrs),
force_text(value))
+class SliderWidget(forms.HiddenInput):
+ def render(self, name, value, attrs=None):
+ if value is None:
+ value = '0'
+ final_attrs = self.build_attrs(attrs, name=name)
+ attrs = attrs or attrs[:]
+ attrs["name"] = name
+ attrs["value"] = value
+ html = """<div style="width:640px"><span id="%(id)s_label">%(value)s</span><div id="%(id)s_slider" style="float:right;width:610px;margin-top:5px"></div></div>
+ <script>
+ $(function() {
+ $("#%(id)s_slider").slider({
+ value: %(value)s,
+ slide: function(event, ui) { $("#%(id)s").val( ui.value ); $("#%(id)s_label").html(ui.value); },
+ });
+ });
+ </script>
+ <input type="hidden" id="%(id)s" name="%(name)s" value="%(value)s"></input>
+ """ % attrs
+ html = html.replace("{","{{").replace("}","}}")
+ return format_html(html,
+ flatatt(final_attrs),
+ force_text(value))
+
+
class PlainTextWidget(forms.HiddenInput):
input_type = 'hidden'
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 19bcda9..8a10f37 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -29,6 +29,79 @@
def __unicode__(self): return u'%s' % (self.name)
+ def get_scalable_nodes(self, slice, max_per_node=None, exclusive_slices=[]):
+ """
+ Get a list of nodes that can be used to scale up a slice.
+
+ slice - slice to scale up
+ max_per_node - maximum numbers of slivers that 'slice' can have on a single node
+ exclusive_slices - list of slices that must have no nodes in common with 'slice'.
+ """
+
+ from core.models import Node, Sliver # late import to get around order-of-imports constraint in __init__.py
+
+ nodes = list(Node.objects.all())
+
+ conflicting_slivers = Sliver.objects.filter(slice__in = exclusive_slices)
+ conflicting_nodes = Node.objects.filter(slivers__in = conflicting_slivers)
+
+ nodes = [x for x in nodes if x not in conflicting_nodes]
+
+ # If max_per_node is set, then limit the number of slivers this slice
+ # can have on a single node.
+ if max_per_node:
+ acceptable_nodes = []
+ for node in nodes:
+ existing_count = node.slivers.filter(slice=slice).count()
+ if existing_count < max_per_node:
+ acceptable_nodes.append(node)
+ nodes = acceptable_nodes
+
+ return nodes
+
+ def pick_node(self, slice, max_per_node=None, exclusive_slices=[]):
+ # Pick the best node to scale up a slice.
+
+ nodes = self.get_scalable_nodes(slice, max_per_node, exclusive_slices)
+ nodes = sorted(nodes, key=lambda node: node.slivers.all().count())
+ if not nodes:
+ return None
+ return nodes[0]
+
+ def adjust_scale(self, slice_hint, scale, max_per_node=None, exclusive_slices=[]):
+ from core.models import Sliver # late import to get around order-of-imports constraint in __init__.py
+
+ slices = [x for x in self.slices.all() if slice_hint in x.name]
+ for slice in slices:
+ while slice.slivers.all().count() > scale:
+ s = slice.slivers.all()[0]
+ # print "drop sliver", s
+ s.delete()
+
+ while slice.slivers.all().count() < scale:
+ node = self.pick_node(slice, max_per_node, exclusive_slices)
+ if not node:
+ # no more available nodes
+ break
+
+ image = slice.default_image
+ if not image:
+ raise XOSConfigurationError("No default_image for slice %s" % slice.name)
+
+ flavor = slice.default_flavor
+ if not flavor:
+ raise XOSConfigurationError("No default_flavor for slice %s" % slice.name)
+
+ s = Sliver(slice=slice,
+ node=node,
+ creator=slice.creator,
+ image=image,
+ flavor=flavor,
+ deployment=node.site_deployment.deployment)
+ s.save()
+
+ # print "add sliver", s
+
class ServiceAttribute(PlCoreBase):
name = models.SlugField(help_text="Attribute Name", max_length=128)
value = StrippedCharField(help_text="Attribute Value", max_length=1024)
diff --git a/xos/core/static/xos.css b/xos/core/static/xos.css
index 513dc06..282c9f4 100644
--- a/xos/core/static/xos.css
+++ b/xos/core/static/xos.css
@@ -182,7 +182,7 @@
float: right;
border: 2px darkGrey;
}
-.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default{
+.ui-state-default #hometabs, .ui-widget-content .ui-state-default #hometabs, .ui-widget-header .ui-state-default {
background: none !important;
border-top: 0px !important;
border-left: 0px !important;
diff --git a/xos/hpc/admin.py b/xos/hpc/admin.py
index 20364b6..08a1cdb 100644
--- a/xos/hpc/admin.py
+++ b/xos/hpc/admin.py
@@ -10,7 +10,7 @@
from django.utils import timezone
from django.contrib.contenttypes import generic
from suit.widgets import LinkedSelect
-from core.admin import ServiceAppAdmin,SliceInline,ServiceAttrAsTabInline, ReadOnlyAwareAdmin, XOSTabularInline
+from core.admin import ServiceAppAdmin,SliceInline,ServiceAttrAsTabInline, ReadOnlyAwareAdmin, XOSTabularInline, SliderWidget
from functools import update_wrapper
from django.contrib.admin.views.main import ChangeList
@@ -106,15 +106,30 @@
# filtered_change_view rather than the default change_view.
return FilteredChangeList
+class HpcServiceForm(forms.ModelForm):
+ scale = forms.IntegerField(widget = SliderWidget, required=False)
+
+ def __init__(self, *args, **kwargs):
+ super(HpcServiceForm, self).__init__(*args, **kwargs)
+ self.fields['scale'].initial = kwargs["instance"].scale
+
+ def save(self, *args, **kwargs):
+ if self.cleaned_data['scale']:
+ self.instance.scale = self.cleaned_data['scale']
+
+ return super(HpcServiceForm, self).save(*args, **kwargs)
+
+
class HpcServiceAdmin(ReadOnlyAwareAdmin):
model = HpcService
verbose_name = "HPC Service"
verbose_name_plural = "HPC Service"
list_display = ("backend_status_icon", "name","enabled")
list_display_links = ('backend_status_icon', 'name', )
- fieldsets = [(None, {'fields': ['backend_status_text', 'name','enabled','versionNumber', 'description', "cmi_hostname"], 'classes':['suit-tab suit-tab-general']})]
+ fieldsets = [(None, {'fields': ['backend_status_text', 'name','scale','enabled','versionNumber', 'description', "cmi_hostname"], 'classes':['suit-tab suit-tab-general']})]
readonly_fields = ('backend_status_text', )
inlines = [SliceInline,ServiceAttrAsTabInline]
+ form = HpcServiceForm
extracontext_registered_admins = True
diff --git a/xos/hpc/models.py b/xos/hpc/models.py
index e915fbc..a3b7c90 100644
--- a/xos/hpc/models.py
+++ b/xos/hpc/models.py
@@ -17,6 +17,26 @@
cmi_hostname = StrippedCharField(max_length=254, null=True, blank=True)
+ @property
+ def scale(self):
+ hpc_slices = [x for x in self.slices.all() if "hpc" in x.name]
+ if not hpc_slices:
+ return 0
+ return hpc_slices[0].slivers.count()
+
+ @scale.setter
+ def scale(self, value):
+ self.set_scale = value
+
+ def save(self, *args, **kwargs):
+ super(HpcService, self).save(*args, **kwargs)
+
+ # scale up/down
+ scale = getattr(self, "set_scale", None)
+ if scale is not None:
+ exclude_slices = [x for x in self.slices.all() if "cmi" in x.name]
+ self.adjust_scale(slice_hint="hpc", scale=scale, exclusive_slices = exclude_slices)
+
class ServiceProvider(PlCoreBase):
class Meta:
app_label = "hpc"