CORD-1393 add allocate_public_service_instance to tenantwithcontainer
store policy status in db

Change-Id: I4a33abc3f237d3261d88fba098a3a089157b1961
diff --git a/xos/core/admin.py b/xos/core/admin.py
index f4f591a..d73320b 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -72,7 +72,7 @@
     (icon, tooltip) = obj.get_backend_icon()
     icon_url = ICON_URLS.get(icon, "unknown")
 
-    return '<img src="%s"> %s' % (icon_url, tooltip)
+    return '<img src="%s"> POLICY=%s SYNC=%s' % (icon_url, obj.policy_status, tooltip)
 
 
 class UploadTextareaWidget(AdminTextareaWidget):
diff --git a/xos/core/models/core.xproto b/xos/core/models/core.xproto
index 197adc9..c460261 100644
--- a/xos/core/models/core.xproto
+++ b/xos/core/models/core.xproto
@@ -15,6 +15,7 @@
      required bool lazy_blocked = 11 [default = False];
      required bool no_sync = 12 [default = False];
      required bool no_policy = 13 [default = False];
+     optional string policy_status = 14 [default = "0 - Policy in process", max_length = 1024];
 }
 
 message User (AbstractBaseUser,PlModelMixIn) {
diff --git a/xos/synchronizers/new_base/exceptions.py b/xos/synchronizers/new_base/exceptions.py
new file mode 100644
index 0000000..5aa8583
--- /dev/null
+++ b/xos/synchronizers/new_base/exceptions.py
@@ -0,0 +1,8 @@
+class SynchronizerException(Exception):
+    pass
+
+class SynchronizerProgrammingError(SynchronizerException): 
+    pass
+
+class SynchronizerConfigurationError(SynchronizerException):
+    pass
diff --git a/xos/synchronizers/new_base/model_policies/model_policy_tenantwithcontainer.py b/xos/synchronizers/new_base/model_policies/model_policy_tenantwithcontainer.py
index b94e6b8..7a1b098 100644
--- a/xos/synchronizers/new_base/model_policies/model_policy_tenantwithcontainer.py
+++ b/xos/synchronizers/new_base/model_policies/model_policy_tenantwithcontainer.py
@@ -1,5 +1,6 @@
 from synchronizers.new_base.modelaccessor import *
 from synchronizers.new_base.policy import Policy
+from synchronizers.new_base.exceptions import *
 
 class Scheduler(object):
     # XOS Scheduler Abstract Base Class
@@ -70,17 +71,63 @@
         # such as creating ports for containers.
         instance.save()
 
+    def ip_to_mac(self, ip):
+        (a, b, c, d) = ip.split('.')
+        return "02:42:%02x:%02x:%02x:%02x" % (int(a), int(b), int(c), int(d))
+
+    def allocate_public_service_instance(self, **kwargs):
+        """ Get a ServiceInstance that provides public connectivity. Currently this means to use AddressPool and
+            VRouterTenant.
+
+            Expect this to be refactored when we break hard-coded service dependencies.
+        """
+        address_pool_name = kwargs.pop("address_pool_name")
+
+        vrouter_service = VRouterService.objects.all()  # TODO: Hardcoded dependency
+        if not vrouter_service:
+            raise Exception("no vrouter services")
+        vrouter_service = vrouter_service[0]
+
+        ap = AddressPool.objects.filter(name=address_pool_name, service_id=vrouter_service.id)
+        if not ap:
+            raise Exception("vRouter unable to find addresspool %s" % name)
+        ap = ap[0]
+
+        ip = ap.get_address()
+        if not ip:
+            raise Exception("AddressPool '%s' has run out of addresses." % ap.name)
+
+        ap.save()  # save the AddressPool to account for address being removed from it
+
+        # TODO: potential partial failure -- AddressPool address is allocated and saved before vrouter tenant
+
+        t = None
+        try:
+            t = VRouterTenant(provider_service=vrouter_service, **kwargs)    # TODO: Hardcoded dependency
+            t.public_ip = ip
+            t.public_mac = self.ip_to_mac(ip)
+            t.address_pool_id = ap.id
+            t.save()
+        except:
+            # cleanup if anything went wrong
+            ap.put_address(ip)
+            if (t and t.id):
+                t.delete()
+            raise
+
+        return t
+
     def get_image(self, tenant):
         slice = tenant.provider_service.slices.all()
         if not slice:
-            raise XOSProgrammingError("provider service has no slice")
+            raise SynchronizerProgrammingError("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)
+        raise SynchronizerProgrammingError("Please set a default image for %s" % self.slice.name)
 
     """ get_legacy_tenant_attribute
         pick_least_loaded_instance_in_slice
@@ -130,7 +177,7 @@
 
         if tenant.instance is None:
             if not tenant.provider_service.slices.count():
-                raise XOSConfigurationError("The service has no slices")
+                raise SynchronizerConfigurationError("The service has no slices")
 
             new_instance_created = False
             instance = None
@@ -146,7 +193,7 @@
                 if not flavor:
                     flavors = Flavor.objects.filter(name="m1.small")
                     if not flavors:
-                        raise XOSConfigurationError("No m1.small flavor")
+                        raise SynchronizerConfigurationError("No m1.small flavor")
                     flavor = flavors[0]
 
                 if slice.default_isolation == "container_vm":
diff --git a/xos/synchronizers/new_base/model_policies/test_config.yaml b/xos/synchronizers/new_base/model_policies/test_config.yaml
new file mode 100644
index 0000000..c05965e
--- /dev/null
+++ b/xos/synchronizers/new_base/model_policies/test_config.yaml
@@ -0,0 +1,4 @@
+name: test-model-policies
+accessor:
+  username: xosadmin@opencord.org
+  password: "sample"
diff --git a/xos/synchronizers/new_base/model_policies/test_model_policy_tenantwithcontainer.py b/xos/synchronizers/new_base/model_policies/test_model_policy_tenantwithcontainer.py
new file mode 100644
index 0000000..b137c44
--- /dev/null
+++ b/xos/synchronizers/new_base/model_policies/test_model_policy_tenantwithcontainer.py
@@ -0,0 +1,35 @@
+import unittest
+from mock import patch
+import mock
+
+import os, sys
+sys.path.append("../../..")
+sys.path.append("../../new_base/model_policies")
+config = basic_conf = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/test_config.yaml")
+from xosconfig import Config
+Config.init(config, 'synchronizer-config-schema.yaml')
+
+import synchronizers.new_base.modelaccessor
+
+from model_policy_tenantwithcontainer import TenantWithContainerPolicy
+
+class MockTenant:
+    provider_service = None
+    deleted = False
+    instance = None
+    service_specific_attribute = {}
+
+class TestModelPolicyVsgTenant(unittest.TestCase):
+    def setUp(self):
+        self.policy = TenantWithContainerPolicy()
+        self.tenant = MockTenant()
+        
+    @patch.object(MockTenant, "provider_service")
+    def test_manage_container_no_slices(self, provider_service):
+        provider_service.slices.count.return_value = 0
+        with self.assertRaises(Exception) as e:
+            self.policy.manage_container(self.tenant)
+        self.assertEqual(e.exception.message, "The service has no slices")
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/xos/synchronizers/new_base/model_policy_loop.py b/xos/synchronizers/new_base/model_policy_loop.py
index 74fb19a..48641b7 100644
--- a/xos/synchronizers/new_base/model_policy_loop.py
+++ b/xos/synchronizers/new_base/model_policy_loop.py
@@ -62,6 +62,9 @@
     def load_model_policies(self, policies_dir):
         policies=[]
         for fn in os.listdir(policies_dir):
+                if fn.startswith("test"):
+                    # don't try to import unit tests!
+                    continue
                 pathname = os.path.join(policies_dir,fn)
                 if os.path.isfile(pathname) and fn.endswith(".py") and (fn!="__init__.py"):
                     module = imp.load_source(fn[:-3], pathname)
@@ -89,6 +92,7 @@
         # These are the models whose children get deleted when they are
         delete_policy_models = ['Slice','Instance','Network']
         sender_name = getattr(instance, "model_name", instance.__class__.__name__)
+        new_policed = model_accessor.now()
 
         #if (action != "deleted"):
         #    walk_inv_deps(self.update_dep, instance)
@@ -108,10 +112,17 @@
                     logger.log_exc("MODEL POLICY: Exception when running handler")
                     policies_failed = True
 
+                    try:
+                        instance.policy_status = "2 - %s" % traceback.format_exc(limit=1)
+                        instance.save(update_fields=["policy_status"])
+                    except:
+                        logger.log_exc("MODEL_POLICY: Exception when storing policy_status")
+
         if not policies_failed:
             try:
-                instance.policed=model_accessor.now()
-                instance.save(update_fields=['policed'])
+                instance.policed=new_policed
+                instance.policy_status = "1 - done"
+                instance.save(update_fields=['policed', 'policy_status'])
             except:
                 logger.log_exc('MODEL POLICY: Object %r failed to update policed timestamp' % instance)
 
diff --git a/xos/xos_client/xosapi/convenience/addresspool.py b/xos/xos_client/xosapi/convenience/addresspool.py
new file mode 100644
index 0000000..1653ec8
--- /dev/null
+++ b/xos/xos_client/xosapi/convenience/addresspool.py
@@ -0,0 +1,48 @@
+from xosapi.orm import ORMWrapper, register_convenience_wrapper
+
+class ORMWrapperAddressPool(ORMWrapper):
+    def get_address(self):
+        ap = self
+        if ap.addresses:
+            avail_ips = ap.addresses.split()
+        else:
+            avail_ips = []
+
+        if ap.inuse:
+            inuse_ips = ap.inuse.split()
+        else:
+            inuse_ips = []
+
+        while avail_ips:
+            addr = avail_ips.pop(0)
+
+            if addr in inuse_ips:
+                # This may have happened if someone re-ran the tosca
+                # recipe and 'refilled' the AddressPool while some addresses
+                # were still in use.
+                continue
+
+            inuse_ips.insert(0, addr)
+
+            ap.inuse = " ".join(inuse_ips)
+            ap.addresses = " ".join(avail_ips)
+            return addr
+
+        return None
+
+
+    def put_address(self, addr):
+        ap = self
+        addresses = ap.addresses or ""
+        parts = addresses.split()
+        if addr not in parts:
+            parts.insert(0, addr)
+            ap.addresses = " ".join(parts)
+
+        inuse = ap.inuse or ""
+        parts = inuse.split()
+        if addr in parts:
+            parts.remove(addr)
+            ap.inuse = " ".join(parts)
+
+register_convenience_wrapper("AddressPool", ORMWrapperAddressPool)
diff --git a/xos/xos_client/xosapi/convenience/vroutertenant.py b/xos/xos_client/xosapi/convenience/vroutertenant.py
index a4cd825..0a59b85 100644
--- a/xos/xos_client/xosapi/convenience/vroutertenant.py
+++ b/xos/xos_client/xosapi/convenience/vroutertenant.py
@@ -30,6 +30,7 @@
         return None
 
     # Use for tenant_for_instance_id
+    # TODO: These should be reimplemented using real database models
 
     def get_attribute(self, name, default=None):
         if self.service_specific_attribute:
@@ -38,5 +39,13 @@
             attributes = {}
         return attributes.get(name, default)
 
+    def set_attribute(self, name, value):
+        if self.service_specific_attribute:
+            attributes = json.loads(self.service_specific_attribute)
+        else:
+            attributes = {}
+        attributes[name] = value
+        self.service_specific_attribute = json.dumps(attributes)
+
 
 register_convenience_wrapper("VRouterTenant", ORMWrapperVRouterTenant)
diff --git a/xos/xos_client/xosapi/orm.py b/xos/xos_client/xosapi/orm.py
index 2665500..ce815b4 100644
--- a/xos/xos_client/xosapi/orm.py
+++ b/xos/xos_client/xosapi/orm.py
@@ -502,6 +502,7 @@
 
     return cls(wrapped_class, *args, **kwargs)
 
+import convenience.addresspool
 import convenience.instance
 import convenience.cordsubscriberroot
 import convenience.volttenant