Merge pull request #117 from jermowery/AddHelloWorldService

Add hello world service
diff --git a/xos/configurations/devel/Dockerfile.devel b/xos/configurations/devel/Dockerfile.devel
index 115b731..60c0143 100644
--- a/xos/configurations/devel/Dockerfile.devel
+++ b/xos/configurations/devel/Dockerfile.devel
@@ -4,6 +4,7 @@
 ADD xos/configurations/common/flat_net_name /root/setup/
 ADD xos/configurations/common/cloudlab-nodes.yaml /opt/xos/configurations/commmon/
 ADD xos/configurations/common/id_rsa.pub /root/setup/padmin_public_key
+ADD xos/configurations/common/id_rsa /opt/xos/observers/helloworldservice_complete/helloworldservice_private_key
 ADD xos/configurations/common/node_key.pub /root/setup/node_key.pub
 ADD xos/configurations/common/node_key /root/setup/node_key
 ADD xos/configurations/common/ceilometer_url /root/setup/ceilometer_url
diff --git a/xos/helloworldservice_complete/__init__.py b/xos/helloworldservice_complete/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/helloworldservice_complete/__init__.py
diff --git a/xos/helloworldservice_complete/admin.py b/xos/helloworldservice_complete/admin.py
new file mode 100644
index 0000000..9a3e4f7
--- /dev/null
+++ b/xos/helloworldservice_complete/admin.py
@@ -0,0 +1,141 @@
+
+from core.admin import ReadOnlyAwareAdmin, SliceInline
+from core.middleware import get_request
+from core.models import User
+from django import forms
+from django.contrib import admin
+from helloworldservice_complete.models import HelloWorldServiceComplete, HelloWorldTenantComplete, HELLO_WORLD_KIND
+
+# The class to provide an admin interface on the web for the service.
+# We do only configuration here and don't change any logic because the logic
+# is taken care of for us by ReadOnlyAwareAdmin
+class HelloWorldServiceCompleteAdmin(ReadOnlyAwareAdmin):
+    # We must set the model so that the admin knows what fields to use
+    model = HelloWorldServiceComplete
+    verbose_name = "Hello World Service"
+    verbose_name_plural = "Hello World Services"
+
+    # Setting list_display creates columns on the admin page, each value here
+    # is a column, the column is populated for every instance of the model.
+    list_display = ("backend_status_icon", "name", "enabled")
+
+    # Used to indicate which values in the columns of the admin form are links.
+    list_display_links = ('backend_status_icon', 'name', )
+
+    # Denotes the sections of the form, the fields in the section, and the
+    # CSS classes used to style them. We represent this as a set of tuples, each
+    # tuple as a name (or None) and a set of fields and classes.
+    # Here the first section does not have a name so we use none. That first
+    # section has several fields indicated in the 'fields' attribute, and styled
+    # by the classes indicated in the 'classes' attribute. The classes given
+    # here are important for rendering the tabs on the form. To give the tabs
+    # we must assign the classes suit-tab and suit-tab-<name> where
+    # where <name> will be used later.
+    fieldsets = [(None, {'fields': ['backend_status_text', 'name', 'enabled',
+                                    'versionNumber', 'description', "view_url"],
+                         'classes':['suit-tab suit-tab-general']})]
+
+    # Denotes the fields that are readonly and cannot be changed.
+    readonly_fields = ('backend_status_text', )
+
+    # Inlines are used to denote other models that can be edited on the same
+    # form as this one. In this case the service form also allows changes
+    # to slices.
+    inlines = [SliceInline]
+
+    extracontext_registered_admins = True
+
+    # Denotes the fields that can be changed by an admin but not be all users
+    user_readonly_fields = ["name", "enabled", "versionNumber", "description"]
+
+    # Associates fieldsets from this form and from the inlines.
+    # The format here are tuples, of (<name>, tab title). <name> comes from the
+    # <name> in the fieldsets.
+    suit_form_tabs = (('general', 'Hello World Service Details'),
+                      ('administration', 'Tenants'),
+                      ('slices', 'Slices'),)
+
+    # Used to include a template for a tab. Here we include the
+    # helloworldserviceadmin template in the top position for the administration
+    # tab.
+    suit_form_includes = (('helloworldserviceadmin.html',
+                           'top',
+                           'administration'),)
+
+    # Used to get the objects for this model that are associated with the
+    # requesting user.
+    def queryset(self, request):
+        return HelloWorldServiceComplete.get_service_objects_by_user(request.user)
+
+# Class to represent the form to add and edit tenants.
+# We need to define this instead of just using an admin like we did for the
+# service because tenants vary more than services and there isn't a common form.
+# This allows us to change the python behavior for the admin form to save extra
+# fields and control defaults.
+class HelloWorldTenantCompleteForm(forms.ModelForm):
+    # Defines a field for the creator of this service. It is a dropdown which
+    # is populated with all of the users.
+    creator = forms.ModelChoiceField(queryset=User.objects.all())
+    # Defines a text field for the display message, it is not required.
+    display_message = forms.CharField(required=False)
+
+    def __init__(self, *args, **kwargs):
+        super(HelloWorldTenantCompleteForm, self).__init__(*args, **kwargs)
+        # Set the kind field to readonly
+        self.fields['kind'].widget.attrs['readonly'] = True
+        # Define the logic for obtaining the objects for the provider_service
+        # dropdown of the tenant form.
+        self.fields[
+            'provider_service'].queryset = HelloWorldServiceComplete.get_service_objects().all()
+        # Set the initial kind to HELLO_WORLD_KIND for this tenant.
+        self.fields['kind'].initial = HELLO_WORLD_KIND
+        # If there is an instance of this model then we can set the initial
+        # form values to the existing values.
+        if self.instance:
+            self.fields['creator'].initial = self.instance.creator
+            self.fields[
+                'display_message'].initial = self.instance.display_message
+
+        # If there is not an instance then we need to set initial values.
+        if (not self.instance) or (not self.instance.pk):
+            self.fields['creator'].initial = get_request().user
+            if HelloWorldServiceComplete.get_service_objects().exists():
+                self.fields["provider_service"].initial = HelloWorldServiceComplete.get_service_objects().all()[0]
+
+    # This function describes what happens when the save button is pressed on
+    # the tenant form. In this case we set the values for the instance that were
+    # entered.
+    def save(self, commit=True):
+        self.instance.creator = self.cleaned_data.get("creator")
+        self.instance.display_message = self.cleaned_data.get(
+            "display_message")
+        return super(HelloWorldTenantCompleteForm, self).save(commit=commit)
+
+    class Meta:
+        model = HelloWorldTenantComplete
+
+# Define the admin form for the tenant. This uses a similar structure as the
+# service but uses HelloWorldTenantCompleteForm to change the python behavior.
+
+
+class HelloWorldTenantCompleteAdmin(ReadOnlyAwareAdmin):
+    verbose_name = "Hello World Tenant"
+    verbose_name_plural = "Hello World Tenants"
+    list_display = ('id', 'backend_status_icon', 'instance', 'display_message')
+    list_display_links = ('backend_status_icon', 'instance', 'display_message',
+                          'id')
+    fieldsets = [(None, {'fields': ['backend_status_text', 'kind',
+                                    'provider_service', 'instance', 'creator',
+                                    'display_message'],
+                         'classes': ['suit-tab suit-tab-general']})]
+    readonly_fields = ('backend_status_text', 'instance',)
+    form = HelloWorldTenantCompleteForm
+
+    suit_form_tabs = (('general', 'Details'),)
+
+    def queryset(self, request):
+        return HelloWorldTenantComplete.get_tenant_objects_by_user(request.user)
+
+# Associate the admin forms with the models.
+admin.site.register(HelloWorldServiceComplete, HelloWorldServiceCompleteAdmin)
+admin.site.register(HelloWorldTenantComplete, HelloWorldTenantCompleteAdmin)
diff --git a/xos/helloworldservice_complete/models.py b/xos/helloworldservice_complete/models.py
new file mode 100644
index 0000000..8a4ce59
--- /dev/null
+++ b/xos/helloworldservice_complete/models.py
@@ -0,0 +1,113 @@
+from core.models import Service, TenantWithContainer
+from django.db import transaction
+
+HELLO_WORLD_KIND = "helloworldservice_complete"
+
+# The class to represent the service. Most of the service logic is given for us
+# in the Service class but, we have some configuration that is specific for
+# this example.
+class HelloWorldServiceComplete(Service):
+    KIND = HELLO_WORLD_KIND
+
+    class Meta:
+        # When the proxy field is set to True the model is represented as
+        # it's superclass in the database, but we can still change the python
+        # behavior. In this case HelloWorldServiceComplete is a Service in the
+        # database.
+        proxy = True
+        # The name used to find this service, all directories are named this
+        app_label = "helloworldservice_complete"
+        verbose_name = "Hello World Service"
+
+# This is the class to represent the tenant. Most of the logic is given to use
+# in TenantWithContainer, however there is some configuration and logic that
+# we need to define for this example.
+class HelloWorldTenantComplete(TenantWithContainer):
+
+    class Meta:
+        # Same as a above, HelloWorldTenantComplete is represented as a
+        # TenantWithContainer, but we change the python behavior.
+        proxy = True
+        verbose_name = "Hello World Tenant"
+
+    # The kind of the service is used on forms to differentiate this service
+    # from the other services.
+    KIND = HELLO_WORLD_KIND
+
+    # Ansible requires that the sync_attributes field contain nat_ip and nat_mac
+    # these will be used to determine where to SSH to for ansible.
+    # Getters must be defined for every attribute specified here.
+    sync_attributes = ("nat_ip", "nat_mac",)
+
+    # default_attributes is used cleanly indicate what the default values for
+    # the fields are.
+    default_attributes = {'display_message': 'Hello World!'}
+
+    def __init__(self, *args, **kwargs):
+        helloworld_services = HelloWorldServiceComplete.get_service_objects().all()
+        # When the tenant is created the default service in the form is set
+        # to be the first created HelloWorldServiceComplete
+        if helloworld_services:
+            self._meta.get_field(
+                "provider_service").default = helloworld_services[0].id
+        super(HelloWorldTenantComplete, self).__init__(*args, **kwargs)
+
+    def save(self, *args, **kwargs):
+        super(HelloWorldTenantComplete, self).save(*args, **kwargs)
+        # This call needs to happen so that an instance is created for this
+        # tenant is created in the slice. One instance is created per tenant.
+        model_policy_helloworld_tenant(self.pk)
+
+    def delete(self, *args, **kwargs):
+        # Delete the instance that was created for this tenant
+        self.cleanup_container()
+        super(HelloWorldTenantComplete, self).delete(*args, **kwargs)
+
+    # Getter for the message that will appear on the webpage
+    # By default it is "Hello World!"
+    @property
+    def display_message(self):
+        return self.get_attribute(
+            "display_message",
+            self.default_attributes['display_message'])
+
+    # Setter for the message that will appear on the webpage
+    @display_message.setter
+    def display_message(self, value):
+        self.set_attribute("display_message", value)
+
+    @property
+    def addresses(self):
+        if (not self.id) or (not self.instance):
+            return {}
+
+        addresses = {}
+        # The ports field refers to networks for the instance.
+        # This loop stores the details for the NAT network that will be
+        # necessary for ansible.
+        for ns in self.instance.ports.all():
+            if "nat" in ns.network.name.lower():
+                addresses["nat"] = (ns.ip, ns.mac)
+        return addresses
+
+    # This getter is necessary because nat_ip is a sync_attribute
+    @property
+    def nat_ip(self):
+        return self.addresses.get("nat", (None, None))[0]
+
+    # This getter is necessary because nat_mac is a sync_attribute
+    @property
+    def nat_mac(self):
+        return self.addresses.get("nat", (None, None))[1]
+
+
+def model_policy_helloworld_tenant(pk):
+    # This section of code is atomic to prevent race conditions
+    with transaction.atomic():
+        # We find all of the tenants that are waiting to update
+        tenant = HelloWorldTenantComplete.objects.select_for_update().filter(pk=pk)
+        if not tenant:
+            return
+        # Since this code is atomic it is safe to always use the first tenant
+        tenant = tenant[0]
+        tenant.manage_container()
diff --git a/xos/helloworldservice_complete/templates/helloworldserviceadmin.html b/xos/helloworldservice_complete/templates/helloworldserviceadmin.html
new file mode 100644
index 0000000..ba418ee
--- /dev/null
+++ b/xos/helloworldservice_complete/templates/helloworldserviceadmin.html
@@ -0,0 +1,10 @@
+<!-- Template used to for the button leading to the HelloWorldTenantComplete form. -->
+<div class = "left-nav">
+  <ul>
+    <li>
+      <a href="/admin/helloworldservice_complete/helloworldtenantcomplete/">
+        Hello World Tenants
+      </a>
+    </li>
+  </ul>
+</div>
diff --git a/xos/observers/helloworldservice_complete/helloworldservice-observer.py b/xos/observers/helloworldservice_complete/helloworldservice-observer.py
new file mode 100755
index 0000000..75dcc46
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/helloworldservice-observer.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+# Runs the standard XOS observer
+
+import importlib
+import os
+import sys
+observer_path = os.path.join(os.path.dirname(
+    os.path.realpath(__file__)), "../..")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-observer")
+mod.main()
diff --git a/xos/observers/helloworldservice_complete/helloworldservice_config b/xos/observers/helloworldservice_complete/helloworldservice_config
new file mode 100644
index 0000000..716e3a0
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/helloworldservice_config
@@ -0,0 +1,36 @@
+# Required by XOS
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+# Required by XOS
+[api]
+nova_enabled=True
+
+# Sets options for the observer
+[observer]
+# Optional name
+name=helloworldservice
+# This is the location to the dependency graph you generate
+dependency_graph=/opt/xos/observers/helloworldservice_complete/model-deps
+# The location of your SyncSteps
+steps_dir=/opt/xos/observers/helloworldservice_complete/steps
+# A temporary directory that will be used by ansible
+sys_dir=/opt/xos/observers/helloworldservice_complete/sys
+# Location of the file to save logging messages to the backend log is often used
+logfile=/var/log/xos_backend.log
+# If this option is true, then nothing will change, we simply pretend to run
+pretend=False
+# If this is False then XOS will use an exponential backoff when the observer
+# fails, since we will be waiting for an instance, we don't want this.
+backoff_disabled=True
+# We want the output from ansible to be logged
+save_ansible_output=True
+# This determines how we SSH to a client, if this is set to True then we try
+# to ssh using the instance name as a proxy, if this is disabled we ssh using
+# the NAT IP of the instance. On CloudLab the first option will fail so we must
+# set this to False
+proxy_ssh=False
diff --git a/xos/observers/helloworldservice_complete/model-deps b/xos/observers/helloworldservice_complete/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/observers/helloworldservice_complete/run.sh b/xos/observers/helloworldservice_complete/run.sh
new file mode 100755
index 0000000..6bce079
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/run.sh
@@ -0,0 +1,3 @@
+# Runs the XOS observer using helloworldservice_config
+export XOS_DIR=/opt/xos
+python helloworldservice-observer.py  -C $XOS_DIR/observers/helloworldservice_complete/helloworldservice_config
diff --git a/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.py b/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.py
new file mode 100644
index 0000000..a4b5435
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.py
@@ -0,0 +1,48 @@
+import os
+import sys
+from django.db.models import Q, F
+from helloworldservice_complete.models import HelloWorldServiceComplete, HelloWorldTenantComplete
+from observers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+# Class to define how we sync a tenant. Using SyncInstanceUsingAnsible we
+# indicate where the find the YAML for ansible, where to find the SSH key,
+# and the logic for determining what tenant needs updating, what additional
+# attributes are needed, and how to delete an instance.
+class SyncHelloWorldTenantComplete(SyncInstanceUsingAnsible):
+    # Indicates the position in the data model, this will run when XOS needs to
+    # enact a HelloWorldTenantComplete
+    provides = [HelloWorldTenantComplete]
+    # The actual model being enacted, usually the same as provides.
+    observes = HelloWorldTenantComplete
+    # Number of milliseconds between interruptions of the observer
+    requested_interval = 0
+    # The ansible template to run
+    template_name = "sync_helloworldtenant.yaml"
+    # The location of the SSH private key to use when ansible connects to
+    # instances.
+    service_key_name = "/opt/xos/observers/helloworldservice_complete/helloworldservice_private_key"
+
+    def __init__(self, *args, **kwargs):
+        super(SyncHelloWorldTenantComplete, self).__init__(*args, **kwargs)
+
+    # Defines the logic for determining what HelloWorldTenantCompletes need to be
+    # enacted.
+    def fetch_pending(self, deleted):
+        # If the update is not a deletion, then we get all of the instnaces that
+        # have been updated or have not been enacted.
+        if (not deleted):
+            objs = HelloWorldTenantComplete.get_tenant_objects().filter(
+                Q(enacted__lt=F('updated')) | Q(enacted=None), Q(lazy_blocked=False))
+        else:
+            # If this is a deletion we get all of the deleted tenants..
+            objs = HelloWorldTenantComplete.get_deleted_tenant_objects()
+
+        return objs
+
+    # 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):
+        return {"display_message": o.display_message}
diff --git a/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.yaml b/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.yaml
new file mode 100644
index 0000000..719c75f
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/steps/sync_helloworldtenant.yaml
@@ -0,0 +1,18 @@
+---
+- hosts: {{ instance_name }}
+  gather_facts: False
+  connection: ssh
+  user: ubuntu
+  sudo: yes
+  tasks:
+  - name: install apache
+    apt: name=apache2 state=present update_cache=yes
+
+  - name: write message
+    shell: echo "{{ display_message }}" > /var/www/html/index.html
+
+  - name: stop apache
+    service: name=apache2 state=stopped
+
+  - name: start apache
+    service: name=apache2 state=started
diff --git a/xos/observers/helloworldservice_complete/stop.sh b/xos/observers/helloworldservice_complete/stop.sh
new file mode 100755
index 0000000..76e68d9
--- /dev/null
+++ b/xos/observers/helloworldservice_complete/stop.sh
@@ -0,0 +1,2 @@
+# Kill the observer
+pkill -9 -f helloworldservice-observer.py
diff --git a/xos/scripts/opencloud b/xos/scripts/opencloud
index 189a673..6b3e737 100755
--- a/xos/scripts/opencloud
+++ b/xos/scripts/opencloud
@@ -144,6 +144,7 @@
     python ./manage.py makemigrations syndicate_storage
     python ./manage.py makemigrations cord
     python ./manage.py makemigrations ceilometer
+    python ./manage.py makemigrations helloworldservice_complete
     python ./manage.py makemigrations onos
     #python ./manage.py makemigrations servcomp
 }
diff --git a/xos/xos/settings.py b/xos/xos/settings.py
index ff586a2..fc2ad2f 100644
--- a/xos/xos/settings.py
+++ b/xos/xos/settings.py
@@ -153,6 +153,7 @@
     'core',
     'hpc',
     'cord',
+    'helloworldservice_complete',
     'services.onos',
     'ceilometer',
     'requestrouter',