CORD-3195 update ExampleService to use new OpenStack modeling

Change-Id: I4982623294ed3ae19bcc39ffc25ae288b55ad94e
diff --git a/VERSION b/VERSION
index 227cea2..e967cc4 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.0.0
+2.1.0-dev0
diff --git a/docs/example-service.md b/docs/example-service.md
index d191f58..2104854 100644
--- a/docs/example-service.md
+++ b/docs/example-service.md
@@ -1,8 +1,8 @@
 # ExampleService #
 
-`ExampleService` is a service intended to demonstrate integration with the XOS `openstack` service. `ExampleService` provides a `ExampleServiceInstance` model that generates and hosts a web page, displaying two text strings on the web page: a `service_message` and a `tenant_message`. Each time a `ExampleServiceInstance` is created, a corresponding `Instance` will also be created which will in turn cause an OpenStack VM to be created that runs an apache web server hosting the web page.
+`ExampleService` is a service intended to demonstrate integration with the XOS `openstack` service. `ExampleService` provides a `ExampleServiceInstance` model that generates and hosts a web page, displaying two text strings on the web page: a `service_message` and a `tenant_message`. Each time a `ExampleServiceInstance` is created, a corresponding `OpenStackServiceInstance` will also be created which will in turn cause an OpenStack VM to be created that runs an apache web server hosting the web page.
 
-Destroying the `ExampleServiceInstance` will cause the linked `Instance` to also be destroyed, which will in turn cause the OpenStack VM to be cleaned up.
+Destroying the `ExampleServiceInstance` will cause the linked `OpenStackServiceInstance` to also be destroyed, which will in turn cause the OpenStack VM to be cleaned up.
 
 ## Implementation ##
 
@@ -14,11 +14,11 @@
 
     * `ExampleServiceInstance` holds per-tenant settings, including a `tenant_message`. Each `ExampleServiceInstance` corresponds to one web server serving one web page. This model has relations for `foreground_color` and `background_color` that allow some additional customization of the served page. `tenant_secret` is a secret that is installed into the container running the web server
 
-    * `Color` implements the color model used by the `foreground_color` and `background_color` fields of `SimpleExampleServiceInstance`.
+    * `Color` implements the color model used by the `foreground_color` and `background_color` fields of `ExampleServiceInstance`.
 
     * `EmbeddedImage` allows embedded images to be attached to web pages. As the foreign key relation is from the embedded image to the service instance, this forms a many-to-one relation that allows many images to be attached to a single web page. 
 
-2. The `model_policies` directory contains a model policy. This model policy is reponsible for automatically creating and deleting the `Instance` associated with each `ExampleServiceInstance`. 
+2. The `model_policies` directory contains a model policy. This model policy is reponsible for automatically creating and deleting the `OpenStackServiceInstance` associated with each `ExampleServiceInstance`. 
 
 3. The `sync_steps` directory contains a sync step that uses Ansible to provision the web server and configure the web page.
 
@@ -28,7 +28,7 @@
 
 ### Prerequisites ###
 
-This document assumes that you have already installed [OpenStack-helm](../prereqs/openstack-helm). 
+This document assumes that you have already installed [OpenStack-helm](../prereqs/openstack-helm.md). 
 
 > Note: Depending on the method that was used to deploy your Kubernetes installation, your installation may require root privilege to interact with Kubernetes. If so, then you may need to use `sudo` with many of the commands in this tutorial, for example `sudo helm init` instead of `helm init`. 
 
diff --git a/xos/synchronizer/model_policies/model_policy_exampleserviceinstance.py b/xos/synchronizer/model_policies/model_policy_exampleserviceinstance.py
index 8244204..5e20122 100644
--- a/xos/synchronizer/model_policies/model_policy_exampleserviceinstance.py
+++ b/xos/synchronizer/model_policies/model_policy_exampleserviceinstance.py
@@ -14,8 +14,56 @@
 # limitations under the License.
 
 
-from synchronizers.new_base.modelaccessor import *
-from synchronizers.new_base.model_policies.model_policy_tenantwithcontainer import TenantWithContainerPolicy
+from synchronizers.new_base.modelaccessor import OpenStackService, Service
+from synchronizers.new_base.policy import Policy
+from synchronizers.new_base.model_policies.model_policy_tenantwithcontainer import LeastLoadedNodeScheduler
 
-class ExampleServiceInstancePolicy(TenantWithContainerPolicy):
+class ExampleServiceInstancePolicy(Policy):
     model_name = "ExampleServiceInstance"
+
+    def handle_create(self, service_instance):
+        return self.handle_update(service_instance)
+
+    def handle_update(self, service_instance):
+        if not service_instance.compute_instance:
+            # TODO: Break dependency
+            compute_service = OpenStackService.objects.first()
+            compute_service_instance_class = Service.objects.get(id=compute_service.id).get_service_instance_class()
+
+            exampleservice = service_instance.owner.leaf_model
+
+            # TODO: What if there is the wrong number of slices?
+            slice = exampleservice.slices.first()
+
+            # TODO: What if there is no default image?
+            image = slice.default_image
+
+            # TODO: What if there is no default flavor?
+            flavor = slice.default_flavor
+
+            if slice.default_node:
+                node = slice.default_node
+            else:
+                scheduler = LeastLoadedNodeScheduler
+                # TODO(smbaker): Labeling and constraints
+                (node, parent) = scheduler(slice).pick()
+
+            name="exampleserviceinstance-%s" % service_instance.id
+            compute_service_instance = compute_service_instance_class(slice=slice,
+                                                                      owner=compute_service,
+                                                                      image=image,
+                                                                      flavor=flavor,
+                                                                      name=name,
+                                                                      node=node)
+            compute_service_instance.save()
+
+            service_instance.compute_instance = compute_service_instance
+            service_instance.save(update_fields=["compute_instance"])
+
+    def handle_delete(self, service_instance):
+        if service_instance.compute_instance:
+            service_instance.compute_instance.delete()
+            service_instance.compute_instance = None
+            # TODO: I'm not sure we can save things that are being deleted...
+            service_instance.save(update_fields=["compute_instance"])
+
diff --git a/xos/synchronizer/models/exampleservice.xproto b/xos/synchronizer/models/exampleservice.xproto
index 0820c9e..e643001 100644
--- a/xos/synchronizer/models/exampleservice.xproto
+++ b/xos/synchronizer/models/exampleservice.xproto
@@ -12,7 +12,11 @@
      required string html_code = 2 [help_text = "Code for this color", db_index = False, max_length = 256, null = False, blank = False];
 }
 
-message ExampleServiceInstance (TenantWithContainer){
+message ServiceInstanceWithCompute (ServiceInstance) {
+    optional manytoone compute_instance->ComputeServiceInstance:service_instance_with_computes = 1 [db_index=True, null=True, blank=True];
+}
+
+message ExampleServiceInstance (ServiceInstanceWithCompute){
      option verbose_name = "Example Service Instance";
      required string tenant_message = 1 [help_text = "Tenant Message to Display", max_length = 254, null = False, db_index = False, blank = False];
      optional manytoone foreground_color->Color:serviceinstance_foreground_colors = 3 [db_index = True, null = True, blank = True];
diff --git a/xos/synchronizer/steps/sync_exampleserviceinstance.py b/xos/synchronizer/steps/sync_exampleserviceinstance.py
index 373fb5e..fb54d62 100644
--- a/xos/synchronizer/steps/sync_exampleserviceinstance.py
+++ b/xos/synchronizer/steps/sync_exampleserviceinstance.py
@@ -16,16 +16,111 @@
 
 import os
 import sys
-from synchronizers.new_base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
-from synchronizers.new_base.modelaccessor import *
-from xos.logger import Logger, logging
+import time
+from synchronizers.new_base.syncstep import SyncStep, DeferredException
+from synchronizers.new_base.ansible_helper import run_template_ssh
+from synchronizers.new_base.modelaccessor import ExampleServiceInstance
+from xosconfig import Config
+from multistructlog import create_logger
 
-parentdir = os.path.join(os.path.dirname(__file__), "..")
-sys.path.insert(0, parentdir)
+log = create_logger(Config().get('logging'))
 
-logger = Logger(level=logging.INFO)
+# TODO(smbaker): Move this to the core
+class SyncServiceInstanceWithComputeUsingAnsible(SyncStep):
+    def __init__(self, *args, **kwargs):
+        SyncStep.__init__(self, *args, **kwargs)
 
-class SyncExampleServiceInstance(SyncInstanceUsingAnsible):
+    def defer_sync(self, o, reason):
+        log.info("defer object", object = str(o), reason = reason, **o.tologdict())
+        raise DeferredException("defer object %s due to %s" % (str(o), reason))
+
+    def get_extra_attributes(self, o):
+        # This is a place to include extra attributes that aren't part of the
+        # object itself.
+
+        return {}
+
+    def run_playbook(self, o, fields, template_name=None):
+        if not template_name:
+            template_name = self.template_name
+        tStart = time.time()
+        run_template_ssh(template_name, fields, object=o)
+        log.info("playbook execution time", time=int(time.time() - tStart), **o.tologdict())
+
+    def get_ssh_ip(self, instance):
+        for port in instance.ports.all():
+            if port.network.template and port.network.template.vtn_kind == "MANAGEMENT_LOCAL":
+                return port.ip
+
+        for port in instance.ports.all():
+            if port.network.template and port.network.template.vtn_kind == "MANAGEMENT_HOST":
+                return port.ip
+
+        return None
+
+    def get_ansible_fields(self, instance):
+        # return all of the fields that tell Ansible how to talk to the context
+        # that's setting up the container.
+
+        # Cast to the leaf_model. For OpenStackServiceInstance, this will allow us access to fields like "node"
+        instance = instance.leaf_model
+
+        node = getattr(instance, "node")
+        if not node:
+            raise Exception("Instance has no node for instance %s" % str(instance))
+
+        if not instance.slice:
+            raise Exception("Instance has no slice for instance %s" % str(instance))
+
+        if not instance.slice.service:
+            raise Exception("Instance's slice has no service for instance %s" % str(instance))
+
+        if not instance.slice.service.private_key_fn:
+            raise Exception("Instance's slice's service has no private_key_fn for instance %s" % str(instance))
+
+        key_name = instance.slice.service.private_key_fn
+        if not os.path.exists(key_name):
+            raise Exception("Node key %s does not exist for instance %s" % (key_name, str(instance)))
+
+        ssh_ip = self.get_ssh_ip(instance)
+        if not ssh_ip:
+            raise Exception("Unable to determine ssh ip for instance %s" % str(instance))
+
+        key = file(key_name).read()
+
+        fields = {"instance_name": instance.name,
+                  "hostname": node.name,
+                  "username": "ubuntu",
+                  "ssh_ip": ssh_ip,
+                  "private_key": key,
+                  "instance_id": "none", # is not used for proxy-ssh ansible connections
+                  }
+
+        return fields
+
+    def sync_record(self, o):
+        log.info("sync'ing object", object=str(o), **o.tologdict())
+
+        compute_service_instance = o.compute_instance
+        if not compute_service_instance:
+            self.defer_sync(o, "waiting on instance")
+            return
+
+        if not compute_service_instance.backend_handle:
+            self.defer_sync(o, "waiting on instance.backend_handle")
+            return
+
+        fields = self.get_ansible_fields(compute_service_instance)
+
+        fields["ansible_tag"] = getattr(o, "ansible_tag", o.__class__.__name__ + "_" + str(o.id))
+
+        fields.update(self.get_extra_attributes(o))
+
+        self.run_playbook(o, fields)
+
+        o.save()
+
+class SyncExampleServiceInstance(SyncServiceInstanceWithComputeUsingAnsible):
 
     provides = [ExampleServiceInstance]
 
@@ -35,30 +130,15 @@
 
     template_name = "exampleserviceinstance_playbook.yaml"
 
-    service_key_name = "/opt/xos/synchronizers/exampleservice/exampleservice_private_key"
-
-    watches = [ModelLink(ServiceDependency,via='servicedependency'), ModelLink(ServiceMonitoringAgentInfo,via='monitoringagentinfo')]
-
     def __init__(self, *args, **kwargs):
         super(SyncExampleServiceInstance, self).__init__(*args, **kwargs)
 
-    def get_exampleservice(self, o):
-        if not o.owner:
-            return None
-
-        exampleservice = ExampleService.objects.filter(id=o.owner.id)
-
-        if not exampleservice:
-            return None
-
-        return exampleservice[0]
-
     # Gets the attributes that are used by the Ansible template but are not
     # part of the set of default attributes.
     def get_extra_attributes(self, o):
         fields = {}
         fields['tenant_message'] = o.tenant_message
-        exampleservice = self.get_exampleservice(o)
+        exampleservice = o.owner.leaf_model
         fields['service_message'] = exampleservice.service_message
 
         if o.foreground_color:
@@ -67,7 +147,7 @@
         if o.background_color:
             fields["background_color"] = o.background_color.html_code
 
-        images=[]
+        images = []
         for image in o.embedded_images.all():
             images.append({"name": image.name,
                            "url": image.url})
@@ -80,33 +160,3 @@
         # when the instance holding the exampleservice is deleted.
         pass
 
-    def handle_service_monitoringagentinfo_watch_notification(self, monitoring_agent_info):
-        if not monitoring_agent_info.service:
-            logger.info("handle watch notifications for service monitoring agent info...ignoring because service attribute in monitoring agent info:%s is null" % (monitoring_agent_info))
-            return
-
-        if not monitoring_agent_info.target_uri:
-            logger.info("handle watch notifications for service monitoring agent info...ignoring because target_uri attribute in monitoring agent info:%s is null" % (monitoring_agent_info))
-            return
-
-        objs = ExampleServiceInstance.objects.all()
-        for obj in objs:
-            if obj.owner.id != monitoring_agent_info.service.id:
-                logger.info("handle watch notifications for service monitoring agent info...ignoring because service attribute in monitoring agent info:%s is not matching" % (monitoring_agent_info))
-                return
-
-            instance = self.get_instance(obj)
-            if not instance:
-               logger.warn("handle watch notifications for service monitoring agent info...: No valid instance found for object %s" % (str(obj)))
-               return
-
-            logger.info("handling watch notification for monitoring agent info:%s for ExampleServiceInstance object:%s" % (monitoring_agent_info, obj))
-
-            #Run ansible playbook to update the routing table entries in the instance
-            fields = self.get_ansible_fields(instance)
-            fields["ansible_tag"] =  obj.__class__.__name__ + "_" + str(obj.id) + "_monitoring"
-            fields["target_uri"] = monitoring_agent_info.target_uri
-
-            template_name = "monitoring_agent.yaml"
-            super(SyncExampleServiceInstance, self).run_playbook(obj, fields, template_name)
-        pass