CORD-1393 create TenantWithContainer model_policy
Change-Id: I0975b4f7ff1ab500f355f48d81c8c2be181c2d1b
diff --git a/xos/coreapi/apihelper.py b/xos/coreapi/apihelper.py
index e76bf97..17fd6c9 100644
--- a/xos/coreapi/apihelper.py
+++ b/xos/coreapi/apihelper.py
@@ -222,7 +222,10 @@
elif (ftype == "IntegerField") or (ftype == "PositiveIntegerField") or (ftype == "BigIntegerField"):
args[name] = val
elif (ftype == "ForeignKey"):
- args[name] = val # field name already has "_id" at the end
+ if val==0: # assume object id 0 means None
+ args[name] = None
+ else:
+ args[name] = val # field name already has "_id" at the end
elif (ftype == "DateTimeField"):
utc = pytz.utc
args[name] = datetime.datetime.fromtimestamp(val,tz=utc)
diff --git a/xos/synchronizers/new_base/model_policies/__init__.py b/xos/synchronizers/new_base/model_policies/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/synchronizers/new_base/model_policies/__init__.py
diff --git a/xos/synchronizers/new_base/model_policies/model_policy_tenantwithcontainer.py b/xos/synchronizers/new_base/model_policies/model_policy_tenantwithcontainer.py
new file mode 100644
index 0000000..b94e6b8
--- /dev/null
+++ b/xos/synchronizers/new_base/model_policies/model_policy_tenantwithcontainer.py
@@ -0,0 +1,184 @@
+from synchronizers.new_base.modelaccessor import *
+from synchronizers.new_base.policy import Policy
+
+class Scheduler(object):
+ # XOS Scheduler Abstract Base Class
+ # Used to implement schedulers that pick which node to put instances on
+
+ def __init__(self, slice):
+ self.slice = slice
+
+ def pick(self):
+ # this method should return a tuple (node, parent)
+ # node is the node to instantiate on
+ # parent is for container_vm instances only, and is the VM that will
+ # hold the container
+
+ raise Exception("Abstract Base")
+
+
+class LeastLoadedNodeScheduler(Scheduler):
+ # This scheduler always return the node with the fewest number of
+ # instances.
+
+ def __init__(self, slice, label=None):
+ super(LeastLoadedNodeScheduler, self).__init__(slice)
+ self.label = label
+
+ def pick(self):
+ # start with all nodes
+ nodes = Node.objects.all()
+
+ # if a label is set, then filter by label
+ if self.label:
+ nodes = nodes.filter(nodelabels__name=self.label)
+
+ # if slice.default_node is set, then filter by default_node
+ if self.slice.default_node:
+ nodes = nodes.filter(name = self.slice.default_node)
+
+ # convert to list
+ nodes = list(nodes)
+
+ # sort so that we pick the least-loaded node
+ nodes = sorted(nodes, key=lambda node: node.instances.count())
+
+ if not nodes:
+ raise Exception(
+ "LeastLoadedNodeScheduler: No suitable nodes to pick from")
+
+ # TODO: logic to filter nodes by which nodes are up, and which
+ # nodes the slice can instantiate on.
+ return [nodes[0], None]
+
+class TenantWithContainerPolicy(Policy):
+ model_name = None # This policy is abstract. Inherit this class into your own policy and override model_name
+
+ def handle_create(self, tenant):
+ return self.handle_update(tenant)
+
+ def handle_update(self, tenant):
+ self.manage_container(tenant)
+
+# def handle_delete(self, tenant):
+# if tenant.vcpe:
+# tenant.vcpe.delete()
+
+
+ def save_instance(self, instance):
+ # Override this function to do custom pre-save or post-save processing,
+ # such as creating ports for containers.
+ instance.save()
+
+ def get_image(self, tenant):
+ slice = tenant.provider_service.slices.all()
+ if not slice:
+ raise XOSProgrammingError("provider service has no slice")
+ slice = slice[0]
+
+ # If slice has default_image set then use it
+ if slice.default_image:
+ return slice.default_image
+
+ raise XOSProgrammingError("Please set a default image for %s" % self.slice.name)
+
+ """ get_legacy_tenant_attribute
+ pick_least_loaded_instance_in_slice
+ count_of_tenants_of_an_instance
+
+ These three methods seem to be used by A-CORD. Look for ways to consolidate with existing methods and eliminate
+ these legacy ones
+ """
+
+ def get_legacy_tenant_attribute(self, tenant, name, default=None):
+ if tenant.service_specific_attribute:
+ attributes = json.loads(tenant.service_specific_attribute)
+ else:
+ attributes = {}
+ return attributes.get(name, default)
+
+ def pick_least_loaded_instance_in_slice(self, tenant, slices, image):
+ for slice in slices:
+ if slice.instances.all().count() > 0:
+ for instance in slice.instances.all():
+ if instance.image != image:
+ continue
+ # Pick the first instance that has lesser than 5 tenants
+ if self.count_of_tenants_of_an_instance(tenant, instance) < 5:
+ return instance
+ return None
+
+ # TODO: Ideally the tenant count for an instance should be maintained using a
+ # many-to-one relationship attribute, however this model being proxy, it does
+ # not permit any new attributes to be defined. Find if any better solutions
+ def count_of_tenants_of_an_instance(self, tenant, instance):
+ tenant_count = 0
+ for tenant in self.__class__.objects.all():
+ if self.get_legacy_tenant_attribute(tenant, "instance_id", None) == instance.id:
+ tenant_count += 1
+ return tenant_count
+
+ def manage_container(self, tenant):
+ if tenant.deleted:
+ return
+
+ desired_image = self.get_image(tenant)
+
+ if (tenant.instance is not None) and (tenant.instance.image.id != desired_image.id):
+ tenant.instance.delete()
+ tenant.instance = None
+
+ if tenant.instance is None:
+ if not tenant.provider_service.slices.count():
+ raise XOSConfigurationError("The service has no slices")
+
+ new_instance_created = False
+ instance = None
+ if self.get_legacy_tenant_attribute(tenant, "use_same_instance_for_multiple_tenants", default=False):
+ # Find if any existing instances can be used for this tenant
+ slices = tenant.provider_service.slices.all()
+ instance = self.pick_least_loaded_instance_in_slice(slices, desired_image)
+
+ if not instance:
+ slice = tenant.provider_service.slices.first()
+
+ flavor = slice.default_flavor
+ if not flavor:
+ flavors = Flavor.objects.filter(name="m1.small")
+ if not flavors:
+ raise XOSConfigurationError("No m1.small flavor")
+ flavor = flavors[0]
+
+ if slice.default_isolation == "container_vm":
+ raise Exception("Not implemented")
+ else:
+ (node, parent) = LeastLoadedNodeScheduler(slice).pick()
+
+ assert(slice is not None)
+ assert(node is not None)
+ assert(desired_image is not None)
+ assert(tenant.creator is not None)
+ assert(node.site_deployment.deployment is not None)
+ assert(flavor is not None)
+
+ try:
+ instance = Instance(slice=slice,
+ node=node,
+ image=desired_image,
+ creator=tenant.creator,
+ deployment=node.site_deployment.deployment,
+ flavor=flavor,
+ isolation=slice.default_isolation,
+ parent=parent)
+ self.save_instance(instance)
+ new_instance_created = True
+
+ tenant.instance = instance
+ tenant.save()
+ except:
+ # NOTE: We don't have transactional support, so if the synchronizer crashes and exits after
+ # creating the instance, but before adding it to the tenant, then we will leave an
+ # orphaned instance.
+ if new_instance_created:
+ instance.delete()
+ raise
\ No newline at end of file
diff --git a/xos/synchronizers/new_base/model_policy_loop.py b/xos/synchronizers/new_base/model_policy_loop.py
index 0c55cb1..74fb19a 100644
--- a/xos/synchronizers/new_base/model_policy_loop.py
+++ b/xos/synchronizers/new_base/model_policy_loop.py
@@ -74,8 +74,11 @@
if inspect.isclass(c) and issubclass(c, Policy) and hasattr(c, "model_name") and (
c not in policies):
+ if not c.model_name:
+ logger.info("load_model_policies: skipping model policy %s" % classname)
+ continue
if not model_accessor.has_model_class(c.model_name):
- logger.error("load_model_policies: unable to find model policy %s" % c.model_name)
+ logger.error("load_model_policies: unable to find policy %s model %s" % (classname, c.model_name))
c.model = model_accessor.get_model_class(c.model_name)
policies.append(c)
diff --git a/xos/xos_client/xosapi/orm.py b/xos/xos_client/xosapi/orm.py
index d9f656b..2665500 100644
--- a/xos/xos_client/xosapi/orm.py
+++ b/xos/xos_client/xosapi/orm.py
@@ -16,8 +16,8 @@
"""
import grpc_client, orm
c=grpc_client.SecureClient("xos-core.cord.lab", username="padmin@vicci.org", password="letmein")
-u=c.xos_orm.User.objects.get(id=1)
-"""
+u=c.xos_orm.User.objects.get(id=1)
+"""
import functools
import time
@@ -123,7 +123,10 @@
def fk_set(self, name, model):
fk_entry = self._fkmap[name]
fk_kind = fk_entry["kind"]
- id = model.id
+ if model:
+ id = model.id
+ else:
+ id = 0
setattr(self._wrapped_class, fk_entry["src_fieldName"], id)
if fk_kind=="generic_fk":
@@ -278,6 +281,9 @@
def exists(self):
return len(self._idList)>0
+ def count(self):
+ return len(self._idList)
+
def first(self):
if self._idList:
model = make_ORMWrapper(self._stub.invoke("Get%s" % self._modelName, self._stub.make_ID(id=self._idList[0])), self._stub)
diff --git a/xos/xos_client/xosapi/orm_test.py b/xos/xos_client/xosapi/orm_test.py
index f276e61..b20a3a9 100644
--- a/xos/xos_client/xosapi/orm_test.py
+++ b/xos/xos_client/xosapi/orm_test.py
@@ -126,6 +126,39 @@
self.assertNotEqual(slice.site, None)
self.assertEqual(slice.site.id, site.id)
+ def test_foreign_key_create_null(self):
+ orm = self.make_coreapi()
+ site = orm.Site(name="mysite")
+ site.save()
+ self.assertTrue(site.id > 0)
+ slice = orm.Slice(name="mysite_foo", site = site, service=None)
+ slice.save()
+ slice.invalidate_cache()
+ self.assertTrue(slice.id > 0)
+ self.assertEqual(slice.service, None)
+
+ def test_foreign_key_set_null(self):
+ orm = self.make_coreapi()
+ site = orm.Site(name="mysite")
+ site.save()
+ self.assertTrue(site.id > 0)
+ service = orm.Service(name="myservice")
+ service.save()
+ self.assertTrue(service.id > 0)
+ # start out slice.service is non-None
+ slice = orm.Slice(name="mysite_foo", site = site, service=service)
+ slice.save()
+ slice.invalidate_cache()
+ self.assertTrue(slice.id > 0)
+ self.assertNotEqual(slice.service, None)
+ self.assertEqual(slice.service.id, service.id)
+ # now set it to None
+ slice.service = None
+ slice.save()
+ slice.invalidate_cache()
+ self.assertEqual(slice.service, None)
+
+
def test_generic_foreign_key_get(self):
orm = self.make_coreapi()
service = orm.Service(name="myservice")