Merge branch 'master' of github.com:open-cloud/xos
diff --git a/xos/core/models/controlleruser.py b/xos/core/models/controlleruser.py
index b950901..0900df7 100644
--- a/xos/core/models/controlleruser.py
+++ b/xos/core/models/controlleruser.py
@@ -15,6 +15,8 @@
     controller = models.ForeignKey(Controller,related_name='controllersusers')
     kuser_id = StrippedCharField(null=True, blank=True, max_length=200, help_text="Keystone user id")
 
+    composite_primary_key = ('user', 'controller', 'kuser_id')
+
     def __unicode__(self):  return u'%s %s' % (self.controller, self.user)
 
     @staticmethod
@@ -38,6 +40,8 @@
     site_privilege = models.ForeignKey('SitePrivilege', related_name='controllersiteprivileges')
     role_id = StrippedCharField(null=True, blank=True, max_length=200, db_index=True, help_text="Keystone id")
 
+    composite_primary_key = ('controller', 'site_privilege', 'role_id')
+
     def __unicode__(self):  return u'%s %s' % (self.controller, self.site_privilege)
 
     def can_update(self, user):
@@ -69,6 +73,8 @@
     slice_privilege = models.ForeignKey('SlicePrivilege', related_name='controllersliceprivileges')
     role_id = StrippedCharField(null=True, blank=True, max_length=200, db_index=True, help_text="Keystone id")
 
+    composite_primary_key = ('controller', 'slice_privilege')
+
     def __unicode__(self):  return u'%s %s' % (self.controller, self.slice_privilege)
 
     def can_update(self, user):
diff --git a/xos/core/models/image.py b/xos/core/models/image.py
index b2123f8..0e12473 100644
--- a/xos/core/models/image.py
+++ b/xos/core/models/image.py
@@ -19,6 +19,8 @@
     image = models.ForeignKey(Image,related_name='imagedeployments')
     deployment = models.ForeignKey(Deployment,related_name='imagedeployments')
 
+    composite_primary_key = ('image', 'deployment')
+
     def __unicode__(self):  return u'%s %s' % (self.image, self.deployment)
 
     def can_update(self, user):
@@ -30,5 +32,7 @@
     image = models.ForeignKey(Image,related_name='controllerimages')
     controller = models.ForeignKey(Controller,related_name='controllerimages')
     glance_image_id = StrippedCharField(null=True, blank=True, max_length=200, help_text="Glance image id") 
-
+    
+    composite_primary_key = ('image', 'controller')
+         
     def __unicode__(self):  return u'%s %s' % (self.image, self.controller)
diff --git a/xos/core/models/network.py b/xos/core/models/network.py
index 2572847..5346785 100644
--- a/xos/core/models/network.py
+++ b/xos/core/models/network.py
@@ -155,7 +155,10 @@
     router_id = models.CharField(null=True, blank=True, max_length=256, help_text="Quantum router id")
     subnet_id = models.CharField(null=True, blank=True, max_length=256, help_text="Quantum subnet id")
     subnet = models.CharField(max_length=32, blank=True)
-       
+      
+      
+    composite_primary_key = ('network', 'controller')
+        
     @staticmethod
     def select_by_user(user):
         if user.is_admin:
@@ -173,6 +176,8 @@
     network = models.ForeignKey(Network,related_name='networkslices')
     slice = models.ForeignKey(Slice,related_name='networkslices')
 
+    composite_primary_key = ('network', 'slice')
+
     def save(self, *args, **kwds):
         slice = self.slice
         if (slice not in self.network.permitted_slices.all()) and (slice != self.network.owner) and (not self.network.permitAllSlices):
@@ -204,6 +209,8 @@
     ip = models.GenericIPAddressField(help_text="Sliver ip address", blank=True, null=True)
     port_id = models.CharField(null=True, blank=True, max_length=256, help_text="Quantum port id")
 
+    composite_primary_key = ('network', 'sliver')
+
     def save(self, *args, **kwds):
         slice = self.sliver.slice
         if (slice not in self.network.permitted_slices.all()) and (slice != self.network.owner) and (not self.network.permitAllSlices):
diff --git a/xos/core/models/plcorebase.py b/xos/core/models/plcorebase.py
index 6a2ba34..2b814df 100644
--- a/xos/core/models/plcorebase.py
+++ b/xos/core/models/plcorebase.py
@@ -1,6 +1,7 @@
 import datetime
 import os
 import sys
+from django import db
 from django.db import models
 from django.forms.models import model_to_dict
 from django.core.urlresolvers import reverse
@@ -149,6 +150,10 @@
                                       default="0 - Provisioning in progress")
     deleted = models.BooleanField(default=False)
 
+    # XXX Django has no official support for composite primray keys yet
+    # so we will hack in an inefficient solution here.  
+    composite_primary_key = []
+
     class Meta:
         # Changing abstract to False would require the managers of subclasses of
         # PlCoreBase to be customized individually.
@@ -183,12 +188,31 @@
             self.enacted=None
             self.save(update_fields=['enacted','deleted'], silent=silent)
 
+    def check_composite_primary_key(self):
+        if not self.composite_primary_key:
+            return 
+        # dictionary containing cpk field name and value
+        cpk_fields = dict([(name, getattr(self, name)) for name in self.composite_primary_key])
+        objs = self.__class__.objects.filter(**cpk_fields)
+        # we can only continue if there are no matches or 
+        # if this record is updating itself         
+        if (len(objs) == 0 or
+            (len(objs) == 1 and self.id and objs[0].id == self.id)):
+            return 
+        # if we reach this point then we've matched more than 1 
+        # existing record or we are trying to  
+        msg = "%s violates composite primray key constraint on fields: %s " % (self, self.composite_primary_key)
+        raise db.Error, msg
+                 
+               
     def save(self, *args, **kwargs):
         # let the user specify silence as either a kwarg or an instance varible
         silent = self.silent
         if "silent" in kwargs:
             silent=silent or kwargs.pop("silent")
 
+        self.check_composite_primary_key()    
+
         super(PlCoreBase, self).save(*args, **kwargs)
 
         # This is a no-op if observer_disabled is set
diff --git a/xos/core/models/site.py b/xos/core/models/site.py
index b26877d..3e8fb82 100644
--- a/xos/core/models/site.py
+++ b/xos/core/models/site.py
@@ -216,6 +216,7 @@
     user = models.ForeignKey('User', related_name='deploymentprivileges')
     deployment = models.ForeignKey('Deployment', related_name='deploymentprivileges')
     role = models.ForeignKey('DeploymentRole',related_name='deploymentprivileges')
+    composite_primary_key = ('user', 'deployment', 'role')
 
     def __unicode__(self):  return u'%s %s %s' % (self.deployment, self.user, self.role)
 
@@ -277,6 +278,8 @@
     controller = models.ForeignKey(Controller, null=True, blank=True, related_name='sitedeployments')
     availability_zone = StrippedCharField(max_length=200, null=True, blank=True, help_text="OpenStack availability zone")
 
+    composite_primary_key = ('site', 'deployment', 'controller')
+
     def __unicode__(self):  return u'%s %s' % (self.deployment, self.site)
     
 class ControllerSite(PlCoreBase):
@@ -284,3 +287,5 @@
     site = models.ForeignKey(Site,related_name='controllersite')
     controller = models.ForeignKey(Controller, null=True, blank=True, related_name='controllersite')
     tenant_id = StrippedCharField(null=True, blank=True, max_length=200, db_index=True, help_text="Keystone tenant id")
+    
+    composite_primary_key = ('site', 'controller') 
diff --git a/xos/core/models/slice.py b/xos/core/models/slice.py
index 7c4e82f..22340ea 100644
--- a/xos/core/models/slice.py
+++ b/xos/core/models/slice.py
@@ -126,6 +126,8 @@
     slice = models.ForeignKey('Slice', related_name='sliceprivileges')
     role = models.ForeignKey('SliceRole',related_name='sliceprivileges')
 
+    composite_primary_key = ('user', 'slice', 'role')
+
     def __unicode__(self):  return u'%s %s %s' % (self.slice, self.user, self.role)
 
     def can_update(self, user):
@@ -148,6 +150,8 @@
     slice = models.ForeignKey(Slice, related_name='controllerslices')
     tenant_id = StrippedCharField(null=True, blank=True, max_length=200, help_text="Keystone tenant id")
 
+    composite_primary_key = ('controller', 'slice')
+     
     def __unicode__(self):  return u'%s %s'  % (self.slice, self.controller)
 
     @staticmethod