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"