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