Revert "Delete observers dir"
This reverts commit 7c5e0cae8320fa42c5127eea2ea3acf0e3a596ce.
diff --git a/xos/observers/__init__.py b/xos/observers/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/observers/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/observers/base/SyncInstanceUsingAnsible.py b/xos/observers/base/SyncInstanceUsingAnsible.py
new file mode 100644
index 0000000..aafbd85
--- /dev/null
+++ b/xos/observers/base/SyncInstanceUsingAnsible.py
@@ -0,0 +1,221 @@
+import hashlib
+import os
+import socket
+import sys
+import base64
+import time
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from core.models import Service, Slice, ControllerSlice, ControllerUser
+from util.logger import Logger, logging
+
+logger = Logger(level=logging.INFO)
+
+class SyncInstanceUsingAnsible(SyncStep):
+ # All of the following should be defined for classes derived from this
+ # base class. Examples below use VCPETenant.
+
+ # provides=[VCPETenant]
+ # observes=VCPETenant
+ # requested_interval=0
+ # template_name = "sync_vcpetenant.yaml"
+ # service_key_name = "/opt/xos/observers/vcpe/vcpe_private_key"
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+
+ def defer_sync(self, o, reason):
+ logger.info("defer object %s due to %s" % (str(o), reason))
+ raise Exception("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 get_instance(self, o):
+ # We need to know what instance is associated with the object. Let's
+ # assume 'o' has a field called 'instance'. If the field is called
+ # something else, or if custom logic is needed, then override this
+ # method.
+
+ return o.instance
+
+ 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)
+ logger.info("playbook execution time %d" % int(time.time()-tStart))
+
+ def pre_sync_hook(self, o, fields):
+ pass
+
+ def post_sync_hook(self, o, fields):
+ pass
+
+ def sync_fields(self, o, fields):
+ self.run_playbook(o, fields)
+
+ def prepare_record(self, o):
+ pass
+
+ def get_node(self,o):
+ return o.node
+
+ def get_node_key(self, node):
+ return "/root/setup/node_key"
+
+ 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.
+
+ if (instance.isolation == "vm"):
+ # legacy where container was configured by sync_vcpetenant.py
+
+ fields = { "instance_name": instance.name,
+ "hostname": instance.node.name,
+ "instance_id": instance.instance_id,
+ "username": "ubuntu",
+ }
+ key_name = self.service_key_name
+ elif (instance.isolation == "container"):
+ # container on bare metal
+ node = self.get_node(instance)
+ hostname = node.name
+ fields = { "hostname": hostname,
+ "baremetal_ssh": True,
+ "instance_name": "rootcontext",
+ "username": "root",
+ "container_name": "%s-%s" % (instance.slice.name, str(instance.id))
+ }
+ key_name = self.get_node_key(node)
+ else:
+ # container in a VM
+ if not instance.parent:
+ raise Exception("Container-in-VM has no parent")
+ if not instance.parent.instance_id:
+ raise Exception("Container-in-VM parent is not yet instantiated")
+ if not instance.parent.slice.service:
+ raise Exception("Container-in-VM parent has no service")
+ if not instance.parent.slice.service.private_key_fn:
+ raise Exception("Container-in-VM parent service has no private_key_fn")
+ fields = { "hostname": instance.parent.node.name,
+ "instance_name": instance.parent.name,
+ "instance_id": instance.parent.instance_id,
+ "username": "ubuntu",
+ "nat_ip": instance.parent.get_ssh_ip(),
+ "container_name": "%s-%s" % (instance.slice.name, str(instance.id))
+ }
+ key_name = instance.parent.slice.service.private_key_fn
+
+ if not os.path.exists(key_name):
+ raise Exception("Node key %s does not exist" % key_name)
+
+ key = file(key_name).read()
+
+ fields["private_key"] = key
+
+ # now the ceilometer stuff
+
+ cslice = ControllerSlice.objects.get(slice=instance.slice)
+ if not cslice:
+ raise Exception("Controller slice object for %s does not exist" % instance.slice.name)
+
+ cuser = ControllerUser.objects.get(user=instance.creator)
+ if not cuser:
+ raise Exception("Controller user object for %s does not exist" % instance.creator)
+
+ fields.update({"keystone_tenant_id": cslice.tenant_id,
+ "keystone_user_id": cuser.kuser_id,
+ "rabbit_user": instance.controller.rabbit_user,
+ "rabbit_password": instance.controller.rabbit_password,
+ "rabbit_host": instance.controller.rabbit_host})
+
+ return fields
+
+ def sync_record(self, o):
+ logger.info("sync'ing object %s" % str(o))
+
+ self.prepare_record(o)
+
+ instance = self.get_instance(o)
+
+ if isinstance(instance, basestring):
+ # sync to some external host
+
+ # XXX - this probably needs more work...
+
+ fields = { "hostname": instance,
+ "instance_id": "ubuntu", # this is the username to log into
+ "private_key": service.key,
+ }
+ else:
+ # sync to an XOS instance
+ if not instance:
+ self.defer_sync(o, "waiting on instance")
+ return
+
+ if not instance.instance_name:
+ self.defer_sync(o, "waiting on instance.instance_name")
+ return
+
+ fields = self.get_ansible_fields(instance)
+
+ fields["ansible_tag"] = o.__class__.__name__ + "_" + str(o.id)
+
+ # If 'o' defines a 'sync_attributes' list, then we'll copy those
+ # attributes into the Ansible recipe's field list automatically.
+ if hasattr(o, "sync_attributes"):
+ for attribute_name in o.sync_attributes:
+ fields[attribute_name] = getattr(o, attribute_name)
+
+ fields.update(self.get_extra_attributes(o))
+
+ self.sync_fields(o, fields)
+
+ o.save()
+
+ def delete_record(self, o):
+ try:
+ controller = o.get_controller()
+ controller_register = json.loads(o.node.site_deployment.controller.backend_register)
+
+ if (controller_register.get('disabled',False)):
+ raise InnocuousException('Controller %s is disabled'%o.node.site_deployment.controller.name)
+ except AttributeError:
+ pass
+
+ instance = self.get_instance(o)
+ if isinstance(instance, basestring):
+ # sync to some external host
+
+ # XXX - this probably needs more work...
+
+ fields = { "hostname": instance,
+ "instance_id": "ubuntu", # this is the username to log into
+ "private_key": service.key,
+ }
+ else:
+ # sync to an XOS instance
+ fields = self.get_ansible_fields(instance)
+
+ fields["ansible_tag"] = o.__class__.__name__ + "_" + str(o.id)
+
+ # If 'o' defines a 'sync_attributes' list, then we'll copy those
+ # attributes into the Ansible recipe's field list automatically.
+ if hasattr(o, "sync_attributes"):
+ for attribute_name in o.sync_attributes:
+ fields[attribute_name] = getattr(o, attribute_name)
+
+ fields.update(self.map_delete_inputs(o))
+
+ fields['delete']=True
+ res = self.run_playbook(o,fields)
+ try:
+ self.map_delete_outputs(o,res)
+ except AttributeError:
+ pass
diff --git a/xos/observers/base/SyncSliverUsingAnsible.py b/xos/observers/base/SyncSliverUsingAnsible.py
new file mode 100644
index 0000000..a76b300
--- /dev/null
+++ b/xos/observers/base/SyncSliverUsingAnsible.py
@@ -0,0 +1,95 @@
+import hashlib
+import os
+import socket
+import sys
+import base64
+import time
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from core.models import Service, Slice
+from util.logger import Logger, logging
+
+logger = Logger(level=logging.INFO)
+
+class SyncInstanceUsingAnsible(SyncStep):
+ # All of the following should be defined for classes derived from this
+ # base class. Examples below use VCPETenant.
+
+ # provides=[VCPETenant]
+ # observes=VCPETenant
+ # requested_interval=0
+ # template_name = "sync_vcpetenant.yaml"
+ # service_key_name = "/opt/xos/observers/vcpe/vcpe_private_key"
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+
+ def defer_sync(self, o, reason):
+ logger.info("defer object %s due to %s" % (str(o), reason))
+ raise Exception("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 get_instance(self, o):
+ # We need to know what instance is associated with the object. Let's
+ # assume 'o' has a field called 'instance'. If the field is called
+ # something else, or if custom logic is needed, then override this
+ # method.
+
+ return o.instance
+
+ def run_playbook(self, o, fields):
+ tStart = time.time()
+ run_template_ssh(self.template_name, fields)
+ logger.info("playbook execution time %d" % int(time.time()-tStart))
+
+ def pre_sync_hook(self, o, fields):
+ pass
+
+ def post_sync_hook(self, o, fields):
+ pass
+
+ def sync_fields(self, o, fields):
+ self.run_playbook(o, fields)
+
+ def sync_record(self, o):
+ logger.info("sync'ing object %s" % str(o))
+
+ instance = self.get_instance(o)
+ if not instance:
+ self.defer_sync(o, "waiting on instance")
+ return
+
+ if not os.path.exists(self.service_key_name):
+ raise Exception("Service key %s does not exist" % self.service_key_name)
+
+ service_key = file(self.service_key_name).read()
+
+ fields = { "instance_name": instance.name,
+ "hostname": instance.node.name,
+ "instance_id": instance.instance_id,
+ "private_key": service_key,
+ "ansible_tag": "vcpe_tenant_" + str(o.id)
+ }
+
+ # If 'o' defines a 'sync_attributes' list, then we'll copy those
+ # attributes into the Ansible recipe's field list automatically.
+ if hasattr(o, "sync_attributes"):
+ for attribute_name in o.sync_attributes:
+ fields[attribute_name] = getattr(o, attribute_name)
+
+ fields.update(self.get_extra_attributes(o))
+
+ self.sync_fields(o, fields)
+
+ o.save()
+
+ def delete_record(self, m):
+ pass
+
diff --git a/xos/observers/base/__init__.py b/xos/observers/base/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/observers/base/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/observers/helloworld/helloworld-observer.py b/xos/observers/helloworld/helloworld-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/helloworld/helloworld-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+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/helloworld/helloworld_config b/xos/observers/helloworld/helloworld_config
new file mode 100644
index 0000000..e32ee0c
--- /dev/null
+++ b/xos/observers/helloworld/helloworld_config
@@ -0,0 +1,47 @@
+[plc]
+name=plc
+deployment=plc
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=localhost
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+logfile=/var/log/xos.log
+
+[nova]
+admin_user=admin@domain.com
+admin_password=admin
+admin_tenant=admin
+url=http://localhost:5000/v2.0/
+default_image=None
+default_flavor=m1.small
+default_security_group=default
+ca_ssl_cert=/etc/ssl/certs/ca-certificates.crt
+
+[observer]
+pretend=False
+backoff_disabled=False
+images_directory=/opt/xos/images
+dependency_graph=/opt/xos/model-deps
+logfile=/var/log/xos_backend.log
+steps_dir=/opt/xos/observers/helloworld/steps
+applist=helloworld
+
+[gui]
+disable_minidashboard=True
+#branding_name=CORD
+#branding_css=/static/cord.css
+#branding_icon=/static/onos-logo.png
diff --git a/xos/observers/helloworld/model-deps b/xos/observers/helloworld/model-deps
new file mode 100644
index 0000000..63188f0
--- /dev/null
+++ b/xos/observers/helloworld/model-deps
@@ -0,0 +1,19 @@
+{
+ "OriginServer": [
+ "ContentProvider"
+ ],
+ "ContentProvider": [
+ "ServiceProvider"
+ ],
+ "CDNPrefix": [
+ "ContentProvider"
+ ],
+ "AccessMap": [
+ "ContentProvider"
+ ],
+ "SiteMap": [
+ "ContentProvider",
+ "ServiceProvider",
+ "CDNPrefix"
+ ]
+}
diff --git a/xos/observers/helloworld/nohup.out b/xos/observers/helloworld/nohup.out
new file mode 100644
index 0000000..74072c6
--- /dev/null
+++ b/xos/observers/helloworld/nohup.out
@@ -0,0 +1 @@
+python: can't open file 'helloworld-observer.py': [Errno 2] No such file or directory
diff --git a/xos/observers/helloworld/run.sh b/xos/observers/helloworld/run.sh
new file mode 100755
index 0000000..f56ffe3
--- /dev/null
+++ b/xos/observers/helloworld/run.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./hpc-backend.py ]]; then
+# ln -s ../xos-observer.py hpc-backend.py
+#fi
+
+export XOS_DIR=/opt/xos
+python helloworld-observer.py -C $XOS_DIR/observers/helloworld/helloworld_config
diff --git a/xos/observers/helloworld/start.sh b/xos/observers/helloworld/start.sh
new file mode 100755
index 0000000..89240cd
--- /dev/null
+++ b/xos/observers/helloworld/start.sh
@@ -0,0 +1,4 @@
+export XOS_DIR=/opt/xos
+
+echo $XOS_DIR/observers/helloworld/helloworld_config
+python helloworld-observer.py -C $XOS_DIR/observers/helloworld/helloworld_config
diff --git a/xos/observers/helloworld/steps/sync_hello.py b/xos/observers/helloworld/steps/sync_hello.py
new file mode 100644
index 0000000..82600e7
--- /dev/null
+++ b/xos/observers/helloworld/steps/sync_hello.py
@@ -0,0 +1,26 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from services.helloworld.models import Hello,World
+from util.logger import Logger, logging
+
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncHello(SyncStep):
+ provides=[Hello]
+ observes=Hello
+ requested_interval=0
+
+ def sync_record(self, record):
+ instance = record.instance_backref
+ instance.userData="packages:\n - apache2\nruncmd:\n - update-rc.d apache2 enable\n - service apache2 start\nwrite_files:\n- content: Hello %s\n path: /var/www/html/hello.txt"%record.name
+ instance.save()
+
+ def delete_record(self, m):
+ return
diff --git a/xos/observers/helloworld/steps/sync_world.py b/xos/observers/helloworld/steps/sync_world.py
new file mode 100644
index 0000000..0a121df
--- /dev/null
+++ b/xos/observers/helloworld/steps/sync_world.py
@@ -0,0 +1,25 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from services.helloworld.models import Hello,World
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncWorld(SyncStep):
+ provides=[World]
+ observes=World
+ requested_interval=0
+
+ def sync_record(self, record):
+ open('/tmp/hello-synchronizer','w').write(record.name)
+
+ def delete_record(self, m):
+ return
diff --git a/xos/observers/helloworld/stop.sh b/xos/observers/helloworld/stop.sh
new file mode 100755
index 0000000..a0b4a8e
--- /dev/null
+++ b/xos/observers/helloworld/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f hpc-observer.py
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..5604296
--- /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 services.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/observers/hpc/Makefile b/xos/observers/hpc/Makefile
new file mode 100644
index 0000000..4a03bd4
--- /dev/null
+++ b/xos/observers/hpc/Makefile
@@ -0,0 +1,10 @@
+hpcobserver.tar.gz:
+ rm -rf BUILD/hpc_observer
+ mkdir -p BUILD/hpc_observer
+ mkdir -p BUILD/hpc_observer/steps
+ mkdir -p BUILD/hpc_observer/deleters
+ cp hpclib.py hpc_observer_config run.sh start.sh stop.sh BUILD/hpc_observer/
+ cp steps/*.py BUILD/hpc_observer/steps/
+ cp deleters/*.py BUILD/hpc_observer/deleters/
+ mkdir -p TARS
+ cd BUILD; tar -czf ../TARS/hpc_observer.tar.gz hpc_observer
diff --git a/xos/observers/hpc/fsck.py b/xos/observers/hpc/fsck.py
new file mode 100644
index 0000000..a6f641b
--- /dev/null
+++ b/xos/observers/hpc/fsck.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+import argparse
+import imp
+import inspect
+import os
+import sys
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+sys.path.append("/opt/xos")
+from xos.config import Config, DEFAULT_CONFIG_FN, XOS_DIR
+from util.logger import Logger, logging
+from synchronizers.base.syncstep import SyncStep
+
+try:
+ from django import setup as django_setup # django 1.7
+except:
+ django_setup = False
+
+logger = Logger(level=logging.INFO)
+
+class XOSConsistencyCheck:
+ def __init__(self):
+ self.sync_steps = []
+ self.load_sync_step_modules()
+
+ def load_sync_step_modules(self, step_dir=None):
+ if step_dir is None:
+ if hasattr(Config(), "observer_steps_dir"):
+ step_dir = Config().observer_steps_dir
+ else:
+ step_dir = XOS_DIR+"/observer/steps"
+
+ for fn in os.listdir(step_dir):
+ pathname = os.path.join(step_dir,fn)
+ if os.path.isfile(pathname) and fn.endswith(".py") and (fn!="__init__.py"):
+ module = imp.load_source(fn[:-3],pathname)
+ for classname in dir(module):
+ c = getattr(module, classname, None)
+
+ # make sure 'c' is a descendent of SyncStep and has a
+ # provides field (this eliminates the abstract base classes
+ # since they don't have a provides)
+
+ if inspect.isclass(c) and issubclass(c, SyncStep) and hasattr(c,"provides") and (c not in self.sync_steps):
+ self.sync_steps.append(c)
+ logger.info('loaded sync steps: %s' % ",".join([x.__name__ for x in self.sync_steps]))
+
+ def run(self):
+ updated = True
+ while updated:
+ updated = False
+
+ for step in self.sync_steps:
+ if hasattr(step, "consistency_check"):
+ updated = updated or step(driver=None).consistency_check()
+
+ if updated:
+ logger.info('re-running consistency checks because something changed')
+
+def main():
+ if not "-C" in sys.argv:
+ print >> sys.stderr, "You probably wanted to use -C " + XOS_DIR + "/hpc_observer/hpc_observer_config"
+
+ # Generate command line parser
+ parser = argparse.ArgumentParser(usage='%(prog)s [options]')
+ # smbaker: util/config.py parses sys.argv[] directly to get config file name; include the option here to avoid
+ # throwing unrecognized argument exceptions
+ parser.add_argument('-C', '--config', dest='config_file', action='store', default=DEFAULT_CONFIG_FN,
+ help='Name of config file.')
+ args = parser.parse_args()
+
+ if django_setup: # 1.7
+ django_setup()
+
+ cc = XOSConsistencyCheck()
+ cc.run()
+
+if __name__ == '__main__':
+ main()
+
diff --git a/xos/observers/hpc/hpc-observer.py b/xos/observers/hpc/hpc-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/hpc/hpc-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+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/hpc/hpc_observer_config b/xos/observers/hpc/hpc_observer_config
new file mode 100644
index 0000000..326e731
--- /dev/null
+++ b/xos/observers/hpc/hpc_observer_config
@@ -0,0 +1,36 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=hpc
+dependency_graph=/opt/xos/observers/hpc/model-deps
+steps_dir=/opt/xos/observers/hpc/steps
+deleters_dir=/opt/xos/observers/hpc/deleters
+log_file=console
+#/var/log/hpc.log
+driver=None
+#cmi_hostname=openclouddev0.internet2.edu
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/hpc/hpc_watcher.py b/xos/observers/hpc/hpc_watcher.py
new file mode 100644
index 0000000..d2efdcc
--- /dev/null
+++ b/xos/observers/hpc/hpc_watcher.py
@@ -0,0 +1,629 @@
+"""
+ hpc_watcher.py
+
+ Daemon to watch the health of HPC and RR instances.
+
+ This deamon uses HpcHealthCheck objects in the Data Model to conduct
+ periodic tests of HPC and RR nodes. Two types of Health Checks are
+ supported:
+
+ kind="dns": checks the request routers to make sure that a DNS
+ name is resolveable and returns the right kind of records.
+
+ resource_name should be set to the domain name to lookup.
+
+ result_contains is option and can be used to hold "A", "CNAME", or
+ a particular address or hostname that should be contained in the
+ query's answer.
+
+ kind="http": checks the hpc nodes to make sure that a URL can be
+ retrieved from the node.
+
+ resource_name should be set to the HostName:Url to fetch. For
+ example, cdn-stream.htm.fiu.edu:/hft2441/intro.mp4
+
+ In addition to the above, HPC heartbeat probes are conducted, similar to
+ the ones that dnsredir conducts.
+
+ The results of health checks are stored in a tag attached to the Instance
+ the healthcheck was conducted against. If all healthchecks of a particular
+ variety were successful for a instance, then "success" will be stored in
+ the tag. Otherwise, the first healthcheck to fail will be stored in the
+ tag.
+
+ Ubuntu prereqs:
+ apt-get install python-pycurl
+ pip install dnslib
+"""
+
+import os
+import socket
+import sys
+sys.path.append("/opt/xos")
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+import django
+from django.contrib.contenttypes.models import ContentType
+from core.models import *
+from services.hpc.models import *
+from services.requestrouter.models import *
+django.setup()
+import time
+import pycurl
+import traceback
+import json
+from StringIO import StringIO
+
+from dnslib.dns import DNSRecord,DNSHeader,DNSQuestion,QTYPE
+from dnslib.digparser import DigParser
+
+from threading import Thread, Condition
+
+"""
+from dnslib import *
+q = DNSRecord(q=DNSQuestion("cdn-stream.htm.fiu.edu"))
+a_pkt = q.send("150.135.65.10", tcp=False, timeout=10)
+a = DNSRecord.parse(a_pkt)
+
+from dnslib import *
+q = DNSRecord(q=DNSQuestion("onlab.vicci.org"))
+a_pkt = q.send("150.135.65.10", tcp=False, timeout=10)
+a = DNSRecord.parse(a_pkt)
+"""
+
+class WorkQueue:
+ def __init__(self):
+ self.job_cv = Condition()
+ self.jobs = []
+ self.result_cv = Condition()
+ self.results = []
+ self.outstanding = 0
+
+ def get_job(self):
+ self.job_cv.acquire()
+ while not self.jobs:
+ self.job_cv.wait()
+ result = self.jobs.pop()
+ self.job_cv.release()
+ return result
+
+ def submit_job(self, job):
+ self.job_cv.acquire()
+ self.jobs.append(job)
+ self.job_cv.notify()
+ self.job_cv.release()
+ self.outstanding = self.outstanding + 1
+
+ def get_result(self):
+ self.result_cv.acquire()
+ while not self.results:
+ self.result_cv.wait()
+ result = self.results.pop()
+ self.result_cv.release()
+ self.outstanding = self.outstanding - 1
+ return result
+
+ def submit_result(self, result):
+ self.result_cv.acquire()
+ self.results.append(result)
+ self.result_cv.notify()
+ self.result_cv.release()
+
+class DnsResolver(Thread):
+ def __init__(self, queue):
+ Thread.__init__(self)
+ self.queue = queue
+ self.daemon = True
+ self.start()
+
+ def run(self):
+ while True:
+ job = self.queue.get_job()
+ self.handle_job(job)
+ self.queue.submit_result(job)
+
+ def handle_job(self, job):
+ domain = job["domain"]
+ server = job["server"]
+ port = job["port"]
+ result_contains = job.get("result_contains", None)
+
+ try:
+ q = DNSRecord(q=DNSQuestion(domain)) #, getattr(QTYPE,"A")))
+
+ a_pkt = q.send(server, port, tcp=False, timeout=10)
+ a = DNSRecord.parse(a_pkt)
+
+ found_record = False
+ for record in a.rr:
+ if (not result_contains):
+ QTYPE_A = getattr(QTYPE,"A")
+ QTYPE_CNAME = getattr(QTYPE, "CNAME")
+ if ((record.rtype==QTYPE_A) or (record.qtype==QTYPE_CNAME)):
+ found_record = True
+ else:
+ tmp = QTYPE.get(record.rtype) + str(record.rdata)
+ if (result_contains in tmp):
+ found_record = True
+
+ if not found_record:
+ if result_contains:
+ job["status"] = "%s,No %s records" % (domain, result_contains)
+ else:
+ job["status"] = "%s,No A or CNAME records" % domain
+
+ return
+
+ except Exception, e:
+ job["status"] = "%s,Exception: %s" % (domain, str(e))
+ return
+
+ job["status"] = "success"
+
+class HpcHeartbeat(Thread):
+ def __init__(self, queue):
+ Thread.__init__(self)
+ self.queue = queue
+ self.daemon = True
+ self.start()
+
+ def run(self):
+ while True:
+ job = self.queue.get_job()
+ self.handle_job(job)
+ self.queue.submit_result(job)
+
+ def curl_error_message(self, e):
+ if e.args[0] == 6:
+ return "couldn't resolve host"
+ if e.args[0] == 7:
+ return "failed to connect"
+ return "curl error %d" % e.args[0]
+
+ def handle_job(self, job):
+ server = job["server"]
+ port = job["port"]
+
+ try:
+ buffer = StringIO()
+ c = pycurl.Curl()
+
+ c.setopt(c.URL, "http://%s:%s/heartbeat" % (server, port))
+ c.setopt(c.WRITEDATA, buffer)
+ c.setopt(c.HTTPHEADER, ['host: hpc-heartbeat', 'X-heartbeat: 1'])
+ c.setopt(c.TIMEOUT, 10)
+ c.setopt(c.CONNECTTIMEOUT, 10)
+ c.setopt(c.NOSIGNAL, 1)
+
+ try:
+ c.perform()
+ response_code = c.getinfo(c.RESPONSE_CODE)
+ except Exception, e:
+ #traceback.print_exc()
+ job["status"] = self.curl_error_message(e)
+ return
+ finally:
+ c.close()
+
+ if response_code != 200:
+ job["status"] = "error response %d" % response_code
+ return
+
+ except Exception, e:
+ job["status"] = "Exception: %s" % str(e)
+ return
+
+ job["status"] = "success"
+
+class HpcFetchUrl(Thread):
+ def __init__(self, queue):
+ Thread.__init__(self)
+ self.queue = queue
+ self.daemon = True
+ self.start()
+
+ def run(self):
+ while True:
+ job = self.queue.get_job()
+ self.handle_job(job)
+ self.queue.submit_result(job)
+
+ def curl_error_message(self, e):
+ if e.args[0] == 6:
+ return "couldn't resolve host"
+ if e.args[0] == 7:
+ return "failed to connect"
+ return "curl error %d" % e.args[0]
+
+ def handle_job(self, job):
+ server = job["server"]
+ port = job["port"]
+ url = job["url"]
+ domain = job["domain"]
+
+ def progress(download_t, download_d, upload_t, upload_d):
+ # limit download size to a megabyte
+ if (download_d > 1024*1024):
+ return 1
+ else:
+ return 0
+
+ try:
+ buffer = StringIO()
+ c = pycurl.Curl()
+
+ c.setopt(c.URL, "http://%s:%s/%s" % (server, port, url))
+ c.setopt(c.WRITEDATA, buffer)
+ c.setopt(c.HTTPHEADER, ['host: ' + domain])
+ c.setopt(c.TIMEOUT, 10)
+ c.setopt(c.CONNECTTIMEOUT, 10)
+ c.setopt(c.NOSIGNAL, 1)
+ c.setopt(c.NOPROGRESS, 0)
+ c.setopt(c.PROGRESSFUNCTION, progress)
+
+ try:
+ try:
+ c.perform()
+ except Exception, e:
+ # prevent callback abort from raising exception
+ if (e.args[0] != pycurl.E_ABORTED_BY_CALLBACK):
+ raise
+ response_code = c.getinfo(c.RESPONSE_CODE)
+ bytes_downloaded = int(c.getinfo(c.SIZE_DOWNLOAD))
+ total_time = float(c.getinfo(c.TOTAL_TIME))
+ except Exception, e:
+ #traceback.print_exc()
+ job["status"] = self.curl_error_message(e)
+ return
+ finally:
+ c.close()
+
+ if response_code != 200:
+ job["status"] = "error response %s" % str(response_code)
+ return
+
+ except Exception, e:
+ #traceback.print_exc()
+ job["status"] = "Exception: %s" % str(e)
+ return
+
+ job["status"] = "success"
+ job["bytes_downloaded"] = bytes_downloaded
+ job["total_time"] = total_time
+
+class WatcherWorker(Thread):
+ def __init__(self, queue):
+ Thread.__init__(self)
+ self.queue = queue
+ self.daemon = True
+ self.start()
+
+ def run(self):
+ while True:
+ job = self.queue.get_job()
+ self.handle_job(job)
+ self.queue.submit_result(job)
+
+ def curl_error_message(self, e):
+ if e.args[0] == 6:
+ return "couldn't resolve host"
+ if e.args[0] == 7:
+ return "failed to connect"
+ return "curl error %d" % e.args[0]
+
+ def handle_job(self, job):
+ server = job["server"]
+ port = job["port"]
+
+ try:
+ buffer = StringIO()
+ c = pycurl.Curl()
+
+ c.setopt(c.URL, "http://%s:%s/" % (server, port))
+ c.setopt(c.WRITEDATA, buffer)
+ c.setopt(c.TIMEOUT, 10)
+ c.setopt(c.CONNECTTIMEOUT, 10)
+ c.setopt(c.NOSIGNAL, 1)
+
+ try:
+ c.perform()
+ response_code = c.getinfo(c.RESPONSE_CODE)
+ except Exception, e:
+ #traceback.print_exc()
+ job["status"] = json.dumps( {"status": self.curl_error_message(e)} )
+ return
+ finally:
+ c.close()
+
+ if response_code != 200:
+ job["status"] = json.dumps( {"status": "error response %d" % response_code} )
+ return
+
+ d = json.loads(buffer.getvalue())
+ d["status"] = "success";
+ job["status"] = json.dumps(d)
+
+ except Exception, e:
+ job["status"] = json.dumps( {"status": "Exception: %s" % str(e)} )
+ return
+
+class BaseWatcher(Thread):
+ def __init__(self):
+ Thread.__init__(self)
+ self.daemon = True
+
+ def get_public_ip(self, service, instance):
+ network_name = None
+ if "hpc" in instance.slice.name:
+ network_name = getattr(service, "watcher_hpc_network", None)
+ elif "demux" in instance.slice.name:
+ network_name = getattr(service, "watcher_dnsdemux_network", None)
+ elif "redir" in instance.slice.name:
+ network_name = getattr(service, "watcher_dnsredir_network", None)
+
+ if network_name and network_name.lower()=="nat":
+ return None
+
+ if (network_name is None) or (network_name=="") or (network_name.lower()=="public"):
+ return instance.get_public_ip()
+
+ for ns in instance.ports.all():
+ if (ns.ip) and (ns.network.name==network_name):
+ return ns.ip
+
+ raise ValueError("Couldn't find network %s" % str(network_name))
+
+ def set_status(self, instance, service, kind, msg, check_error=True):
+ #print instance.node.name, kind, msg
+ if check_error:
+ instance.has_error = (msg!="success")
+
+ instance_type = ContentType.objects.get_for_model(instance)
+
+ t = Tag.objects.filter(service=service, name=kind+".msg", content_type__pk=instance_type.id, object_id=instance.id)
+ if t:
+ t=t[0]
+ if (t.value != msg):
+ t.value = msg
+ t.save()
+ else:
+ Tag(service=service, name=kind+".msg", content_object = instance, value=msg).save()
+
+ t = Tag.objects.filter(service=service, name=kind+".time", content_type__pk=instance_type.id, object_id=instance.id)
+ if t:
+ t=t[0]
+ t.value = str(time.time())
+ t.save()
+ else:
+ Tag(service=service, name=kind+".time", content_object = instance, value=str(time.time())).save()
+
+ def get_service_slices(self, service, kind=None):
+ try:
+ slices = service.slices.all()
+ except:
+ # buggy data model
+ slices = service.service.all()
+
+ if kind:
+ return [x for x in slices if (kind in x.name)]
+ else:
+ return list(slices)
+
+class RRWatcher(BaseWatcher):
+ def __init__(self):
+ BaseWatcher.__init__(self)
+
+ self.resolver_queue = WorkQueue()
+ for i in range(0,10):
+ DnsResolver(queue = self.resolver_queue)
+
+ def check_request_routers(self, service, instances):
+ for instance in instances:
+ instance.has_error = False
+
+ try:
+ ip = self.get_public_ip(service, instance)
+ except Exception, e:
+ self.set_status(instance, service, "watcher.DNS", "exception: %s" % str(e))
+ continue
+ if not ip:
+ try:
+ ip = socket.gethostbyname(instance.node.name)
+ except:
+ self.set_status(instance, service, "watcher.DNS", "dns resolution failure")
+ continue
+
+ if not ip:
+ self.set_status(instance, service, "watcher.DNS", "no IP address")
+ continue
+
+ checks = HpcHealthCheck.objects.filter(kind="dns")
+ if not checks:
+ self.set_status(instance, service, "watcher.DNS", "no DNS HealthCheck tests configured")
+
+ for check in checks:
+ self.resolver_queue.submit_job({"domain": check.resource_name, "server": ip, "port": 53, "instance": instance, "result_contains": check.result_contains})
+
+ while self.resolver_queue.outstanding > 0:
+ result = self.resolver_queue.get_result()
+ instance = result["instance"]
+ if (result["status"]!="success") and (not instance.has_error):
+ self.set_status(instance, service, "watcher.DNS", result["status"])
+
+ for instance in instances:
+ if not instance.has_error:
+ self.set_status(instance, service, "watcher.DNS", "success")
+
+ def run_once(self):
+ for hpcService in HpcService.objects.all():
+ for slice in self.get_service_slices(hpcService, "dnsdemux"):
+ self.check_request_routers(hpcService, slice.instances.all())
+
+ for rrService in RequestRouterService.objects.all():
+ for slice in self.get_service_slices(rrService, "dnsdemux"):
+ self.check_request_routers(rrService, slice.instances.all())
+
+ def run(self):
+ while True:
+ self.run_once()
+ time.sleep(10)
+
+ django.db.reset_queries()
+
+class HpcProber(BaseWatcher):
+ def __init__(self):
+ BaseWatcher.__init__(self)
+
+ self.heartbeat_queue = WorkQueue()
+ for i in range(0, 10):
+ HpcHeartbeat(queue = self.heartbeat_queue)
+
+ def probe_hpc(self, service, instances):
+ for instance in instances:
+ instance.has_error = False
+
+ self.heartbeat_queue.submit_job({"server": instance.node.name, "port": 8009, "instance": instance})
+
+ while self.heartbeat_queue.outstanding > 0:
+ result = self.heartbeat_queue.get_result()
+ instance = result["instance"]
+ if (result["status"]!="success") and (not instance.has_error):
+ self.set_status(instance, service, "watcher.HPC-hb", result["status"])
+
+ for instance in instances:
+ if not instance.has_error:
+ self.set_status(instance, service, "watcher.HPC-hb", "success")
+
+ def run_once(self):
+ for hpcService in HpcService.objects.all():
+ for slice in self.get_service_slices(hpcService, "hpc"):
+ self.probe_hpc(hpcService, slice.instances.all())
+
+ def run(self):
+ while True:
+ self.run_once()
+ time.sleep(10)
+
+ django.db.reset_queries()
+
+class HpcFetcher(BaseWatcher):
+ def __init__(self):
+ BaseWatcher.__init__(self)
+
+ self.fetch_queue = WorkQueue()
+ for i in range(0, 10):
+ HpcFetchUrl(queue = self.fetch_queue)
+
+ def fetch_hpc(self, service, instances):
+ for instance in instances:
+ instance.has_error = False
+ instance.url_status = []
+
+ checks = HpcHealthCheck.objects.filter(kind="http")
+ if not checks:
+ self.set_status(instance, service, "watcher.HPC-fetch", "no HTTP HealthCheck tests configured")
+
+ for check in checks:
+ if (not check.resource_name) or (":" not in check.resource_name):
+ self.set_status(instance, service, "watcher.HPC-fetch", "malformed resource_name: " + str(check.resource_name))
+ break
+
+ (domain, url) = check.resource_name.split(":",1)
+
+ self.fetch_queue.submit_job({"server": instance.node.name, "port": 80, "instance": instance, "domain": domain, "url": url})
+
+ while self.fetch_queue.outstanding > 0:
+ result = self.fetch_queue.get_result()
+ instance = result["instance"]
+ if (result["status"] == "success"):
+ instance.url_status.append( (result["domain"] + result["url"], "success", result["bytes_downloaded"], result["total_time"]) )
+ if (result["status"]!="success") and (not instance.has_error):
+ self.set_status(instance, service, "watcher.HPC-fetch", result["status"])
+
+ for instance in instances:
+ self.set_status(instance, service, "watcher.HPC-fetch-urls", json.dumps(instance.url_status), check_error=False)
+ if not instance.has_error:
+ self.set_status(instance, service, "watcher.HPC-fetch", "success")
+
+ def run_once(self):
+ for hpcService in HpcService.objects.all():
+ for slice in self.get_service_slices(hpcService, "hpc"):
+ try:
+ self.fetch_hpc(hpcService, slice.instances.all())
+ except:
+ traceback.print_exc()
+
+ def run(self):
+ while True:
+ self.run_once()
+ time.sleep(10)
+
+ django.db.reset_queries()
+
+class WatcherFetcher(BaseWatcher):
+ def __init__(self):
+ BaseWatcher.__init__(self)
+
+ self.fetch_queue = WorkQueue()
+ for i in range(0, 10):
+ WatcherWorker(queue = self.fetch_queue)
+
+ def fetch_watcher(self, service, instances):
+ for instance in instances:
+ try:
+ ip = self.get_public_ip(service, instance)
+ except Exception, e:
+ self.set_status(instance, service, "watcher.watcher", json.dumps({"status": "exception: %s" % str(e)}) )
+ continue
+ if not ip:
+ try:
+ ip = socket.gethostbyname(instance.node.name)
+ except:
+ self.set_status(instance, service, "watcher.watcher", json.dumps({"status": "dns resolution failure"}) )
+ continue
+
+ if not ip:
+ self.set_status(instance, service, "watcher.watcher", json.dumps({"status": "no IP address"}) )
+ continue
+
+ port = 8015
+ if ("redir" in instance.slice.name):
+ port = 8016
+ elif ("demux" in instance.slice.name):
+ port = 8017
+
+ self.fetch_queue.submit_job({"server": ip, "port": port, "instance": instance})
+
+ while self.fetch_queue.outstanding > 0:
+ result = self.fetch_queue.get_result()
+ instance = result["instance"]
+ self.set_status(instance, service, "watcher.watcher", result["status"])
+
+ def run_once(self):
+ for hpcService in HpcService.objects.all():
+ for slice in self.get_service_slices(hpcService):
+ self.fetch_watcher(hpcService, slice.instances.all())
+
+ def run(self):
+ while True:
+ self.run_once()
+ time.sleep(10)
+
+ django.db.reset_queries()
+
+
+if __name__ == "__main__":
+ if "--once" in sys.argv:
+ RRWatcher().run_once()
+ HpcProber().run_once()
+ HpcFetcher().run_once()
+ WatcherFetcher().run_once()
+ else:
+ RRWatcher().start()
+ HpcProber().start()
+ HpcFetcher().start()
+ WatcherFetcher().start()
+
+ print "Running forever..."
+ while True:
+ time.sleep(60)
+
diff --git a/xos/observers/hpc/hpclib.py b/xos/observers/hpc/hpclib.py
new file mode 100644
index 0000000..88abf23
--- /dev/null
+++ b/xos/observers/hpc/hpclib.py
@@ -0,0 +1,126 @@
+import os
+import base64
+import string
+import sys
+import xmlrpclib
+
+if __name__ == '__main__':
+ sys.path.append("/opt/xos")
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+from xos.config import Config
+from core.models import Service
+from services.hpc.models import HpcService
+from services.requestrouter.models import RequestRouterService
+from util.logger import Logger, logging
+
+logger = Logger(level=logging.INFO)
+
+class APIHelper:
+ def __init__(self, proxy, auth, method=None):
+ self.proxy = proxy
+ self.auth = auth
+ self.method = method
+
+ def __getattr__(self, name):
+ if name.startswith("_"):
+ return getattr(self, name)
+ else:
+ return APIHelper(self.proxy, self.auth, name)
+
+ def __call__(self, *args):
+ method = getattr(self.proxy, self.method)
+ return method(self.auth, *args)
+
+class CmiClient:
+ def __init__(self, hostname, port=8003, username="apiuser", password="apiuser"):
+ self.connect_api(hostname, port, username, password)
+
+ def connect_api(self, hostname, port=8003, username="apiuser", password="apiuser"):
+ #print "https://%s:%d/COAPI/" % (hostname, port)
+ cob = xmlrpclib.ServerProxy("https://%s:%d/COAPI/" % (hostname, port), allow_none=True)
+ cob_auth = {}
+ cob_auth["Username"] = username
+ cob_auth["AuthString"] = password
+ cob_auth["AuthMethod"] = "password"
+
+ onev = xmlrpclib.ServerProxy("https://%s:%d/ONEV_API/" % (hostname, port), allow_none=True)
+ onev_auth = {}
+ onev_auth["Username"] = username
+ onev_auth["AuthString"] = password
+ onev_auth["AuthMethod"] = "password"
+
+ self.cob = APIHelper(cob, cob_auth)
+ self.onev = APIHelper(onev, onev_auth)
+
+class HpcLibrary:
+ def __init__(self):
+ self._client = None
+
+ def make_account_name(self, x):
+ x=x.lower()
+ y = ""
+ for c in x:
+ if (c in (string.lowercase + string.digits)):
+ y = y + c
+ return y[:20]
+
+ def get_hpc_service(self):
+ hpc_service_name = getattr(Config(), "observer_hpc_service", None)
+ if hpc_service_name:
+ hpc_service = HpcService.objects.filter(name = hpc_service_name)
+ else:
+ hpc_service = HpcService.objects.all()
+
+ if not hpc_service:
+ if hpc_service_name:
+ raise Exception("No HPC Service with name %s" % hpc_service_name)
+ else:
+ raise Exception("No HPC Services")
+ hpc_service = hpc_service[0]
+
+ return hpc_service
+
+ def get_cmi_hostname(self, hpc_service=None):
+ if getattr(Config(),"observer_cmi_hostname",None):
+ return getattr(Config(),"observer_cmi_hostname")
+
+ if (hpc_service is None):
+ hpc_service = self.get_hpc_service()
+
+ if hpc_service.cmi_hostname:
+ return hpc_service.cmi_hostname
+
+ try:
+ slices = hpc_service.slices.all()
+ except:
+ # deal with buggy data model
+ slices = hpc_service.service.all()
+
+ for slice in slices:
+ if slice.name.endswith("cmi"):
+ for instance in slice.instances.all():
+ if instance.node:
+ return instance.node.name
+
+ raise Exception("Failed to find a CMI instance")
+
+ @property
+ def client(self):
+ if self._client is None:
+ self._client = CmiClient(self.get_cmi_hostname())
+ return self._client
+
+if __name__ == '__main__':
+ import django
+ django.setup()
+
+ lib = HpcLibrary()
+
+ print "testing API connection to", lib.get_cmi_hostname()
+ lib.client.cob.GetNewObjects()
+ lib.client.onev.ListAll("CDN")
+
+
+
+
diff --git a/xos/observers/hpc/model-deps b/xos/observers/hpc/model-deps
new file mode 100644
index 0000000..63188f0
--- /dev/null
+++ b/xos/observers/hpc/model-deps
@@ -0,0 +1,19 @@
+{
+ "OriginServer": [
+ "ContentProvider"
+ ],
+ "ContentProvider": [
+ "ServiceProvider"
+ ],
+ "CDNPrefix": [
+ "ContentProvider"
+ ],
+ "AccessMap": [
+ "ContentProvider"
+ ],
+ "SiteMap": [
+ "ContentProvider",
+ "ServiceProvider",
+ "CDNPrefix"
+ ]
+}
diff --git a/xos/observers/hpc/run.sh b/xos/observers/hpc/run.sh
new file mode 100755
index 0000000..f77d751
--- /dev/null
+++ b/xos/observers/hpc/run.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./hpc-backend.py ]]; then
+# ln -s ../xos-observer.py hpc-backend.py
+#fi
+
+export XOS_DIR=/opt/xos
+python hpc-observer.py -C $XOS_DIR/observers/hpc/hpc_observer_config
diff --git a/xos/observers/hpc/start.sh b/xos/observers/hpc/start.sh
new file mode 100755
index 0000000..305c07f
--- /dev/null
+++ b/xos/observers/hpc/start.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./hpc-backend.py ]]; then
+# ln -s ../xos-observer.py hpc-backend.py
+#fi
+
+export XOS_DIR=/opt/xos
+nohup python hpc-observer.py -C $XOS_DIR/observers/hpc/hpc_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/hpc/steps/garbage_collector.py b/xos/observers/hpc/steps/garbage_collector.py
new file mode 100644
index 0000000..25172a3
--- /dev/null
+++ b/xos/observers/hpc/steps/garbage_collector.py
@@ -0,0 +1,67 @@
+import os
+import sys
+import base64
+import traceback
+from collections import defaultdict
+from django.db.models import F, Q
+from xos.config import Config
+from util.logger import Logger, logging
+from synchronizers.base.syncstep import SyncStep
+from services.hpc.models import ServiceProvider, ContentProvider, CDNPrefix, OriginServer
+from core.models import *
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class GarbageCollector(SyncStep, HpcLibrary):
+# requested_interval = 86400
+ requested_interval = 0
+ provides=[]
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+ HpcLibrary.__init__(self)
+
+ def call(self, **args):
+ logger.info("running garbage collector")
+ try:
+ self.gc_originservers()
+ self.gc_cdnprefixes()
+ self.gc_contentproviders()
+ self.gc_serviceproviders()
+ except:
+ traceback.print_exc()
+
+ def gc_onev(self, ps_class, ps_idField, onev_className, onev_idField):
+ # get the CMI's objects
+ onev_objs = self.client.onev.ListAll(onev_className)
+
+ # get the data model's objects,
+ ps_objs = ps_class.objects.filter(enacted__isnull=False)
+ ps_ids = [str(getattr(x,ps_idField,None)) for x in ps_objs]
+
+ # for each onev object, if it's id does not exist in a data model
+ # object, then delete it.
+ for onev_obj in onev_objs:
+ onev_id = onev_obj[onev_idField]
+ if str(onev_id) not in ps_ids:
+ logger.info("garbage collecting %s %s" % (onev_className, str(onev_id)))
+ self.client.onev.Delete(onev_className, onev_id)
+
+ def gc_originservers(self):
+ self.gc_onev(OriginServer, "origin_server_id", "OriginServer", "origin_server_id")
+
+ def gc_cdnprefixes(self):
+ self.gc_onev(CDNPrefix, "cdn_prefix_id", "CDNPrefix", "cdn_prefix_id")
+
+ def gc_contentproviders(self):
+ self.gc_onev(ContentProvider, "content_provider_id", "ContentProvider", "content_provider_id")
+
+ def gc_serviceproviders(self):
+ self.gc_onev(ServiceProvider, "service_provider_id", "ServiceProvider", "service_provider_id")
+
diff --git a/xos/observers/hpc/steps/sync_cdnprefix.py b/xos/observers/hpc/steps/sync_cdnprefix.py
new file mode 100644
index 0000000..d2b0276
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_cdnprefix.py
@@ -0,0 +1,101 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import ServiceProvider, ContentProvider, CDNPrefix
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncCDNPrefix(SyncStep, HpcLibrary):
+ provides=[CDNPrefix]
+ observes=CDNPrefix
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+ HpcLibrary.__init__(self)
+
+ def filter_hpc_service(self, objs):
+ hpcService = self.get_hpc_service()
+
+ return [x for x in objs if x.contentProvider.serviceProvider.hpcService == hpcService]
+
+ def fetch_pending(self, deleted):
+ #self.consistency_check()
+
+ return self.filter_hpc_service(SyncStep.fetch_pending(self, deleted))
+
+ def consistency_check(self):
+ # set to true if something changed
+ result=False
+
+ # sanity check to make sure our PS objects have CMI objects behind them
+ all_p_ids = [x["cdn_prefix_id"] for x in self.client.onev.ListAll("CDNPrefix")]
+
+ all_p_ids = []
+ all_origins = {}
+ for x in self.client.onev.ListAll("CDNPrefix"):
+ id = x["cdn_prefix_id"]
+ all_p_ids.append(id)
+ all_origins[id] = x.get("default_origin_server", None)
+
+ for p in CDNPrefix.objects.all():
+ if (p.cdn_prefix_id is None):
+ continue
+
+ if (p.cdn_prefix_id not in all_p_ids):
+ logger.info("CDN Prefix %s was not found on CMI" % p.cdn_prefix_id)
+ p.cdn_prefix_id=None
+ p.save()
+ result = True
+
+ if (p.defaultOriginServer!=None) and (all_origins.get(p.cdn_prefix_id,None) != p.defaultOriginServer.url):
+ logger.info("CDN Prefix %s does not have default origin server on CMI" % str(p))
+ p.save() # this will set updated>enacted and force observer to re-sync
+ result = True
+
+ return result
+
+ def sync_record(self, cp):
+ logger.info("sync'ing cdn prefix %s" % str(cp))
+
+ if (not cp.contentProvider) or (not cp.contentProvider.content_provider_id):
+ raise Exception("CDN Prefix %s is linked to a contentProvider without an id" % str(cp))
+
+ cpid = cp.contentProvider.content_provider_id
+
+ cp_dict = {"service": "HyperCache", "enabled": cp.enabled, "content_provider_id": cpid, "cdn_prefix": cp.prefix}
+
+ if cp.defaultOriginServer and cp.defaultOriginServer.url:
+ if (not cp.defaultOriginServer.origin_server_id):
+ # It's probably a bad idea to try to set defaultOriginServer before
+ # we've crated defaultOriginServer.
+ raise Exception("cdn prefix %s is waiting for it's default origin server to get an id" % str(cp))
+
+ cp_dict["default_origin_server"] = cp.defaultOriginServer.url
+
+ #print cp_dict
+
+ if not cp.cdn_prefix_id:
+ id = self.client.onev.Create("CDNPrefix", cp_dict)
+ cp.cdn_prefix_id = id
+ else:
+ del cp_dict["content_provider_id"] # this can't be updated
+ del cp_dict["cdn_prefix"] # this can't be updated either
+ self.client.onev.Update("CDNPrefix", cp.cdn_prefix_id, cp_dict)
+
+ cp.save()
+
+ def delete_record(self, m):
+ if m.cdn_prefix_id is not None:
+ self.client.onev.Delete("CDNPrefix", m.cdn_prefix_id)
diff --git a/xos/observers/hpc/steps/sync_contentprovider.py b/xos/observers/hpc/steps/sync_contentprovider.py
new file mode 100644
index 0000000..4e95c36
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_contentprovider.py
@@ -0,0 +1,78 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import ServiceProvider, ContentProvider
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncContentProvider(SyncStep, HpcLibrary):
+ provides=[ContentProvider]
+ observes=ContentProvider
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+ HpcLibrary.__init__(self)
+
+ def filter_hpc_service(self, objs):
+ hpcService = self.get_hpc_service()
+
+ return [x for x in objs if x.serviceProvider.hpcService == hpcService]
+
+ def fetch_pending(self, deleted):
+ #self.consistency_check()
+
+ return self.filter_hpc_service(SyncStep.fetch_pending(self, deleted))
+
+ def consistency_check(self):
+ # set to true if something changed
+ result=False
+
+ # sanity check to make sure our PS objects have CMI objects behind them
+ all_cp_ids = [x["content_provider_id"] for x in self.client.onev.ListAll("ContentProvider")]
+ for cp in ContentProvider.objects.all():
+ if (cp.content_provider_id is not None) and (cp.content_provider_id not in all_cp_ids):
+ logger.info("Content provider %s was not found on CMI" % cp.content_provider_id)
+ cp.content_provider_id=None
+ cp.save()
+ result = True
+
+ return result
+
+ def sync_record(self, cp):
+ logger.info("sync'ing content provider %s" % str(cp))
+ account_name = self.make_account_name(cp.name)
+
+ if (not cp.serviceProvider) or (not cp.serviceProvider.service_provider_id):
+ raise Exception("ContentProvider %s is linked to a serviceProvider with no id" % str(cp))
+
+ spid = cp.serviceProvider.service_provider_id
+
+ cp_dict = {"account": account_name, "name": cp.name, "enabled": cp.enabled}
+
+ #print cp_dict
+
+ if not cp.content_provider_id:
+ cp_dict["service_provider_id"] = spid
+ id = self.client.onev.Create("ContentProvider", cp_dict)
+ cp.content_provider_id = id
+ else:
+ self.client.onev.Update("ContentProvider", cp.content_provider_id, cp_dict)
+
+ cp.save()
+
+ def delete_record(self, m):
+ if m.content_provider_id is not None:
+ self.client.onev.Delete("ContentProvider", m.content_provider_id)
+
diff --git a/xos/observers/hpc/steps/sync_hpcservices.py b/xos/observers/hpc/steps/sync_hpcservices.py
new file mode 100644
index 0000000..47165cc
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_hpcservices.py
@@ -0,0 +1,43 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import HpcService
+from services.requestrouter.models import RequestRouterService
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncHpcService(SyncStep, HpcLibrary):
+ provides=[HpcService]
+ observes=HpcService
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+ HpcLibrary.__init__(self)
+
+ def filter_hpc_service(self, objs):
+ hpcService = self.get_hpc_service()
+
+ return [x for x in objs if x == hpcService]
+
+ def fetch_pending(self, deleted):
+ # Looks like deletion is not supported for this object - Sapan
+ if (deleted):
+ return []
+ else:
+ return self.filter_hpc_service(HpcService.objects.filter(Q(enacted__lt=F('updated')) | Q(enacted=None)))
+
+ def sync_record(self, hpc_service):
+ logger.info("sync'ing hpc_service %s" % str(hpc_service))
+ hpc_service.save()
diff --git a/xos/observers/hpc/steps/sync_originserver.py b/xos/observers/hpc/steps/sync_originserver.py
new file mode 100644
index 0000000..435bbc1
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_originserver.py
@@ -0,0 +1,92 @@
+import os
+import sys
+import base64
+
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import ServiceProvider, ContentProvider, CDNPrefix, OriginServer
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncOriginServer(SyncStep, HpcLibrary):
+ provides=[OriginServer]
+ observes=OriginServer
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+ HpcLibrary.__init__(self)
+
+ def filter_hpc_service(self, objs):
+ hpcService = self.get_hpc_service()
+
+ return [x for x in objs if x.contentProvider.serviceProvider.hpcService == hpcService]
+
+ def fetch_pending(self, deleted):
+ #self.consistency_check()
+
+ return self.filter_hpc_service(SyncStep.fetch_pending(self, deleted))
+
+ def consistency_check(self):
+ # set to true if something changed
+ result=False
+
+ # sanity check to make sure our PS objects have CMI objects behind them
+ all_ors_ids = [x["origin_server_id"] for x in self.client.onev.ListAll("OriginServer")]
+ for ors in OriginServer.objects.all():
+ if (ors.origin_server_id is not None) and (ors.origin_server_id not in all_ors_ids):
+ # we have an origin server ID, but it doesn't exist in the CMI
+ # something went wrong
+ # start over
+ logger.info("origin server %s was not found on CMI" % ors.origin_server_id)
+ ors.origin_server_id=None
+ ors.save()
+ result = True
+
+ return result
+
+ def sync_record(self, ors):
+ logger.info("sync'ing origin server %s" % str(ors))
+
+ if (not ors.contentProvider) or (not ors.contentProvider.content_provider_id):
+ raise Exception("Origin Server %s is linked to a contentProvider with no id" % str(ors))
+
+ cpid = ors.contentProvider.content_provider_id
+
+ # validation requires URL start with http://
+ url = ors.url
+ if not url.startswith("http://"):
+ url = "http://" + url
+
+ ors_dict = {"authenticated_content": ors.authenticated, "zone_redirects": ors.redirects, "content_provider_id": cpid, "url": url, "service_type": "HyperCache", "caching_type": "Optimistic", "description": ors.description}
+ if not ors_dict["description"]:
+ ors_dict["description"] = "blank"
+
+ #print os_dict
+
+ if not ors.origin_server_id:
+ id = self.client.onev.Create("OriginServer", ors_dict)
+ ors.origin_server_id = id
+ else:
+ self.client.onev.Update("OriginServer", ors.origin_server_id, ors_dict)
+
+ # ... something breaks (analytics) if the URL starts with http://, so we
+ # change it in cob after we added it via onev.
+ url = url[7:]
+ self.client.cob.UpdateContent(ors.origin_server_id, {"url": url})
+
+ ors.silent = True
+ ors.save()
+
+ def delete_record(self, m):
+ if m.origin_server_id is not None:
+ self.client.onev.Delete("OriginServer", m.origin_server_id)
diff --git a/xos/observers/hpc/steps/sync_serviceprovider.py b/xos/observers/hpc/steps/sync_serviceprovider.py
new file mode 100644
index 0000000..e4e31f0
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_serviceprovider.py
@@ -0,0 +1,67 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import ServiceProvider
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncServiceProvider(SyncStep, HpcLibrary):
+ provides=[ServiceProvider]
+ observes=ServiceProvider
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+ HpcLibrary.__init__(self)
+
+ def filter_hpc_service(self, objs):
+ hpcService = self.get_hpc_service()
+
+ return [x for x in objs if x.hpcService == hpcService]
+
+ def fetch_pending(self, deleted):
+ #self.consistency_check()
+
+ return self.filter_hpc_service(SyncStep.fetch_pending(self, deleted))
+
+ def consistency_check(self):
+ # set to true if something changed
+ result=False
+
+ # sanity check to make sure our PS objects have CMI objects behind them
+ all_sp_ids = [x["service_provider_id"] for x in self.client.onev.ListAll("ServiceProvider")]
+ for sp in ServiceProvider.objects.all():
+ if (sp.service_provider_id is not None) and (sp.service_provider_id not in all_sp_ids):
+ logger.info("Service provider %s was not found on CMI" % sp.service_provider_id)
+ sp.service_provider_id=None
+ sp.save()
+ result = True
+
+ return result
+
+ def sync_record(self, sp):
+ logger.info("sync'ing service provider %s" % str(sp))
+ account_name = self.make_account_name(sp.name)
+ sp_dict = {"account": account_name, "name": sp.name, "enabled": sp.enabled}
+ if not sp.service_provider_id:
+ id = self.client.onev.Create("ServiceProvider", sp_dict)
+ sp.service_provider_id = id
+ else:
+ self.client.onev.Update("ServiceProvider", sp.service_provider_id, sp_dict)
+
+ sp.save()
+
+ def delete_record(self, m):
+ if m.service_provider_id is not None:
+ self.client.onev.Delete("ServiceProvider", m.service_provider_id)
diff --git a/xos/observers/hpc/steps/sync_sitemap.py b/xos/observers/hpc/steps/sync_sitemap.py
new file mode 100644
index 0000000..e662458
--- /dev/null
+++ b/xos/observers/hpc/steps/sync_sitemap.py
@@ -0,0 +1,118 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.hpc.models import ServiceProvider, ContentProvider, CDNPrefix, SiteMap
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from hpclib import HpcLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncSiteMap(SyncStep, HpcLibrary):
+ provides=[SiteMap]
+ observes=SiteMap
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+ HpcLibrary.__init__(self)
+
+ def filter_hpc_service(self, objs):
+ hpcService = self.get_hpc_service()
+
+ filtered_objs = []
+ for x in objs:
+ if ((x.hpcService == hpcService) or
+ ((x.serviceProvider != None) and (x.serviceProvider.hpcService == hpcService)) or
+ ((x.contentProvider != None) and (x.contentProvider.serviceProvider.hpcService == hpcService)) or
+ ((x.cdnPrefix!= None) and (x.cdnPrefix.contentProvider.serviceProvider.hpcService == hpcService))):
+ filtered_objs.append(x)
+
+ return filtered_objs
+
+ def fetch_pending(self, deleted):
+ return self.filter_hpc_service(SyncStep.fetch_pending(self, deleted))
+
+ def consistency_check(self):
+ # set to true if something changed
+ result=False
+
+ # sanity check to make sure our PS objects have CMI objects behind them
+ all_map_ids = [x["map_id"] for x in self.client.onev.ListAll("Map")]
+ for map in SiteMap.objects.all():
+ if (map.map_id is not None) and (map.map_id not in all_map_ids):
+ logger.info("Map %s was not found on CMI" % map.map_id)
+ map.map_id=None
+ map.save()
+ result = True
+
+ return result
+
+ def update_bind(self, map, map_dict, field_name, to_name, ids):
+ for id in ids:
+ if (not id in map_dict.get(field_name, [])):
+ print "Bind Map", map.map_id, "to", to_name, id
+ self.client.onev.Bind("Map", map.map_id, to_name, id)
+
+ for id in map_dict.get(field_name, []):
+ if (not id in ids):
+ print "Unbind Map", map.map_id, "from", to_name, id
+ self.client.onev.UnBind("map", map.map_id, to_name, id)
+
+ def sync_record(self, map):
+ logger.info("sync'ing SiteMap %s" % str(map))
+
+ if not map.map:
+ # no contents
+ return
+
+ content = map.map.read()
+
+ map_dict = {"name": map.name, "type": "site", "content": content}
+
+ cdn_prefix_ids=[]
+ service_provider_ids=[]
+ content_provider_ids=[]
+
+ if (map.contentProvider):
+ if not map.contentProvider.content_provider_id:
+ raise Exception("Map %s links to a contentProvider with no id" % map.name)
+ conent_provider_ids = [map.contentProvider.content_provider_id]
+
+ if (map.serviceProvider):
+ if not map.serviceProvider.service_provider_id:
+ raise Exception("Map %s links to a serviceProvider with no id" % map.name)
+ service_provider_ids = [map.serviceProvider.service_provider_id]
+
+ if (map.cdnPrefix):
+ if not map.cdnPrefix.cdn_prefix_id:
+ raise Exception("Map %s links to a cdnPrefix with no id" % map.name)
+ cdn_prefix_ids = [map.cdnPrefix.cdn_prefix_id]
+
+ if not map.map_id:
+ print "Create Map", map_dict
+ id = self.client.onev.Create("Map", map_dict)
+ map.map_id = id
+ else:
+ print "Update Map", map_dict
+ # these things we probably cannot update
+ del map_dict["name"]
+ self.client.onev.Update("Map", map.map_id, map_dict)
+
+ cmi_map_dict = self.client.onev.Read("Map", map.map_id)
+
+ self.update_bind(map, cmi_map_dict, "cdn_prefix_ids", "CDNPrefix", cdn_prefix_ids)
+
+ map.save()
+
+ def delete_record(self, m):
+ if m.map_id is not None:
+ self.client.onev.Delete("Map", m.map_id)
diff --git a/xos/observers/hpc/stop.sh b/xos/observers/hpc/stop.sh
new file mode 100755
index 0000000..a0b4a8e
--- /dev/null
+++ b/xos/observers/hpc/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f hpc-observer.py
diff --git a/xos/observers/hpc/supervisor/hpc-observer.conf b/xos/observers/hpc/supervisor/hpc-observer.conf
new file mode 100644
index 0000000..f2c79d4
--- /dev/null
+++ b/xos/observers/hpc/supervisor/hpc-observer.conf
@@ -0,0 +1,2 @@
+[program:hpc-observer]
+command=python /opt/xos/observers/hpc/hpc-observer.py -C /opt/xos/observers/hpc/hpc_observer_config
diff --git a/xos/observers/hpc/supervisor/hpc-watcher.conf b/xos/observers/hpc/supervisor/hpc-watcher.conf
new file mode 100644
index 0000000..e0f4eb1
--- /dev/null
+++ b/xos/observers/hpc/supervisor/hpc-watcher.conf
@@ -0,0 +1,2 @@
+[program:hpc-watcher]
+command=python /opt/xos/observers/hpc/hpc_watcher.py
diff --git a/xos/observers/monitoring_channel/files/docker.list b/xos/observers/monitoring_channel/files/docker.list
new file mode 100644
index 0000000..0ee9ae0
--- /dev/null
+++ b/xos/observers/monitoring_channel/files/docker.list
@@ -0,0 +1 @@
+deb https://get.docker.com/ubuntu docker main
diff --git a/xos/observers/monitoring_channel/files/vm-resolv.conf b/xos/observers/monitoring_channel/files/vm-resolv.conf
new file mode 100644
index 0000000..cae093a
--- /dev/null
+++ b/xos/observers/monitoring_channel/files/vm-resolv.conf
@@ -0,0 +1 @@
+nameserver 8.8.8.8
diff --git a/xos/observers/monitoring_channel/model-deps b/xos/observers/monitoring_channel/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/observers/monitoring_channel/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/observers/monitoring_channel/monitoring_channel_observer.py b/xos/observers/monitoring_channel/monitoring_channel_observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/monitoring_channel/monitoring_channel_observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+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/monitoring_channel/monitoring_channel_observer_config b/xos/observers/monitoring_channel/monitoring_channel_observer_config
new file mode 100644
index 0000000..5657e1d
--- /dev/null
+++ b/xos/observers/monitoring_channel/monitoring_channel_observer_config
@@ -0,0 +1,41 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=monitoring_channel
+dependency_graph=/opt/xos/observers/monitoring_channel/model-deps
+steps_dir=/opt/xos/observers/monitoring_channel/steps
+sys_dir=/opt/xos/observers/monitoring_channel/sys
+deleters_dir=/opt/xos/observers/monitoring_channel/deleters
+log_file=console
+driver=None
+pretend=False
+backoff_disabled=True
+save_ansible_output=True
+# set proxy_ssh to false on cloudlab
+proxy_ssh=False
+full_setup=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/monitoring_channel/steps/sync_monitoringchannel.py b/xos/observers/monitoring_channel/steps/sync_monitoringchannel.py
new file mode 100644
index 0000000..c265517
--- /dev/null
+++ b/xos/observers/monitoring_channel/steps/sync_monitoringchannel.py
@@ -0,0 +1,76 @@
+import hashlib
+import os
+import socket
+import sys
+import base64
+import time
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from observers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+from core.models import Service, Slice
+from services.ceilometer.models import MonitoringChannel
+from util.logger import Logger, logging
+
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncMonitoringChannel(SyncInstanceUsingAnsible):
+ provides=[MonitoringChannel]
+ observes=MonitoringChannel
+ requested_interval=0
+ template_name = "sync_monitoringchannel.yaml"
+ service_key_name = "/opt/xos/observers/monitoring_channel/monitoring_channel_private_key"
+
+ def __init__(self, *args, **kwargs):
+ super(SyncMonitoringChannel, self).__init__(*args, **kwargs)
+
+ def fetch_pending(self, deleted):
+ if (not deleted):
+ objs = MonitoringChannel.get_tenant_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
+ else:
+ objs = MonitoringChannel.get_deleted_tenant_objects()
+
+ return objs
+
+ def get_extra_attributes(self, o):
+ # This is a place to include extra attributes. In the case of Monitoring Channel, we need to know
+ # 1) Allowed tenant ids
+ # 2) Ceilometer API service endpoint URL if running externally
+ # 3) Credentials to access Ceilometer API service
+
+ instance = self.get_instance(o)
+
+ try:
+ full_setup = Config().observer_full_setup
+ except:
+ full_setup = True
+
+ fields = {"unique_id": o.id,
+ "allowed_tenant_ids": o.tenant_list,
+ "auth_url":instance.controller.auth_url,
+ "admin_user":instance.controller.admin_user,
+ "admin_password":instance.controller.admin_password,
+ "admin_tenant":instance.controller.admin_tenant,
+ "full_setup": full_setup}
+
+ return fields
+
+ def run_playbook(self, o, fields):
+ #ansible_hash = hashlib.md5(repr(sorted(fields.items()))).hexdigest()
+ #quick_update = (o.last_ansible_hash == ansible_hash)
+
+ #if quick_update:
+ # logger.info("quick_update triggered; skipping ansible recipe")
+ #else:
+ super(SyncMonitoringChannel, self).run_playbook(o, fields)
+
+ #o.last_ansible_hash = ansible_hash
+
+ def map_delete_inputs(self, o):
+ fields = {"unique_id": o.id,
+ "delete": True}
+ return fields
diff --git a/xos/observers/monitoring_channel/steps/sync_monitoringchannel.yaml b/xos/observers/monitoring_channel/steps/sync_monitoringchannel.yaml
new file mode 100644
index 0000000..6c5fc8c
--- /dev/null
+++ b/xos/observers/monitoring_channel/steps/sync_monitoringchannel.yaml
@@ -0,0 +1,104 @@
+---
+- hosts: {{ instance_name }}
+ gather_facts: False
+ connection: ssh
+ user: ubuntu
+ sudo: yes
+ vars:
+ unique_id: {{ unique_id }}
+ auth_url: {{ auth_url }}
+ admin_user: {{ admin_user }}
+ admin_password: {{ admin_password }}
+ admin_tenant: {{ admin_tenant }}
+ shared_lan_ip: {{ private_ip }}
+ shared_lan_mac: {{ private_mac }}
+ headnode_flat_lan_ip: {{ rabbit_host }}
+ ceilometer_client_acess_ip: {{ ceilometer_ip }}
+ ceilometer_client_acess_mac: {{ ceilometer_mac }}
+ ceilometer_host_port: {{ ceilometer_port }}
+ allowed_tenant_ids:
+ {% for allowed_tenant_id in allowed_tenant_ids %}
+ - {{ allowed_tenant_id }}
+ {% endfor %}
+
+ tasks:
+{% if delete %}
+ - name: Remove tenant
+# FIXME: Adding dummy template action to avoid "action attribute missing in task" error
+ template: src=/opt/xos/observers/monitoring_channel/templates/ceilometer_proxy_config.j2 dest=/usr/local/share/monitoring-channel-{{ unique_id }}_ceilometer_proxy_config mode=0777
+ notify:
+ - stop monitoring-channel
+ - remove container
+{% else %}
+{% if full_setup %}
+ - name: Docker repository
+ copy: src=/opt/xos/observers/monitoring_channel/files/docker.list
+ dest=/etc/apt/sources.list.d/docker.list
+
+ - name: Import the repository key
+ apt_key: keyserver=keyserver.ubuntu.com id=36A1D7869245C8950F966E92D8576A8BA88D21E9
+
+ - name: install Docker
+ apt: name=lxc-docker state=present update_cache=yes
+
+ - name: install python-setuptools
+ apt: name=python-setuptools state=present
+
+ - name: install pip
+ easy_install: name=pip
+
+ - name: install docker-py
+ pip: name=docker-py version=0.5.3
+
+ - name: install Pipework
+ get_url: url=https://raw.githubusercontent.com/jpetazzo/pipework/master/pipework
+ dest=/usr/local/bin/pipework
+ mode=0755
+
+ - name: Disable resolvconf service
+ shell: service resolvconf stop
+ shell: echo manual > /etc/init/resolvconf.override
+ shell: rm -f /etc/resolv.conf
+
+ - name: Install resolv.conf
+ copy: src=/opt/xos/observers/monitoring_channel/files/vm-resolv.conf
+ dest=/etc/resolv.conf
+{% endif %}
+
+ - name: ceilometer proxy config
+ template: src=/opt/xos/observers/monitoring_channel/templates/ceilometer_proxy_config.j2 dest=/usr/local/share/monitoring-channel-{{ unique_id }}_ceilometer_proxy_config mode=0777
+ notify:
+# - restart monitoring-channel
+ - stop monitoring-channel
+ - remove container
+ - start monitoring-channel
+
+ - name: Monitoring channel upstart
+ template: src=/opt/xos/observers/monitoring_channel/templates/monitoring-channel.conf.j2 dest=/etc/init/monitoring-channel-{{ unique_id }}.conf
+
+ - name: Monitoring channel startup script
+ template: src=/opt/xos/observers/monitoring_channel/templates/start-monitoring-channel.sh.j2 dest=/usr/local/sbin/start-monitoring-channel-{{ unique_id }}.sh mode=0755
+ notify:
+# - restart monitoring-channel
+ - stop monitoring-channel
+ - remove container
+ - start monitoring-channel
+
+# These are samples, not necessary for correct function of demo
+
+ - name: Make sure Monitoring channel service is running
+ service: name=monitoring-channel-{{ unique_id }} state=started
+{% endif %}
+
+ handlers:
+ - name: restart monitoring-channel
+ shell: service monitoring-channel-{{ unique_id }} stop; sleep 1; service monitoring-channel-{{ unique_id }} start
+
+ - name: stop monitoring-channel
+ service: name=monitoring-channel-{{ unique_id }} state=stopped
+
+ - name: remove container
+ docker: name=monitoring-channel-{{ unique_id }} state=absent image=monitoring-channel
+
+ - name: start monitoring-channel
+ service: name=monitoring-channel-{{ unique_id }} state=started
diff --git a/xos/observers/monitoring_channel/supervisor/monitoring_channel_observer.conf b/xos/observers/monitoring_channel/supervisor/monitoring_channel_observer.conf
new file mode 100644
index 0000000..1b78703
--- /dev/null
+++ b/xos/observers/monitoring_channel/supervisor/monitoring_channel_observer.conf
@@ -0,0 +1,2 @@
+[program:monitoring_channel_observer]
+command=python /opt/xos/observers/monitoring_channel/monitoring_channel_observer.py -C /opt/xos/observers/monitoring_channel/monitoring_channel_observer_config
diff --git a/xos/observers/monitoring_channel/templates/Dockerfile.monitoring_channel b/xos/observers/monitoring_channel/templates/Dockerfile.monitoring_channel
new file mode 100644
index 0000000..45defb8
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/Dockerfile.monitoring_channel
@@ -0,0 +1,26 @@
+FROM ubuntu:14.04.2
+MAINTAINER Andy Bavier <acb@cs.princeton.edu>
+
+# XXX Workaround for docker bug:
+# https://github.com/docker/docker/issues/6345
+# Kernel 3.15 breaks docker, uss the line below as a workaround
+# until there is a fix
+RUN ln -s -f /bin/true /usr/bin/chfn
+# XXX End workaround
+
+# Install.
+RUN apt-get update && apt-get install -y \
+ python-pip \
+ python-dev
+
+RUN pip install web.py
+RUN pip install wsgilog
+RUN pip install python-ceilometerclient
+RUN mkdir -p /usr/local/share
+ADD ceilometer_proxy_server.py /usr/local/share/
+RUN chmod +x /usr/local/share/ceilometer_proxy_server.py
+ADD start_ceilometer_proxy /usr/local/sbin/
+RUN chmod +x /usr/local/sbin/start_ceilometer_proxy
+EXPOSE 8000
+WORKDIR /usr/local/share
+CMD /usr/local/sbin/start_ceilometer_proxy
diff --git a/xos/observers/monitoring_channel/templates/ceilometer_proxy_config.j2 b/xos/observers/monitoring_channel/templates/ceilometer_proxy_config.j2
new file mode 100644
index 0000000..4c712f1
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/ceilometer_proxy_config.j2
@@ -0,0 +1,16 @@
+# This file autogenerated by monitoring-channel observer
+# It contains a list of attributes to be used by ceilometer proxy web server
+# syntax: key=value
+
+[default]
+auth_url={{ auth_url }}
+admin_user={{ admin_user }}
+admin_tenant={{ admin_tenant }}
+admin_password={{ admin_password }}
+
+[allowed_tenants]
+{% if allowed_tenant_ids %}
+{% for tenant_id in allowed_tenant_ids %}
+{{ tenant_id }}
+{% endfor %}
+{% endif %}
diff --git a/xos/observers/monitoring_channel/templates/ceilometer_proxy_server.py b/xos/observers/monitoring_channel/templates/ceilometer_proxy_server.py
new file mode 100644
index 0000000..62f0804
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/ceilometer_proxy_server.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python
+import web
+import ConfigParser
+import io
+import json
+from ceilometerclient import client
+import logging
+import urllib
+import urllib2
+from wsgilog import WsgiLog
+
+web.config.debug=False
+
+logfile = "ceilometer_proxy_server.log"
+level=logging.INFO
+logger=logging.getLogger('ceilometer_proxy_server')
+logger.setLevel(level)
+handler=logging.handlers.RotatingFileHandler(logfile,maxBytes=1000000, backupCount=1)
+logger.addHandler(handler)
+
+class FileLog(WsgiLog):
+ def __init__(self, application):
+ WsgiLog.__init__(
+ self,
+ application,
+ logformat = '%(message)s',
+ tofile = True,
+ toprint = True,
+ prnlevel = level,
+ file = logfile,
+ backups =1
+ )
+ def __call__(self, environ, start_response):
+ def hstart_response(status, response_headers, *args):
+ out = start_response(status, response_headers, *args)
+ try:
+ logline=environ["SERVER_PROTOCOL"]+" "+environ["REQUEST_METHOD"]+" "+environ["REQUEST_URI"]+" - "+status
+ except err:
+ logline="Could not log <%s> due to err <%s>" % (str(environ), err)
+ logger.info(logline)
+
+ return out
+
+ return super(FileLog, self).__call__(environ, hstart_response)
+
+#TODOs:
+#-See if we can avoid using python-ceilometerclient and instead use the REST calls directly with AuthToken
+#
+urls = (
+ r'^/v2/meters$', 'meter_list',
+ r'^/v2/meters/(?P<meter_name>[A-Za-z0-9_:.\-]+)/statistics$', 'statistics_list',
+ r'^/v2/samples$', 'sample_list',
+ r'^/v2/resources$', 'resource_list',
+ r'^/v2/subscribe$', 'pubsub_handler',
+)
+
+app = web.application(urls, globals())
+
+config = None
+ceilometer_client = None
+
+
+def parse_ceilometer_proxy_config():
+ global config
+ config = ConfigParser.RawConfigParser(allow_no_value=True)
+ config.read('ceilometer_proxy_config')
+
+def ceilometerclient():
+ global config, ceilometer_client
+ if ceilometer_client:
+ return ceilometer_client
+
+ if not config:
+ parse_ceilometer_proxy_config()
+
+ keystone = {}
+ keystone['os_username']=config.get('default','admin_user')
+ keystone['os_password']=config.get('default','admin_password')
+ keystone['os_auth_url']=config.get('default','auth_url')
+ keystone['os_tenant_name']=config.get('default','admin_tenant')
+ ceilometer_client = client.get_client(2,**keystone)
+ logger.info('ceilometer get_client is successful')
+ return ceilometer_client
+
+def make_query(user_id=None, tenant_id=None, resource_id=None,
+ user_ids=None, tenant_ids=None, resource_ids=None):
+ """Returns query built from given parameters.
+
+ This query can be then used for querying resources, meters and
+ statistics.
+
+ :Parameters:
+ - `user_id`: user_id, has a priority over list of ids
+ - `tenant_id`: tenant_id, has a priority over list of ids
+ - `resource_id`: resource_id, has a priority over list of ids
+ - `user_ids`: list of user_ids
+ - `tenant_ids`: list of tenant_ids
+ - `resource_ids`: list of resource_ids
+ """
+ user_ids = user_ids or []
+ tenant_ids = tenant_ids or []
+ resource_ids = resource_ids or []
+
+ query = []
+ if user_id:
+ user_ids = [user_id]
+ for u_id in user_ids:
+ query.append({"field": "user_id", "op": "eq", "value": u_id})
+
+ if tenant_id:
+ tenant_ids = [tenant_id]
+ for t_id in tenant_ids:
+ query.append({"field": "project_id", "op": "eq", "value": t_id})
+
+ if resource_id:
+ resource_ids = [resource_id]
+ for r_id in resource_ids:
+ query.append({"field": "resource_id", "op": "eq", "value": r_id})
+
+ return query
+
+def filter_query_params(query_params):
+ new_query=[]
+ i=0
+ user_specified_tenants=[]
+ for field in query_params['q.field']:
+ if (field != 'project_id') and (field != 'project'):
+ query = {}
+ query['field']=field
+ if query_params['q.op'][i] != '':
+ query['op']=query_params['q.op'][i]
+ query['value']=query_params['q.value'][i]
+ new_query.append(query)
+ else:
+ user_specified_tenants.append(query_params['q.value'][i])
+ i=i+1
+ return new_query,user_specified_tenants
+
+class meter_list:
+ def GET(self):
+ global config
+ keyword_args = {
+ "q.field": [],
+ "q.op": [],
+ "q.type": [],
+ "q.value": [],
+ }
+ query_params = web.input(**keyword_args)
+ new_query, user_specified_tenants = filter_query_params(query_params)
+
+ client = ceilometerclient()
+ meters=[]
+ for (k,v) in config.items('allowed_tenants'):
+ if user_specified_tenants and (k not in user_specified_tenants):
+ continue
+ final_query=[]
+ final_query.extend(new_query)
+ query = make_query(tenant_id=k)
+ final_query.extend(query)
+ logger.debug('final query=%s',final_query)
+ results = client.meters.list(q=final_query)
+ meters.extend(results)
+ return json.dumps([ob._info for ob in meters])
+
+class statistics_list:
+ def GET(self, meter_name):
+ global config
+ keyword_args = {
+ "q.field": [],
+ "q.op": [],
+ "q.type": [],
+ "q.value": [],
+ "period": None
+ }
+ query_params = web.input(**keyword_args)
+ new_query, user_specified_tenants = filter_query_params(query_params)
+
+ client = ceilometerclient()
+ period = query_params.period
+ statistics = []
+ for (k,v) in config.items('allowed_tenants'):
+ if user_specified_tenants and (k not in user_specified_tenants):
+ continue
+ final_query=[]
+ final_query.extend(new_query)
+ query = make_query(tenant_id=k)
+ final_query.extend(query)
+ logger.debug('final query=%s',final_query)
+ results = client.statistics.list(meter_name=meter_name, q=final_query, period=period)
+ statistics.extend(results)
+ return json.dumps([ob._info for ob in statistics])
+
+class sample_list:
+ def GET(self):
+ global config
+ keyword_args = {
+ "q.field": [],
+ "q.op": [],
+ "q.type": [],
+ "q.value": [],
+ "limit": None,
+ }
+ query_params = web.input(**keyword_args)
+ new_query, user_specified_tenants = filter_query_params(query_params)
+
+ client = ceilometerclient()
+ limit=query_params.limit
+ samples=[]
+ for (k,v) in config.items('allowed_tenants'):
+ if user_specified_tenants and (k not in user_specified_tenants):
+ continue
+ final_query=[]
+ final_query.extend(new_query)
+ query = make_query(tenant_id=k)
+ final_query.extend(query)
+ logger.debug('final query=%s',final_query)
+ results = client.new_samples.list(q=final_query,limit=limit)
+ samples.extend(results)
+ return json.dumps([ob._info for ob in samples])
+
+class resource_list:
+ def GET(self):
+ global config
+ keyword_args = {
+ "q.field": [],
+ "q.op": [],
+ "q.type": [],
+ "q.value": [],
+ "limit": None,
+ "links": None,
+ }
+ query_params = web.input(**keyword_args)
+ new_query, user_specified_tenants = filter_query_params(query_params)
+
+ client = ceilometerclient()
+ limit=query_params.limit
+ links=query_params.links
+ resources=[]
+ for (k,v) in config.items('allowed_tenants'):
+ if user_specified_tenants and (k not in user_specified_tenants):
+ continue
+ final_query=[]
+ final_query.extend(new_query)
+ query = make_query(tenant_id=k)
+ final_query.extend(query)
+ logger.debug('final query=%s',final_query)
+ results = client.resources.list(q=final_query, limit=limit, links=links)
+ resources.extend(results)
+ return json.dumps([ob._info for ob in resources])
+
+class pubsub_handler:
+ def POST(self):
+ global config
+ parse_ceilometer_proxy_config()
+ data_str = unicode(web.data(),'iso-8859-1')
+ post_data = json.loads(data_str)
+ final_query=[]
+ for (k,v) in config.items('allowed_tenants'):
+ query = make_query(tenant_id=k)
+ final_query.extend(query)
+ if not final_query:
+ raise Exception("Not allowed to subscribe to any meters")
+ post_data["query"] = final_query
+ #TODO: The PUB/SUB url needs to be read from config
+ put_request = urllib2.Request("http://10.11.10.1:4455/subscribe", json.dumps(post_data))
+ put_request.get_method = lambda: 'SUB'
+ put_request.add_header('Content-Type', 'application/json')
+ response = urllib2.urlopen(put_request)
+ response_text = response.read()
+ return json.dumps(response_text)
+
+ def DELETE(self):
+ data_str = web.data()
+ #TODO: The PUB/SUB url needs to be read from config
+ put_request = urllib2.Request("http://10.11.10.1:4455/unsubscribe", data_str)
+ put_request.get_method = lambda: 'UNSUB'
+ put_request.add_header('Content-Type', 'application/json')
+ response = urllib2.urlopen(put_request)
+ response_text = response.read()
+ return json.dumps(response_text)
+
+if __name__ == "__main__":
+ app.run(FileLog)
diff --git a/xos/observers/monitoring_channel/templates/monitoring-channel.conf.j2 b/xos/observers/monitoring_channel/templates/monitoring-channel.conf.j2
new file mode 100644
index 0000000..eb937ac
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/monitoring-channel.conf.j2
@@ -0,0 +1,10 @@
+# Upstart script for Monitoring channel
+description "Upstart script for Monitoring channel container"
+author "andy@onlab.us"
+start on filesystem and started docker
+stop on runlevel [!2345]
+respawn
+
+script
+ /usr/local/sbin/start-monitoring-channel-{{ unique_id }}.sh
+end script
diff --git a/xos/observers/monitoring_channel/templates/start-monitoring-channel.sh.j2 b/xos/observers/monitoring_channel/templates/start-monitoring-channel.sh.j2
new file mode 100755
index 0000000..f56c247
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/start-monitoring-channel.sh.j2
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+function mac_to_iface {
+ MAC=$1
+ ifconfig|grep $MAC| awk '{print $1}'|grep -v '\.'
+}
+
+function generate_mac_from_ip {
+ IP=$1
+ printf "02:42:%02x:%02x:%02x:%02x\n" `echo $IP|awk -F '.' '{print $1, $2, $3, $4}'`
+}
+
+iptables -L > /dev/null
+ip6tables -L > /dev/null
+
+MONITORING_CHANNEL=monitoring-channel-{{ unique_id }}
+HEADNODEFLATLANIP={{ headnode_flat_lan_ip }}
+HOST_FORWARDING_PORT_FOR_CEILOMETER={{ ceilometer_host_port }}
+
+docker inspect $MONITORING_CHANNEL > /dev/null 2>&1
+if [ "$?" == 1 ]
+then
+ #sudo docker build -t monitoring-channel -f Dockerfile.monitoring_channel .
+ sudo docker pull srikanthvavila/monitoring-channel
+ docker run -d --name=$MONITORING_CHANNEL --add-host="ctl:$HEADNODEFLATLANIP" --privileged=true -p $HOST_FORWARDING_PORT_FOR_CEILOMETER:8000 srikanthvavila/monitoring-channel
+else
+ docker start $MONITORING_CHANNEL
+fi
+
+# Set up networking via pipework
+#SHARED_LAN_IFACE=$( mac_to_iface {{ shared_lan_mac }} )
+#docker exec $MONITORING_CHANNEL ifconfig eth0 >> /dev/null || pipework $SHARED_LAN_IFACE -i eth0 $MONITORING_CHANNEL 192.168.0.1/24
+
+# Make sure VM's eth0 (hpc_client) has no IP address
+#ifconfig $HPC_IFACE 0.0.0.0
+
+# Now copy ceilometer proxy configuration to container
+cat /usr/local/share/monitoring-channel-{{ unique_id }}_ceilometer_proxy_config | docker exec -i $MONITORING_CHANNEL bash -c 'cat > /usr/local/share/ceilometer_proxy_config'
+
+# Attach to container
+docker start -a $MONITORING_CHANNEL
diff --git a/xos/observers/monitoring_channel/templates/start_ceilometer_proxy b/xos/observers/monitoring_channel/templates/start_ceilometer_proxy
new file mode 100644
index 0000000..ddaa9c8
--- /dev/null
+++ b/xos/observers/monitoring_channel/templates/start_ceilometer_proxy
@@ -0,0 +1 @@
+/usr/local/share/ceilometer_proxy_server.py 8000
diff --git a/xos/observers/onos/model-deps b/xos/observers/onos/model-deps
new file mode 100644
index 0000000..2da80e0
--- /dev/null
+++ b/xos/observers/onos/model-deps
@@ -0,0 +1,5 @@
+{
+ "ONOSApp": [
+ "ONOSService"
+ ]
+}
diff --git a/xos/observers/onos/onos-observer.py b/xos/observers/onos/onos-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/onos/onos-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+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/onos/onos_observer_config b/xos/observers/onos/onos_observer_config
new file mode 100644
index 0000000..3c6d63d
--- /dev/null
+++ b/xos/observers/onos/onos_observer_config
@@ -0,0 +1,41 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=onos
+dependency_graph=/opt/xos/observers/onos/model-deps
+steps_dir=/opt/xos/observers/onos/steps
+sys_dir=/opt/xos/observers/onos/sys
+deleters_dir=/opt/xos/observers/onos/deleters
+log_file=console
+driver=None
+pretend=False
+backoff_disabled=True
+save_ansible_output=True
+# set proxy_ssh to false on cloudlab
+proxy_ssh=False
+full_setup=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/onos/run.sh b/xos/observers/onos/run.sh
new file mode 100755
index 0000000..ea4c511
--- /dev/null
+++ b/xos/observers/onos/run.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vcpe-observer.py ]]; then
+# ln -s ../../xos-observer.py vcpe-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+python onos-observer.py -C $XOS_DIR/observers/onos/onos_observer_config
diff --git a/xos/observers/onos/scripts/dockerip.sh b/xos/observers/onos/scripts/dockerip.sh
new file mode 100644
index 0000000..732c3fe
--- /dev/null
+++ b/xos/observers/onos/scripts/dockerip.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+MODE=`docker inspect --format '{{ .HostConfig.NetworkMode }}' $1 | tr -d '\n' | tr -d '\r'`
+if [[ "$MODE" == "host" ]]; then
+ echo -n "127.0.0.1"
+else
+ docker inspect --format '{{ .NetworkSettings.IPAddress }}' $1 | tr -d '\n' | tr -d '\r'
+fi
+
diff --git a/xos/observers/onos/start.sh b/xos/observers/onos/start.sh
new file mode 100755
index 0000000..c13ffbe
--- /dev/null
+++ b/xos/observers/onos/start.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vcpe-observer.py ]]; then
+# ln -s ../../xos-observer.py vcpe-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+nohup python onos-observer.py -C $XOS_DIR/observers/onos/onos_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/onos/steps/sync_onosapp.py b/xos/observers/onos/steps/sync_onosapp.py
new file mode 100644
index 0000000..047306d
--- /dev/null
+++ b/xos/observers/onos/steps/sync_onosapp.py
@@ -0,0 +1,167 @@
+import hashlib
+import os
+import socket
+import socket
+import sys
+import base64
+import time
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from observers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+from core.models import Service, Slice
+from services.onos.models import ONOSService, ONOSApp
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncONOSApp(SyncInstanceUsingAnsible):
+ provides=[ONOSApp]
+ observes=ONOSApp
+ requested_interval=0
+ template_name = "sync_onosapp.yaml"
+ service_key_name = "/opt/xos/observers/onos/onos_key"
+
+ def __init__(self, *args, **kwargs):
+ super(SyncONOSApp, self).__init__(*args, **kwargs)
+
+ def fetch_pending(self, deleted):
+ if (not deleted):
+ objs = ONOSApp.get_tenant_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
+ else:
+ objs = ONOSApp.get_deleted_tenant_objects()
+
+ return objs
+
+ def get_instance(self, o):
+ # We assume the ONOS service owns a slice, so pick one of the instances
+ # inside that slice to sync to.
+
+ serv = self.get_onos_service(o)
+
+ if serv.use_external_host:
+ return serv.use_external_host
+
+ if serv.slices.exists():
+ slice = serv.slices.all()[0]
+ if slice.instances.exists():
+ return slice.instances.all()[0]
+
+ return None
+
+ def get_onos_service(self, o):
+ if not o.provider_service:
+ return None
+
+ onoses = ONOSService.get_service_objects().filter(id=o.provider_service.id)
+ if not onoses:
+ return None
+
+ return onoses[0]
+
+ def get_files_dir(self, o):
+ if not hasattr(Config(), "observer_steps_dir"):
+ # make steps_dir mandatory; there's no valid reason for it to not
+ # be defined.
+ raise Exception("observer_steps_dir is not defined in config file")
+
+ step_dir = Config().observer_steps_dir
+
+ return os.path.join(step_dir, "..", "files", str(self.get_onos_service(o).id), o.name)
+
+ def get_cluster_configuration(self, o):
+ instance = self.get_instance(o)
+ if not instance:
+ raise "No instance for ONOS App"
+ node_ips = [socket.gethostbyname(instance.node.name)]
+
+ ipPrefix = ".".join(node_ips[0].split(".")[:3]) + ".*"
+ result = '{ "nodes": ['
+ result = result + ",".join(['{ "ip": "%s"}' % ip for ip in node_ips])
+ result = result + '], "ipPrefix": "%s"}' % ipPrefix
+ return result
+
+
+ def write_configs(self, o):
+ o.config_fns = []
+ o.rest_configs = []
+ o.files_dir = self.get_files_dir(o)
+
+ if not os.path.exists(o.files_dir):
+ os.makedirs(o.files_dir)
+
+ # Combine the service attributes with the tenant attributes. Tenant
+ # attribute can override service attributes.
+ attrs = o.provider_service.serviceattribute_dict
+ attrs.update(o.tenantattribute_dict)
+
+ ordered_attrs = attrs.keys()
+
+ o.early_rest_configs=[]
+ if ("cordvtn" in o.dependencies):
+ # For VTN, since it's running in a docker host container, we need
+ # to make sure it configures the cluster using the right ip addresses.
+ # NOTE: rest_onos/v1/cluster/configuration/ will reboot the cluster and
+ # must go first.
+ name="rest_onos/v1/cluster/configuration/"
+ value= self.get_cluster_configuration(o)
+ fn = name[5:].replace("/","_")
+ endpoint = name[5:]
+ file(os.path.join(o.files_dir, fn),"w").write(" " +value)
+ o.early_rest_configs.append( {"endpoint": endpoint, "fn": fn} )
+
+ for name in attrs.keys():
+ value = attrs[name]
+ if name.startswith("config_"):
+ fn = name[7:] # .replace("_json",".json")
+ o.config_fns.append(fn)
+ file(os.path.join(o.files_dir, fn),"w").write(value)
+ if name.startswith("rest_"):
+ fn = name[5:].replace("/","_")
+ endpoint = name[5:]
+ # Ansible goes out of it's way to make our life difficult. If
+ # 'lookup' sees a file that it thinks contains json, then it'll
+ # insist on parsing and return a json object. We just want
+ # a string, so prepend a space and then strip the space off
+ # later.
+ file(os.path.join(o.files_dir, fn),"w").write(" " +value)
+ o.rest_configs.append( {"endpoint": endpoint, "fn": fn} )
+
+ def prepare_record(self, o):
+ self.write_configs(o)
+
+ def get_extra_attributes(self, o):
+ instance = self.get_instance(o)
+
+ fields={}
+ fields["files_dir"] = o.files_dir
+ fields["appname"] = o.name
+ fields["nat_ip"] = instance.get_ssh_ip()
+ fields["config_fns"] = o.config_fns
+ fields["rest_configs"] = o.rest_configs
+ fields["early_rest_configs"] = o.early_rest_configs
+ if o.dependencies:
+ fields["dependencies"] = [x.strip() for x in o.dependencies.split(",")]
+ else:
+ fields["dependencies"] = []
+
+ if (instance.isolation=="container"):
+ fields["ONOS_container"] = "%s-%s" % (instance.slice.name, str(instance.id))
+ else:
+ fields["ONOS_container"] = "ONOS"
+ return fields
+
+ def sync_fields(self, o, fields):
+ # the super causes the playbook to be run
+ super(SyncONOSApp, self).sync_fields(o, fields)
+
+ def run_playbook(self, o, fields):
+ super(SyncONOSApp, self).run_playbook(o, fields)
+
+ def delete_record(self, m):
+ pass
diff --git a/xos/observers/onos/steps/sync_onosapp.yaml b/xos/observers/onos/steps/sync_onosapp.yaml
new file mode 100644
index 0000000..a03368b
--- /dev/null
+++ b/xos/observers/onos/steps/sync_onosapp.yaml
@@ -0,0 +1,114 @@
+---
+- hosts: {{ instance_name }}
+ gather_facts: False
+ connection: ssh
+ user: {{ username }}
+ sudo: yes
+ vars:
+ appname: {{ appname }}
+ dependencies: {{ dependencies }}
+{% if rest_configs %}
+ rest_configs:
+{% for rest_config in rest_configs %}
+ - endpoint: {{ rest_config.endpoint }}
+ body: "{{ '{{' }} lookup('file', '{{ files_dir }}/{{ rest_config.fn }}') {{ '}}' }}"
+{% endfor %}
+{% endif %}
+{% if early_rest_configs %}
+ early_rest_configs:
+{% for early_rest_config in early_rest_configs %}
+ - endpoint: {{ early_rest_config.endpoint }}
+ body: "{{ '{{' }} lookup('file', '{{ files_dir }}/{{ early_rest_config.fn }}') {{ '}}' }}"
+{% endfor %}
+{% endif %}
+
+ tasks:
+
+ - name: Get Docker IP
+ script: /opt/xos/observers/onos/scripts/dockerip.sh {{ ONOS_container }}
+ register: onosaddr
+
+ - name: Wait for ONOS to come up
+ wait_for:
+ host={{ '{{' }} onosaddr.stdout {{ '}}' }}
+ port={{ '{{' }} item {{ '}}' }}
+ state=present
+ with_items:
+ - 8101
+ - 8181
+ - 9876
+
+ - name: Config file directory
+ file:
+ path=/home/ubuntu/{{ appname }}/
+ state=directory
+
+{% if config_fns %}
+ - name: Copy over configuration files
+ copy:
+ src={{ files_dir }}/{{ '{{' }} item {{ '}}' }}
+ dest=/home/ubuntu/{{ appname }}/{{ '{{' }} item {{ '}}' }}
+ with_items:
+ {% for config_fn in config_fns %}
+ - {{ config_fn }}
+ {% endfor %}
+
+ - name: Make sure config directory exists
+ shell: docker exec {{ ONOS_container }} mkdir -p /root/onos/config/
+ sudo: yes
+
+ - name: Copy config files into container
+ shell: docker cp {{ appname }}/{{ '{{' }} item {{ '}}' }} {{ ONOS_container }}:/root/onos/config/
+ sudo: yes
+ with_items:
+ {% for config_fn in config_fns %}
+ - {{ config_fn }}
+ {% endfor %}
+{% endif %}
+
+ # Don't know how to check for this condition, just wait
+ - name: Wait for ONOS to install the apps
+ wait_for: timeout=15
+
+{% if early_rest_configs %}
+ - name: Add ONOS early configuration values
+ uri:
+ url: http://{{ '{{' }} onosaddr.stdout {{ '}}' }}:8181/{{ '{{' }} item.endpoint {{ '}}' }}
+ body: "{{ '{{' }} item.body {{ '}}' }}"
+ body_format: raw
+ method: POST
+ user: karaf
+ password: karaf
+ with_items: "early_rest_configs"
+
+ # Don't know how to check for this condition, just wait
+ - name: Wait for ONOS to restart
+ wait_for: timeout=15
+{% endif %}
+
+{% if dependencies %}
+ - name: Add dependencies to ONOS
+ uri:
+ url: http://{{ '{{' }} onosaddr.stdout {{ '}}' }}:8181/onos/v1/applications/{{ '{{' }} item {{ '}}' }}/active
+ method: POST
+ user: karaf
+ password: karaf
+ with_items:
+ {% for dependency in dependencies %}
+ - {{ dependency }}
+ {% endfor %}
+{% endif %}
+
+{% if rest_configs %}
+# Do this after services have been activated, or it will cause an exception.
+# vOLT will re-read its net config; vbng may not.
+ - name: Add ONOS configuration values
+ uri:
+ url: http://{{ '{{' }} onosaddr.stdout {{ '}}' }}:8181/{{ '{{' }} item.endpoint {{ '}}' }} #http://localhost:8181/onos/v1/network/configuration/
+ body: "{{ '{{' }} item.body {{ '}}' }}"
+ body_format: raw
+ method: POST
+ user: karaf
+ password: karaf
+ with_items: "rest_configs"
+{% endif %}
diff --git a/xos/observers/onos/steps/sync_onosservice.py b/xos/observers/onos/steps/sync_onosservice.py
new file mode 100644
index 0000000..efa5d72
--- /dev/null
+++ b/xos/observers/onos/steps/sync_onosservice.py
@@ -0,0 +1,77 @@
+import hashlib
+import os
+import socket
+import sys
+import base64
+import time
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from observers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+from core.models import Service, Slice
+from services.onos.models import ONOSService, ONOSApp
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncONOSService(SyncInstanceUsingAnsible):
+ provides=[ONOSService]
+ observes=ONOSService
+ requested_interval=0
+ template_name = "sync_onosservice.yaml"
+ service_key_name = "/opt/xos/observers/onos/onos_key"
+
+ def __init__(self, *args, **kwargs):
+ super(SyncONOSService, self).__init__(*args, **kwargs)
+
+ def fetch_pending(self, deleted):
+ if (not deleted):
+ objs = ONOSService.get_service_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
+ else:
+ objs = ONOSService.get_deleted_service_objects()
+
+ return objs
+
+ def get_instance(self, o):
+ # We assume the ONOS service owns a slice, so pick one of the instances
+ # inside that slice to sync to.
+
+ serv = o
+
+ if serv.use_external_host:
+ return serv.use_external_host
+
+ if serv.slices.exists():
+ slice = serv.slices.all()[0]
+ if slice.instances.exists():
+ return slice.instances.all()[0]
+
+ return None
+
+ def get_extra_attributes(self, o):
+ fields={}
+ fields["instance_hostname"] = self.get_instance(o).instance_name.replace("_","-")
+ fields["appname"] = o.name
+ fields["nat_ip"] = self.get_instance(o).get_ssh_ip()
+ fields["ONOS_container"] = "ONOS"
+ return fields
+
+ def sync_fields(self, o, fields):
+ # the super causes the playbook to be run
+ super(SyncONOSService, self).sync_fields(o, fields)
+
+ def run_playbook(self, o, fields):
+ instance = self.get_instance(o)
+ if (instance.isolation=="container"):
+ # If the instance is already a container, then we don't need to
+ # install ONOS.
+ return
+ super(SyncONOSService, self).run_playbook(o, fields)
+
+ def delete_record(self, m):
+ pass
diff --git a/xos/observers/onos/steps/sync_onosservice.yaml b/xos/observers/onos/steps/sync_onosservice.yaml
new file mode 100644
index 0000000..fd9c3db
--- /dev/null
+++ b/xos/observers/onos/steps/sync_onosservice.yaml
@@ -0,0 +1,66 @@
+---
+- hosts: {{ instance_name }}
+ gather_facts: False
+ connection: ssh
+ user: ubuntu
+ sudo: yes
+
+ tasks:
+
+ - name: Fix /etc/hosts
+ lineinfile:
+ dest=/etc/hosts
+ regexp="127.0.0.1 localhost"
+ line="127.0.0.1 localhost {{ instance_hostname }}"
+
+ - name: Add repo key
+ apt_key:
+ keyserver=hkp://pgp.mit.edu:80
+ id=58118E89F3A912897C070ADBF76221572C52609D
+
+ - name: Install Docker repo
+ apt_repository:
+ repo="deb https://apt.dockerproject.org/repo ubuntu-trusty main"
+ state=present
+
+ - name: Install Docker
+ apt:
+ name={{ '{{' }} item {{ '}}' }}
+ state=latest
+ update_cache=yes
+ with_items:
+ - docker-engine
+ - python-pip
+ - python-httplib2
+
+ - name: Install docker-py
+ pip:
+ name=docker-py
+ state=latest
+
+ - name: Start ONOS container
+ docker:
+ docker_api_version: "1.18"
+ name: {{ ONOS_container }}
+ # was: reloaded
+ state: running
+ image: onosproject/onos
+ ports:
+ - "6653:6653"
+ - "8101:8101"
+ - "8181:8181"
+ - "9876:9876"
+
+ - name: Get Docker IP
+ script: /opt/xos/observers/onos/scripts/dockerip.sh {{ ONOS_container }}
+ register: dockerip
+
+ - name: Wait for ONOS to come up
+ wait_for:
+ host={{ '{{' }} dockerip.stdout {{ '}}' }}
+ port={{ '{{' }} item {{ '}}' }}
+ state=present
+ with_items:
+ - 8101
+ - 8181
+ - 9876
diff --git a/xos/observers/onos/stop.sh b/xos/observers/onos/stop.sh
new file mode 100755
index 0000000..17d6eb7
--- /dev/null
+++ b/xos/observers/onos/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f onos-observer.py
diff --git a/xos/observers/onos/supervisor/onos-observer.conf b/xos/observers/onos/supervisor/onos-observer.conf
new file mode 100644
index 0000000..36e00d9
--- /dev/null
+++ b/xos/observers/onos/supervisor/onos-observer.conf
@@ -0,0 +1,9 @@
+[supervisord]
+logfile=/var/log/supervisord.log ; (main log file;default $CWD/supervisord.log)
+pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
+nodaemon=true
+
+[program:synchronizer]
+command=python /opt/xos/observers/onos/onos-observer.py -C /opt/xos/observers/onos/onos_observer_config
+stderr_logfile=/var/log/supervisor/synchronizer.err.log
+stdout_logfile=/var/log/supervisor/synchronizer.out.log
diff --git a/xos/observers/requestrouter/configurationPush.py b/xos/observers/requestrouter/configurationPush.py
new file mode 100644
index 0000000..857de8b
--- /dev/null
+++ b/xos/observers/requestrouter/configurationPush.py
@@ -0,0 +1,30 @@
+import ansible.playbook
+import ansible.constants as C
+import ansible.utils.template
+from ansible import errors
+from ansible import callbacks
+from ansible import utils
+from subprocess import call
+
+class ConfigurationPush:
+ def __init__(self):
+ pass
+
+ def config_push(self, service_name, user, playbook_name,hostfile):
+ '''stats = callbacks.AggregateStats()
+ playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
+ runner_cb = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY)
+ pb = ansible.playbook.PlayBook(playbook="playbook/site.yml",
+ callbacks=playbook_cb,
+ runner_callbacks=runner_cb,
+ stats=stats
+ )
+ result = pb.run()
+ print result
+ '''
+
+ call("ansible-playbook --private-key=planetw "+playbook_name+" -i "+hostfile+" -u "+user+" --extra-vars \"name="+service_name+"\"", shell=True)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/xos/observers/requestrouter/deleters/servicemap_deleter.py b/xos/observers/requestrouter/deleters/servicemap_deleter.py
new file mode 100644
index 0000000..427242f
--- /dev/null
+++ b/xos/observers/requestrouter/deleters/servicemap_deleter.py
@@ -0,0 +1,42 @@
+import os
+import sys
+import traceback
+from services.requestrouter.models import ServiceMap
+from synchronizers.base.deleter import Deleter
+from util.logger import Logger, logging
+from xos.config import Config, XOS_DIR
+
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from rrlib import RequestRouterLibrary
+from configurationPush import ConfigurationPush
+import rrlib_config
+
+logger = Logger(level=logging.INFO)
+
+class ServiceMapDeleter(Deleter, RequestRouterLibrary, ConfigurationPush):
+ model='ServiceMap'
+
+ def __init__(self, **args):
+ Deleter.__init__(self, **args)
+ RequestRouterLibrary.__init__(self)
+ ConfigurationPush.__init__(self)
+
+
+ def call(self, pk, model_dict):
+ try:
+ servicemap = ServiceMap.objects.get(pk=pk)
+ service_uid = self.get_servicemap_uid(servicemap)
+ self.config_push(service_uid, rrlib_config.REDIR_USER, XOS_DIR + "/observers/requestrouter/playbook/site_redir_delete.yml", "/etc/ansible/requestrouter/dnsredir/hosts")
+ self.config_push(service_uid, rrlib_config.DEMUX_USER, XOS_DIR + "/observers/requestrouter/playbook/site_demux_delete.yml", "/etc/ansible/requestrouter/dnsdemux/hosts")
+ print "XXX delete ServiceMap %s", servicemap.name
+ return True
+ except Exception, e:
+ traceback.print_exc()
+ logger.exception("Failed to erase map '%s'" % map_name)
+ return False
+
+if __name__ == "__main__":
+ smap = ServiceMapDeleter()
+ smap.call( 6, {'name': 'Service23'} )
diff --git a/xos/observers/requestrouter/model-deps b/xos/observers/requestrouter/model-deps
new file mode 100644
index 0000000..36ef620
--- /dev/null
+++ b/xos/observers/requestrouter/model-deps
@@ -0,0 +1,12 @@
+{
+ "ServiceMap": [
+ "Slice"
+ ],
+ "Slice": [
+ "Site",
+ "Service"
+ ],
+ "User": [
+ "Site"
+ ]
+}
diff --git a/xos/observers/requestrouter/playbook/roles/delete_demux/handlers/main.yml b/xos/observers/requestrouter/playbook/roles/delete_demux/handlers/main.yml
new file mode 100644
index 0000000..a888468
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_demux/handlers/main.yml
@@ -0,0 +1,3 @@
+---
+- name: restart dnsdemux
+ service: name=dnsdemux state=restarted
diff --git a/xos/observers/requestrouter/playbook/roles/delete_demux/tasks/main.yml b/xos/observers/requestrouter/playbook/roles/delete_demux/tasks/main.yml
new file mode 100644
index 0000000..417401a
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_demux/tasks/main.yml
@@ -0,0 +1,5 @@
+---
+# This playbook contains plays to delete configuration files from dnsdemux slice
+- name: delete dnsdemux config files
+ file: path={{dst_dnsdemux_conf}}/{{name}}.conf state=absent
+ notify: restart dnsdemux
diff --git a/xos/observers/requestrouter/playbook/roles/delete_demux/vars/main.yml b/xos/observers/requestrouter/playbook/roles/delete_demux/vars/main.yml
new file mode 100644
index 0000000..b457050
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_demux/vars/main.yml
@@ -0,0 +1,6 @@
+---
+# Variables listed here are applicable to all host groups
+
+src_dnsdemux: ../../../../temp_config/dnsdemux
+dst_dnsdemux_conf: /etc/dnsdemux/default/
+
diff --git a/xos/observers/requestrouter/playbook/roles/delete_redir/handlers/main.yml b/xos/observers/requestrouter/playbook/roles/delete_redir/handlers/main.yml
new file mode 100644
index 0000000..8aa497e
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_redir/handlers/main.yml
@@ -0,0 +1,3 @@
+---
+- name: restart dnsredir
+ service: name=dnsredir state=restarted
diff --git a/xos/observers/requestrouter/playbook/roles/delete_redir/tasks/main.yml b/xos/observers/requestrouter/playbook/roles/delete_redir/tasks/main.yml
new file mode 100644
index 0000000..58833bb
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_redir/tasks/main.yml
@@ -0,0 +1,13 @@
+---
+# This playbook contains plays to delete configuration files from dnsredir slice
+- name: delete maps.d directory ( and deletes all intermeadiate directories )
+ #copy: src={{src_dnsredir}}/{{name}}.d dest={{dst_dnsredir_confdir}}
+ file: path={{dst_dnsredir_confdir}}/{{name}}.d/maps.d state=absent
+
+- name: delete config directory
+ file: path={{dst_dnsredir_confdir}}/{{name}}.d/ state=absent
+
+- name: copy dnsredir config file
+ file: path={{dst_dnsredir_conf}}/{{name}}.conf state=absent
+ notify: restart dnsredir
+
diff --git a/xos/observers/requestrouter/playbook/roles/delete_redir/vars/main.yml b/xos/observers/requestrouter/playbook/roles/delete_redir/vars/main.yml
new file mode 100644
index 0000000..dc79d35
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/delete_redir/vars/main.yml
@@ -0,0 +1,6 @@
+---
+# Variables listed here are applicable to all host groups
+
+src_dnsredir: ../../../../temp_config/dnsredir
+dst_dnsredir_conf: /etc/dnsredir/conf.d/
+dst_dnsredir_confdir: /etc/dnsredir
diff --git a/xos/observers/requestrouter/playbook/roles/demux/handlers/main.yml b/xos/observers/requestrouter/playbook/roles/demux/handlers/main.yml
new file mode 100644
index 0000000..a888468
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/demux/handlers/main.yml
@@ -0,0 +1,3 @@
+---
+- name: restart dnsdemux
+ service: name=dnsdemux state=restarted
diff --git a/xos/observers/requestrouter/playbook/roles/demux/tasks/main.yml b/xos/observers/requestrouter/playbook/roles/demux/tasks/main.yml
new file mode 100644
index 0000000..0339aed
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/demux/tasks/main.yml
@@ -0,0 +1,9 @@
+---
+# This playbook contains plays to copy configurations to destination
+- name: copy dnsdemux config files
+ copy: src={{src_dnsdemux}}/{{name}}.conf dest={{dst_dnsdemux_conf}}/{{name}}.conf
+ notify: restart dnsdemux
+
+#- name: Start the dnsdemux service
+ #service: name=dnsdemux state=started enabled=true
+
diff --git a/xos/observers/requestrouter/playbook/roles/demux/vars/main.yml b/xos/observers/requestrouter/playbook/roles/demux/vars/main.yml
new file mode 100644
index 0000000..b457050
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/demux/vars/main.yml
@@ -0,0 +1,6 @@
+---
+# Variables listed here are applicable to all host groups
+
+src_dnsdemux: ../../../../temp_config/dnsdemux
+dst_dnsdemux_conf: /etc/dnsdemux/default/
+
diff --git a/xos/observers/requestrouter/playbook/roles/redir/handlers/main.yml b/xos/observers/requestrouter/playbook/roles/redir/handlers/main.yml
new file mode 100644
index 0000000..8aa497e
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/redir/handlers/main.yml
@@ -0,0 +1,3 @@
+---
+- name: restart dnsredir
+ service: name=dnsredir state=restarted
diff --git a/xos/observers/requestrouter/playbook/roles/redir/tasks/main.yml b/xos/observers/requestrouter/playbook/roles/redir/tasks/main.yml
new file mode 100644
index 0000000..e390ed9
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/redir/tasks/main.yml
@@ -0,0 +1,19 @@
+---
+# This playbook contains plays to copy configurations to destination
+- name: create maps.d directory ( and creates all intermeadiate directories )
+ #copy: src={{src_dnsredir}}/{{name}}.d dest={{dst_dnsredir_confdir}}
+ file: src={{src_dnsredir}}/{{name}}.d/maps.d dest={{dst_dnsredir_confdir}}/{{name}}.d/maps.d state=directory
+
+- name: copy map.conf
+ copy: src={{src_dnsredir}}/{{name}}.d/maps.d/map.conf dest={{dst_dnsredir_confdir}}/{{name}}.d/maps.d/map.conf
+
+- name: copy codeen_nodes.conf
+ copy: src={{src_dnsredir}}/{{name}}.d/codeen_nodes.conf dest={{dst_dnsredir_confdir}}/{{name}}.d/codeen_nodes.conf
+
+- name: copy node-to-ip.txt
+ copy: src={{src_dnsredir}}/{{name}}.d/node-to-ip.txt dest={{dst_dnsredir_confdir}}/{{name}}.d/node-to-ip.txt
+
+- name: copy dnsredir config file
+ copy: src={{src_dnsredir}}/{{name}}.conf dest={{dst_dnsredir_conf}}/{{name}}.conf
+ notify: restart dnsredir
+
diff --git a/xos/observers/requestrouter/playbook/roles/redir/vars/main.yml b/xos/observers/requestrouter/playbook/roles/redir/vars/main.yml
new file mode 100644
index 0000000..dc79d35
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/roles/redir/vars/main.yml
@@ -0,0 +1,6 @@
+---
+# Variables listed here are applicable to all host groups
+
+src_dnsredir: ../../../../temp_config/dnsredir
+dst_dnsredir_conf: /etc/dnsredir/conf.d/
+dst_dnsredir_confdir: /etc/dnsredir
diff --git a/xos/observers/requestrouter/playbook/site_demux.yml b/xos/observers/requestrouter/playbook/site_demux.yml
new file mode 100644
index 0000000..b5261dc
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/site_demux.yml
@@ -0,0 +1,9 @@
+---
+# This playbook copies the dnsdemux configuration files from temp directory.
+
+- name: copies the configuration files from temp directory
+ hosts: all
+ #remote_user: {{r_user}}
+
+ roles:
+ - demux
diff --git a/xos/observers/requestrouter/playbook/site_demux_delete.yml b/xos/observers/requestrouter/playbook/site_demux_delete.yml
new file mode 100644
index 0000000..49a7c87
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/site_demux_delete.yml
@@ -0,0 +1,9 @@
+---
+# This playbook deletes the dnsdemux configuration files from request router instances
+
+- name: deletes the configuration files from request router instances
+ hosts: all
+ #remote_user: {{r_user}}
+
+ roles:
+ - delete_demux
diff --git a/xos/observers/requestrouter/playbook/site_redir.yml b/xos/observers/requestrouter/playbook/site_redir.yml
new file mode 100644
index 0000000..50a7284
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/site_redir.yml
@@ -0,0 +1,9 @@
+---
+# This playbook copies the dnsredir configuration files from temp directory.
+
+- name: copies the configuration files from temp directory
+ hosts: all
+ #remote_user: {{r_user}}
+
+ roles:
+ - redir
diff --git a/xos/observers/requestrouter/playbook/site_redir_delete.yml b/xos/observers/requestrouter/playbook/site_redir_delete.yml
new file mode 100644
index 0000000..9a8611d
--- /dev/null
+++ b/xos/observers/requestrouter/playbook/site_redir_delete.yml
@@ -0,0 +1,9 @@
+---
+# This playbook deletes the dnsredir configuration files from request router instances
+
+- name: deletes the configuration files from request router instances
+ hosts: all
+ #remote_user: {{r_user}}
+
+ roles:
+ - delete_redir
diff --git a/xos/observers/requestrouter/rr_observer_config b/xos/observers/requestrouter/rr_observer_config
new file mode 100644
index 0000000..ec3a1ba
--- /dev/null
+++ b/xos/observers/requestrouter/rr_observer_config
@@ -0,0 +1,36 @@
+
+[plc]
+name=plc
+#deployment=VICCI
+deployment=VINI
+
+[db]
+name=xos
+user=plstackuser
+#password=2uMDYtJK
+password=1HL07C0E
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+dependency_graph=/opt/xos/observers/requestrouter/model-deps
+steps_dir=/opt/xos/observers/requestrouter/steps
+deleters_dir=/opt/xos/observers/requestrouter/deleters
+log_file=console
+#/var/log/hpc.log
+driver=None
+
+#[feefie]
+#client_id='vicci_dev_central'
+#user_id='pl'
diff --git a/xos/observers/requestrouter/rrlib.py b/xos/observers/requestrouter/rrlib.py
new file mode 100644
index 0000000..9faca4a
--- /dev/null
+++ b/xos/observers/requestrouter/rrlib.py
@@ -0,0 +1,203 @@
+import os
+import base64
+import string
+import sys
+import socket
+from sets import Set
+if __name__ == '__main__':
+ sys.path.append("/opt/xos")
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+from xos.config import Config
+from core.models import Service
+from services.requestrouter.models import RequestRouterService, ServiceMap
+from util.logger import Logger, logging
+import rrlib_config
+
+logger = Logger(level=logging.INFO)
+
+'''
+Conventions:
+1) All dnsredir backend will listen at port 9000+ servicemap.pk ( where pk is the primary key generated in django model)
+'''
+
+class RequestRouterLibrary:
+
+ def __init__(self):
+ pass
+
+ def gen_slice_info(self, service=None):
+ """generates instance information from slice of request router
+ """
+
+ if (service is None ):
+ service = RequestRouterService.objects.get()
+
+ mapping = {}
+ #static mapping for demo purpose
+ #mapping["node47.princeton.vicci.org"] = "128.112.171.112"
+ mapping["node48.princeton.vicci.org"] = "128.112.171.114"
+
+ '''for slice in service.service.all():
+ name = slice.name
+ for instance in slice.instances.all():
+ mapping[instance.name] = str(instance.ip)
+ '''
+ return mapping
+
+ def gen_servicemap_slice_info(self, servicemap):
+ """generates instance information from slice of servicemap
+ """
+
+ wzone = Set(['arizona', 'stanford', 'on.lab', 'housten']) # zone=1 in cooden.conf
+ ezone = Set(['princeton', 'atlanta', 'new york', 'georgia tech']) # zone=2 in coodeen.conf
+
+ mapping_zone = {}
+ mapping_ip = {}
+ slice = servicemap.slice
+ name = slice.name
+ for instance in slice.instances.all():
+ mapping_ip[instance.node.name] = socket.gethostbyname(instance.node.name)
+ #print "instance name "+instance.name+str(instance.ip)+"\n"
+ site = instance.node.site.name
+ if(site.lower() in wzone):
+ mapping_zone[instance.node.name] = str(1)
+ else:
+ mapping_zone[instance.node.name] = str(2)
+
+ return mapping_ip, mapping_zone
+
+
+
+ def gen_slice_file(self, service):
+ """ generates host file for the slice information
+ to be used by ansible to push configuration files
+ """
+
+ mapping = self.gen_slice_info(service)
+
+ fn = "/etc/ansible/requestrouter/dnsredir/hosts"
+ f = open(fn, "w")
+ for (k,v) in mapping.items():
+ f.write("%s\n" % k)
+
+ fn = "/etc/ansible/requestrouter/dnsdemux/hosts"
+ f = open(fn, "w")
+ for (k,v) in mapping.items():
+ f.write("%s\n" % k)
+
+
+ def get_servicemap_uid(self, servicemap):
+ seq = ("service_", str(servicemap.pk));
+ return "".join(seq)
+
+ def get_service_port(self, servicemap):
+ return str(9000+servicemap.pk)
+
+ def gen_dnsredir_serviceconf(self, servicemap):
+ objname = self.get_servicemap_uid(servicemap)
+
+ rr_mapping = self.gen_slice_info(None)
+
+ #generate dnsredir.conf file parameters to be used in static file.
+ mapping = {}
+ mapping["port_listen"] = self.get_service_port(servicemap)
+ mapping["configdir"] = rrlib_config.DNSREDIR_CONFIGDIR_PREFIX+objname+".d/"
+ mapping["logdir"] = rrlib_config.DNSREDIR_LOGDIR_PREFIX+objname+".d"
+ mapping["pidfile"] = rrlib_config.DNSREDIR_PIDFILE_PREFIX+objname+".pid"
+ mapping["domain_name"] = servicemap.prefix
+ mapping["heartbeat_port"] = rrlib_config.HEARTBEAT_PORT
+
+ #generate dnsredir.conf file
+
+ fn = "./temp_config/dnsredir/"+objname+".conf"
+ f = open(fn, "w")
+ for (k,v) in rr_mapping.items():
+ f.write(mapping["domain_name"]+". NS "+k+". "+v+" 3600 \n" % mapping)
+
+
+ f.write("""
+Default_TTL 30
+
+Port %(port_listen)s
+
+ConfigDir %(configdir)s
+
+MapsDir maps.d
+
+HTTPPort %(heartbeat_port)d
+
+PidFile %(pidfile)s
+
+HttpRequestPort 8081
+
+""" % mapping)
+
+ #generate configdirectory
+
+ os.mkdir("./temp_config/dnsredir/"+objname+".d")
+
+ #geenrate codeen_nodes.conf
+ mapping_ip, mapping_zone = self.gen_servicemap_slice_info(servicemap)
+
+ codeen_name = "./temp_config/dnsredir/"+objname+".d/codeen_nodes.conf"
+ f = open(codeen_name, "w")
+ for (k,v) in mapping_zone.items():
+ f.write(k+" zone="+v+" \n")
+
+ iptxt = "./temp_config/dnsredir/"+objname+".d/node-to-ip.txt"
+ f = open(iptxt, "w")
+ for (k,v) in mapping_ip.items():
+ f.write(k+" "+v+" \n")
+
+ #generate maps directory
+ os.mkdir("./temp_config/dnsredir/"+objname+".d/maps.d")
+
+ # redirection map
+ map = "./temp_config/dnsredir/"+objname+".d/maps.d/map.conf"
+ f = open(map, "w")
+ #hardcoded probable public IP masks from arizona and princeton region respectively
+ f.write("prefix "+servicemap.prefix+" \n")
+ f.write("map 150.135.211.252/32 zone 1 || zone 2 \n")
+ f.write("map 128.112.171.112/24 zone 2 || zone 1 \n")
+ f.write("map 0.0.0.0/0 zone 1 || zone 2 \n")
+
+
+ def gen_dnsdemux_serviceconf(self, servicemap):
+ '''
+ generates frontend service*.conf file for each of the service
+ It assumes that there is a dnsdemux frontend running on the RR istallation and will
+ just add a conf file for each service in /etc/dnsdemux/default
+ '''
+ objname = self.get_servicemap_uid(servicemap)
+ #generate dnsdemux.conf file parameters to be used in static file.
+
+ port_listen = self.get_service_port(servicemap)
+ domain_name = servicemap.prefix
+ #generate service specific .conf file
+
+ rr_mapping = self.gen_slice_info(None)
+
+ fn = "./temp_config/dnsdemux/"+objname+".conf"
+ f = open(fn, "w")
+
+ for (k,v) in rr_mapping.items():
+ f.write("Forward "+v+" "+port_listen+" 8081 "+domain_name+".\n")
+
+
+ def teardown_temp_configfiles(self, objname):
+ if os.path.exists("./temp_config/dnsdemux/"+objname+".conf"):
+ os.remove("./temp_config/dnsdemux/"+objname+".conf")
+ if os.path.exists("./temp_config/dnsredir/"+objname+".d/maps.d/map.conf"):
+ os.remove("./temp_config/dnsredir/"+objname+".d/maps.d/map.conf")
+ if os.path.exists("./temp_config/dnsredir/"+objname+".d/maps.d"):
+ os.rmdir("./temp_config/dnsredir/"+objname+".d/maps.d")
+ if os.path.exists("./temp_config/dnsredir/"+objname+".d/node-to-ip.txt"):
+ os.remove("./temp_config/dnsredir/"+objname+".d/node-to-ip.txt")
+ if os.path.exists("./temp_config/dnsredir/"+objname+".d/codeen_nodes.conf"):
+ os.remove("./temp_config/dnsredir/"+objname+".d/codeen_nodes.conf")
+ if os.path.exists("./temp_config/dnsredir/"+objname+".d"):
+ os.rmdir("./temp_config/dnsredir/"+objname+".d")
+ if os.path.exists("./temp_config/dnsredir/"+objname+".conf"):
+ os.remove("./temp_config/dnsredir/"+objname+".conf")
+
diff --git a/xos/observers/requestrouter/rrlib_config.py b/xos/observers/requestrouter/rrlib_config.py
new file mode 100644
index 0000000..d0c00c2
--- /dev/null
+++ b/xos/observers/requestrouter/rrlib_config.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+
+DNSREDIR_CONFIGDIR_PREFIX = "/etc/dnsredir/"
+DNSREDIR_LOGDIR_PREFIX = "/var/log/dnsredir/"
+DNSREDIR_PIDFILE_PREFIX = "/var/run/dnsredir."
+REDIR_USER = "princeton_coredirect"
+#REDIR_USER = "arizona_tools1"
+#DEMUX_USER = "arizona_tools1"
+DEMUX_USER = "princeton_codnsdemux"
+HEARTBEAT_PORT = 9000
+#not required as of now, as there will be only one frontend dnsdemux for all the services
+#DNSDEMUX_CONFIGDIR_PREFIX = "/etc/dnsdemux/"
+#DNSDEMUX_LOGDIR_PREFIX = "/var/log/dnsdemux/"
+#DNSDEMUX_PIDFILE_PREFIX = "/var/run/dnsdemux."
+
+TBD = "TBD"
diff --git a/xos/observers/requestrouter/steps/sync_requestrouterservices.py b/xos/observers/requestrouter/steps/sync_requestrouterservices.py
new file mode 100644
index 0000000..9af3584
--- /dev/null
+++ b/xos/observers/requestrouter/steps/sync_requestrouterservices.py
@@ -0,0 +1,46 @@
+import os
+import sys
+import base64
+import traceback
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.requestrouter.models import RequestRouterService
+from util.logger import Logger, logging
+
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from rrlib import RequestRouterLibrary
+
+logger = Logger(level=logging.INFO)
+
+class SyncRequestRouterService(SyncStep, RequestRouterLibrary):
+ provides=[RequestRouterService]
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+ RequestRouterLibrary.__init__(self)
+
+ def fetch_pending(self):
+ try:
+ ret = RequestRouterService.objects.filter(Q(enacted__lt=F('updated')) | Q(enacted=None))
+ return ret
+ except Exception, e:
+ traceback.print_exc()
+ return None
+
+ def sync_record(self, rr_service):
+ try:
+ print "syncing service!"
+ logger.info("sync'ing rr_service %s" % str(rr_service))
+ self.gen_slice_file(rr_service)
+ rr_service.save()
+ return True
+ except Exception, e:
+ traceback.print_exc()
+ return False
+
+
diff --git a/xos/observers/requestrouter/steps/sync_servicemap.py b/xos/observers/requestrouter/steps/sync_servicemap.py
new file mode 100644
index 0000000..18bcec4
--- /dev/null
+++ b/xos/observers/requestrouter/steps/sync_servicemap.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import base64
+import traceback
+from django.db.models import F, Q
+from xos.config import Config, XOS_DIR
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.requestrouter.models import ServiceMap
+from util.logger import Logger, logging
+
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from rrlib import RequestRouterLibrary
+from configurationPush import ConfigurationPush
+import rrlib_config
+
+logger = Logger(level=logging.INFO)
+
+class SyncServiceMap(SyncStep, RequestRouterLibrary, ConfigurationPush):
+ provides=[ServiceMap]
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+ RequestRouterLibrary.__init__(self)
+ ConfigurationPush.__init__(self)
+
+ def fetch_pending(self):
+ try:
+ ret = ServiceMap.objects.filter(Q(enacted__lt=F('updated')) | Q(enacted=None))
+ return ret
+ except Exception, e:
+ traceback.print_exc()
+ return None
+
+ def sync_record(self, servicemap):
+ try:
+ print "sync! %s " % self.get_servicemap_uid(servicemap)
+ self.gen_dnsredir_serviceconf(servicemap)
+ self.gen_dnsdemux_serviceconf(servicemap)
+ # push generated files from temp_config
+ service_uid = self.get_servicemap_uid(servicemap)
+ self.config_push(service_uid, rrlib_config.REDIR_USER, XOS_DIR + "/observers/requestrouter/playbook/site_redir.yml", "/etc/ansible/requestrouter/dnsredir/hosts")
+ self.config_push(service_uid, rrlib_config.DEMUX_USER, XOS_DIR + "/observers/requestrouter/playbook/site_demux.yml", "/etc/ansible/requestrouter/dnsdemux/hosts")
+ self.teardown_temp_configfiles(service_uid)
+ except Exception, e:
+ traceback.print_exc()
+ return False
+
+if __name__ == "__main__":
+ sv = SyncServiceMap()
+
+ recs = sv.fetch_pending()
+
+ for rec in recs:
+ sv.sync_record( rec )
diff --git a/xos/observers/syndicate/__init__.py b/xos/observers/syndicate/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/observers/syndicate/__init__.py
diff --git a/xos/observers/syndicate/model-deps b/xos/observers/syndicate/model-deps
new file mode 100644
index 0000000..b15c5d1
--- /dev/null
+++ b/xos/observers/syndicate/model-deps
@@ -0,0 +1,16 @@
+{
+ "SlicePrivilege": [
+ "User",
+ "Slice"
+ ],
+ "Slice": [
+ "Site",
+ "Service"
+ ],
+ "VolumeAccessRight": [
+ "Volume"
+ ],
+ "User": [
+ "Site"
+ ]
+}
diff --git a/xos/observers/syndicate/requirements.py b/xos/observers/syndicate/requirements.py
new file mode 100644
index 0000000..303fd3d
--- /dev/null
+++ b/xos/observers/syndicate/requirements.py
@@ -0,0 +1,5 @@
+requests
+gevent
+grequests
+setproctitle
+psutil
diff --git a/xos/observers/syndicate/run.sh b/xos/observers/syndicate/run.sh
new file mode 100644
index 0000000..82960a9
--- /dev/null
+++ b/xos/observers/syndicate/run.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+python syndicate-backend.py -C $XOS_DIR/observers/syndicate/syndicate_observer_config
diff --git a/xos/observers/syndicate/start.sh b/xos/observers/syndicate/start.sh
new file mode 100644
index 0000000..1c408a1
--- /dev/null
+++ b/xos/observers/syndicate/start.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+nohup python syndicate-backend.py -C $XOS_DIR/observers/syndicate/syndicate_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/syndicate/steps/sync_volume.py b/xos/observers/syndicate/steps/sync_volume.py
new file mode 100644
index 0000000..e6dc90b
--- /dev/null
+++ b/xos/observers/syndicate/steps/sync_volume.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import traceback
+import base64
+
+if __name__ == "__main__":
+ # for testing
+ if os.getenv("OPENCLOUD_PYTHONPATH"):
+ sys.path.append( os.getenv("OPENCLOUD_PYTHONPATH") )
+ else:
+ print >> sys.stderr, "No OPENCLOUD_PYTHONPATH variable set. Assuming that OpenCloud is in PYTHONPATH"
+
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from services.syndicate_storage.models import Volume
+
+import logging
+from logging import Logger
+logging.basicConfig( format='[%(levelname)s] [%(module)s:%(lineno)d] %(message)s' )
+logger = logging.getLogger()
+logger.setLevel( logging.INFO )
+
+# point to planetstack
+if __name__ != "__main__":
+ if os.getenv("OPENCLOUD_PYTHONPATH") is not None:
+ sys.path.insert(0, os.getenv("OPENCLOUD_PYTHONPATH"))
+ else:
+ logger.warning("No OPENCLOUD_PYTHONPATH set; assuming your PYTHONPATH works")
+
+# syndicatelib will be in stes/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+import syndicatelib
+
+
+class SyncVolume(SyncStep):
+ provides=[Volume]
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+
+ def sync_record(self, volume):
+ """
+ Synchronize a Volume record with Syndicate.
+ """
+
+ logger.info( "Sync Volume = %s\n\n" % volume.name )
+
+ user_email = volume.owner_id.email
+ config = syndicatelib.get_config()
+
+ volume_principal_id = syndicatelib.make_volume_principal_id( user_email, volume.name )
+
+ # get the observer secret
+ try:
+ observer_secret = config.SYNDICATE_OPENCLOUD_SECRET
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("config is missing SYNDICATE_OPENCLOUD_SECRET")
+ raise e
+
+ # volume owner must exist as a Syndicate user...
+ try:
+ rc, user = syndicatelib.ensure_principal_exists( volume_principal_id, observer_secret, is_admin=False, max_UGs=1100, max_RGs=1)
+ assert rc == True, "Failed to create or read volume principal '%s'" % volume_principal_id
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("Failed to ensure principal '%s' exists" % volume_principal_id )
+ raise e
+
+ # volume must exist
+
+ # create or update the Volume
+ try:
+ new_volume = syndicatelib.ensure_volume_exists( volume_principal_id, volume, user=user )
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("Failed to ensure volume '%s' exists" % volume.name )
+ raise e
+
+ # did we create the Volume?
+ if new_volume is not None:
+ # we're good
+ pass
+
+ # otherwise, just update it
+ else:
+ try:
+ rc = syndicatelib.update_volume( volume )
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("Failed to update volume '%s', exception = %s" % (volume.name, e.message))
+ raise e
+
+ return True
+
+ def delete_record(self, volume):
+ try:
+ volume_name = volume.name
+ syndicatelib.ensure_volume_absent( volume_name )
+ except Exception, e:
+ traceback.print_exc()
+ logger.exception("Failed to erase volume '%s'" % volume_name)
+ raise e
+
+
+
+
+
+if __name__ == "__main__":
+ sv = SyncVolume()
+
+
+ # first, set all volumes to not-enacted so we can test
+ for v in Volume.objects.all():
+ v.enacted = None
+ v.save()
+
+ # NOTE: for resetting only
+ if len(sys.argv) > 1 and sys.argv[1] == "reset":
+ sys.exit(0)
+
+ recs = sv.fetch_pending()
+
+ for rec in recs:
+ rc = sv.sync_record( rec )
+ if not rc:
+ print "\n\nFailed to sync %s\n\n" % (rec.name)
+
diff --git a/xos/observers/syndicate/steps/sync_volumeaccessright.py b/xos/observers/syndicate/steps/sync_volumeaccessright.py
new file mode 100644
index 0000000..2889502
--- /dev/null
+++ b/xos/observers/syndicate/steps/sync_volumeaccessright.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import base64
+import traceback
+
+if __name__ == "__main__":
+ # for testing
+ if os.getenv("OPENCLOUD_PYTHONPATH"):
+ sys.path.append( os.getenv("OPENCLOUD_PYTHONPATH") )
+ else:
+ print >> sys.stderr, "No OPENCLOUD_PYTHONPATH variable set. Assuming that OpenCloud is in PYTHONPATH"
+
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+
+import logging
+from logging import Logger
+logging.basicConfig( format='[%(levelname)s] [%(module)s:%(lineno)d] %(message)s' )
+logger = logging.getLogger()
+logger.setLevel( logging.INFO )
+
+# point to planetstack
+if __name__ != "__main__":
+ if os.getenv("OPENCLOUD_PYTHONPATH") is not None:
+ sys.path.insert(0, os.getenv("OPENCLOUD_PYTHONPATH"))
+ else:
+ logger.warning("No OPENCLOUD_PYTHONPATH set; assuming your PYTHONPATH works")
+
+from services.syndicate_storage.models import VolumeAccessRight
+
+# syndicatelib will be in stes/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+import syndicatelib
+
+class SyncVolumeAccessRight(SyncStep):
+ provides=[VolumeAccessRight]
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+
+ def sync_record(self, vac):
+
+ syndicate_caps = "UNKNOWN" # for exception handling
+
+ # get arguments
+ config = syndicatelib.get_config()
+ user_email = vac.owner_id.email
+ volume_name = vac.volume.name
+ syndicate_caps = syndicatelib.opencloud_caps_to_syndicate_caps( vac.cap_read_data, vac.cap_write_data, vac.cap_host_data )
+
+ logger.info( "Sync VolumeAccessRight for (%s, %s)" % (user_email, volume_name) )
+
+ # validate config
+ try:
+ RG_port = config.SYNDICATE_RG_DEFAULT_PORT
+ observer_secret = config.SYNDICATE_OPENCLOUD_SECRET
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("syndicatelib config is missing SYNDICATE_RG_DEFAULT_PORT, SYNDICATE_OPENCLOUD_SECRET")
+ raise e
+
+ # ensure the user exists and has credentials
+ try:
+ rc, user = syndicatelib.ensure_principal_exists( user_email, observer_secret, is_admin=False, max_UGs=1100, max_RGs=1 )
+ assert rc is True, "Failed to ensure principal %s exists (rc = %s,%s)" % (user_email, rc, user)
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("Failed to ensure user '%s' exists" % user_email )
+ raise e
+
+ # make the access right for the user to create their own UGs, and provision an RG for this user that will listen on localhost.
+ # the user will have to supply their own RG closure.
+ try:
+ rc = syndicatelib.setup_volume_access( user_email, volume_name, syndicate_caps, RG_port, observer_secret )
+ assert rc is True, "Failed to setup volume access for %s in %s" % (user_email, volume_name)
+
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("Faoed to ensure user %s can access Volume %s with rights %s" % (user_email, volume_name, syndicate_caps))
+ raise e
+
+ return True
+
+ # Jude: this will simply go on to purge the object from
+ # OpenCloud. The previous 'deleter' version was a no-op also.
+ def delete_record(self, obj):
+ pass
+
+
+if __name__ == "__main__":
+
+ # first, set all VolumeAccessRights to not-enacted so we can test
+ for v in VolumeAccessRight.objects.all():
+ v.enacted = None
+ v.save()
+
+ # NOTE: for resetting only
+ if len(sys.argv) > 1 and sys.argv[1] == "reset":
+ sys.exit(0)
+
+
+ sv = SyncVolumeAccessRight()
+ recs = sv.fetch_pending()
+
+ for rec in recs:
+ sv.sync_record( rec )
+
diff --git a/xos/observers/syndicate/steps/sync_volumeslice.py b/xos/observers/syndicate/steps/sync_volumeslice.py
new file mode 100644
index 0000000..1be61b9
--- /dev/null
+++ b/xos/observers/syndicate/steps/sync_volumeslice.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import base64
+import traceback
+
+if __name__ == "__main__":
+ # for testing
+ if os.getenv("OPENCLOUD_PYTHONPATH"):
+ sys.path.append( os.getenv("OPENCLOUD_PYTHONPATH") )
+ else:
+ print >> sys.stderr, "No OPENCLOUD_PYTHONPATH variable set. Assuming that OpenCloud is in PYTHONPATH"
+
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service, Slice
+
+import logging
+from logging import Logger
+logging.basicConfig( format='[%(levelname)s] [%(module)s:%(lineno)d] %(message)s' )
+logger = logging.getLogger()
+logger.setLevel( logging.INFO )
+
+# point to planetstack
+if __name__ != "__main__":
+ if os.getenv("OPENCLOUD_PYTHONPATH") is not None:
+ sys.path.insert(0, os.getenv("OPENCLOUD_PYTHONPATH"))
+ else:
+ logger.warning("No OPENCLOUD_PYTHONPATH set; assuming your PYTHONPATH works")
+
+from services.syndicate_storage.models import VolumeSlice,VolumeAccessRight,Volume
+
+# syndicatelib will be in stes/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+import syndicatelib
+
+
+class SyncVolumeSlice(SyncStep):
+ provides=[VolumeSlice]
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+
+ def sync_record(self, vs):
+
+ logger.info("Sync VolumeSlice for (%s, %s)" % (vs.volume_id.name, vs.slice_id.name))
+
+ # extract arguments...
+ user_email = vs.slice_id.creator.email
+ slice_name = vs.slice_id.name
+ volume_name = vs.volume_id.name
+ syndicate_caps = syndicatelib.opencloud_caps_to_syndicate_caps( vs.cap_read_data, vs.cap_write_data, vs.cap_host_data )
+ RG_port = vs.RG_portnum
+ UG_port = vs.UG_portnum
+ slice_secret = None
+
+ config = syndicatelib.get_config()
+ try:
+ observer_secret = config.SYNDICATE_OPENCLOUD_SECRET
+ RG_closure = config.SYNDICATE_RG_CLOSURE
+ observer_pkey_path = config.SYNDICATE_PRIVATE_KEY
+ syndicate_url = config.SYNDICATE_SMI_URL
+
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("syndicatelib config is missing one or more of the following: SYNDICATE_OPENCLOUD_SECRET, SYNDICATE_RG_CLOSURE, SYNDICATE_PRIVATE_KEY, SYNDICATE_SMI_URL")
+ raise e
+
+ # get secrets...
+ try:
+ observer_pkey_pem = syndicatelib.get_private_key_pem( observer_pkey_path )
+ assert observer_pkey_pem is not None, "Failed to load Observer private key"
+
+ # get/create the slice secret
+ slice_secret = syndicatelib.get_or_create_slice_secret( observer_pkey_pem, slice_name )
+ assert slice_secret is not None, "Failed to get or create slice secret for %s" % slice_name
+
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("Failed to load secret credentials")
+ raise e
+
+ # make sure there's a slice-controlled Syndicate user account for the slice owner
+ slice_principal_id = syndicatelib.make_slice_principal_id( user_email, slice_name )
+
+ try:
+ rc, user = syndicatelib.ensure_principal_exists( slice_principal_id, observer_secret, is_admin=False, max_UGs=1100, max_RGs=1 )
+ assert rc is True, "Failed to ensure principal %s exists (rc = %s,%s)" % (slice_principal_id, rc, user)
+ except Exception, e:
+ traceback.print_exc()
+ logger.error('Failed to ensure slice user %s exists' % slice_principal_id)
+ raise e
+
+ # grant the slice-owning user the ability to provision UGs in this Volume, and also provision for the user the (single) RG the slice will instantiate in each VM.
+ try:
+ rc = syndicatelib.setup_volume_access( slice_principal_id, volume_name, syndicate_caps, RG_port, observer_secret, RG_closure=RG_closure )
+ assert rc is True, "Failed to set up Volume access for slice %s in %s" % (slice_principal_id, volume_name)
+
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("Failed to set up Volume access for slice %s in %s" % (slice_principal_id, volume_name))
+ raise e
+
+ # generate and save slice credentials....
+ try:
+ slice_cred = syndicatelib.save_slice_credentials( observer_pkey_pem, syndicate_url, slice_principal_id, volume_name, slice_name, observer_secret, slice_secret, UG_port, existing_user=user )
+ assert slice_cred is not None, "Failed to generate slice credential for %s in %s" % (slice_principal_id, volume_name )
+
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("Failed to generate slice credential for %s in %s" % (slice_principal_id, volume_name))
+ raise e
+
+ # ... and push them all out.
+ try:
+ rc = syndicatelib.push_credentials_to_slice( slice_name, slice_cred )
+ assert rc is True, "Failed to push credentials to slice %s for volume %s" % (slice_name, volume_name)
+
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("Failed to push slice credentials to %s for volume %s" % (slice_name, volume_name))
+ raise e
+
+ return True
+
+ # This method will simply cause the object to be purged from OpenCloud
+ def delete_record(self, volume_slice):
+ pass
+
+
+if __name__ == "__main__":
+ sv = SyncVolumeSlice()
+
+ # first, set all VolumeSlice to not-enacted so we can test
+ for v in VolumeSlice.objects.all():
+ v.enacted = None
+ v.save()
+
+ # NOTE: for resetting only
+ if len(sys.argv) > 1 and sys.argv[1] == "reset":
+ sys.exit(0)
+
+ recs = sv.fetch_pending()
+
+ for rec in recs:
+ if rec.slice_id.creator is None:
+ print "Ignoring slice %s, since it has no creator" % (rec.slice_id)
+ continue
+
+ sv.sync_record( rec )
+
diff --git a/xos/observers/syndicate/stop.sh b/xos/observers/syndicate/stop.sh
new file mode 100644
index 0000000..f4a8e28
--- /dev/null
+++ b/xos/observers/syndicate/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f syndicate-backend.py
diff --git a/xos/observers/syndicate/syndicate-backend.py b/xos/observers/syndicate/syndicate-backend.py
new file mode 100644
index 0000000..9b53c77
--- /dev/null
+++ b/xos/observers/syndicate/syndicate-backend.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+from synchronizers.base.backend import Backend
+
+if __name__ == '__main__':
+
+ backend = Backend()
+ backend.run()
+
diff --git a/xos/observers/syndicate/syndicate_observer_config b/xos/observers/syndicate/syndicate_observer_config
new file mode 100644
index 0000000..7e6d78a
--- /dev/null
+++ b/xos/observers/syndicate/syndicate_observer_config
@@ -0,0 +1,35 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+#user=plstackuser
+#password=2uMDYtJK
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+dependency_graph=/opt/xos/observers/syndicate/model-deps
+steps_dir=/opt/xos/observers/syndicate/steps
+deleters_dir=/opt/xos/observers/syndicate/deleters
+log_file=console
+driver=None
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/syndicate/syndicatelib.py b/xos/observers/syndicate/syndicatelib.py
new file mode 100644
index 0000000..56bd120
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib.py
@@ -0,0 +1,1353 @@
+#!/usr/bin/env python
+
+"""
+Define some common methods for the Syndicate observer.
+"""
+import os
+import sys
+import random
+import json
+import time
+import requests
+import traceback
+import base64
+import BaseHTTPServer
+import setproctitle
+import threading
+import urllib
+
+from Crypto.Hash import SHA256 as HashAlg
+from Crypto.PublicKey import RSA as CryptoKey
+from Crypto import Random
+from Crypto.Signature import PKCS1_PSS as CryptoSigner
+
+import logging
+from logging import Logger
+logging.basicConfig( format='[%(levelname)s] [%(module)s:%(lineno)d] %(message)s' )
+logger = logging.getLogger()
+logger.setLevel( logging.INFO )
+
+# get config package
+import syndicatelib_config.config as CONFIG
+
+# get the Syndicate modules
+import syndicate
+
+import syndicate.client.bin.syntool as syntool
+import syndicate.client.common.msconfig as msconfig
+import syndicate.client.common.api as api
+import syndicate.util.storage as syndicate_storage
+import syndicate.util.watchdog as syndicate_watchdog
+import syndicate.util.daemonize as syndicate_daemon
+import syndicate.util.crypto as syndicate_crypto
+import syndicate.util.provisioning as syndicate_provisioning
+import syndicate.syndicate as c_syndicate
+
+# for testing
+TESTING = False
+class FakeObject(object):
+ def __init__(self):
+ pass
+
+if os.getenv("OPENCLOUD_PYTHONPATH") is not None:
+ sys.path.insert(0, os.getenv("OPENCLOUD_PYTHONPATH"))
+else:
+ logger.warning("No OPENCLOUD_PYTHONPATH set. Assuming Syndicate models are in your PYTHONPATH")
+
+try:
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+ # get our models
+ import services.syndicate_storage.models as models
+
+ # get OpenCloud models
+ from core.models import Slice,Instance
+
+ from django.core.exceptions import ObjectDoesNotExist
+ from django.db import IntegrityError
+
+except ImportError, ie:
+ logger.warning("Failed to import models; some tests will not work")
+
+ # create a fake "models" package that has exactly the members we need for testing.
+ models = FakeObject()
+ models.Volume = FakeObject()
+ models.Volume.CAP_READ_DATA = 1
+ models.Volume.CAP_WRITE_DATA = 2
+ models.Volume.CAP_HOST_DATA = 4
+
+ TESTING = True
+
+
+#-------------------------------
+class SyndicateObserverError( Exception ):
+ pass
+
+#-------------------------------
+def get_config():
+ """
+ Return the imported config
+ """
+ return CONFIG
+
+
+#-------------------------------
+def make_openid_url( email ):
+ """
+ Generate an OpenID identity URL from an email address.
+ """
+ return os.path.join( CONFIG.SYNDICATE_OPENID_TRUSTROOT, "id", email )
+
+
+#-------------------------------
+def connect_syndicate( username=CONFIG.SYNDICATE_OPENCLOUD_USER, password=CONFIG.SYNDICATE_OPENCLOUD_PASSWORD, user_pkey_pem=CONFIG.SYNDICATE_OPENCLOUD_PKEY ):
+ """
+ Connect to the OpenCloud Syndicate SMI, using the OpenCloud user credentials.
+ """
+ debug = True
+ if hasattr(CONFIG, "DEBUG"):
+ debug = CONFIG.DEBUG
+
+ client = syntool.Client( username, CONFIG.SYNDICATE_SMI_URL,
+ password=password,
+ user_pkey_pem=user_pkey_pem,
+ debug=debug )
+
+ return client
+
+
+#-------------------------------
+def opencloud_caps_to_syndicate_caps( cap_read, cap_write, cap_host ):
+ """
+ Convert OpenCloud capability bits from the UI into Syndicate's capability bits.
+ """
+ syn_caps = 0
+
+ if cap_read:
+ syn_caps |= (msconfig.GATEWAY_CAP_READ_DATA | msconfig.GATEWAY_CAP_READ_METADATA)
+ if cap_write:
+ syn_caps |= (msconfig.GATEWAY_CAP_WRITE_DATA | msconfig.GATEWAY_CAP_WRITE_METADATA)
+ if cap_host:
+ syn_caps |= (msconfig.GATEWAY_CAP_COORDINATE)
+
+ return syn_caps
+
+#-------------------------------
+def ensure_user_exists( user_email, **user_kw ):
+ """
+ Given an OpenCloud user, ensure that the corresponding
+ Syndicate user exists on the MS. This method does NOT
+ create any OpenCloud-specific data.
+
+ Return the (created, user), where created==True if the user
+ was created and created==False if the user was read.
+ Raise an exception on error.
+ """
+
+ client = connect_syndicate()
+ user_openid_url = make_openid_url( user_email )
+
+ return syndicate_provisioning.ensure_user_exists( client, user_email, user_openid_url, **user_kw )
+
+
+#-------------------------------
+def ensure_user_absent( user_email ):
+ """
+ Ensure that a given OpenCloud user's associated Syndicate user record
+ has been deleted. This method does NOT delete any OpenCloud-specific data.
+
+ Returns True on success
+ Raises an exception on error
+ """
+
+ client = connect_syndicate()
+
+ return client.delete_user( user_email )
+
+
+#-------------------------------
+def make_volume_principal_id( user_email, volume_name ):
+ """
+ Create a principal id for a Volume owner.
+ """
+
+ volume_name_safe = urllib.quote( volume_name )
+
+ return "volume_%s.%s" % (volume_name_safe, user_email)
+
+
+#-------------------------------
+def make_slice_principal_id( user_email, slice_name ):
+ """
+ Create a principal id for a slice owner.
+ """
+
+ slice_name_safe = urllib.quote( slice_name )
+
+ return "slice_%s.%s" % (slice_name, user_email)
+
+
+#-------------------------------
+def ensure_principal_exists( user_email, observer_secret, **user_kw ):
+ """
+ Ensure that a Syndicate user exists, as well as its OpenCloud-specific data.
+
+ Return (True, (None OR user)) on success. Returns a user if the user was created.
+ Return (False, None) on error
+ """
+
+ try:
+ created, new_user = ensure_user_exists( user_email, **user_kw )
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("Failed to ensure user '%s' exists" % user_email )
+ return (False, None)
+
+ # if we created a new user, then save its (sealed) credentials to the Django DB
+ if created:
+ try:
+ rc = put_principal_data( user_email, observer_secret, new_user['signing_public_key'], new_user['signing_private_key'] )
+ assert rc == True, "Failed to save SyndicatePrincipal"
+ except Exception, e:
+ traceback.print_exc()
+ logger.error("Failed to save private key for principal %s" % (user_email))
+ return (False, None)
+
+ return (True, new_user)
+
+
+
+#-------------------------------
+def ensure_principal_absent( user_email ):
+ """
+ Ensure that a Syndicate user does not exists, and remove the OpenCloud-specific data.
+
+ Return True on success.
+ """
+
+ ensure_user_absent( user_email )
+ delete_principal_data( user_email )
+ return True
+
+#-------------------------------
+def ensure_volume_exists( user_email, opencloud_volume, user=None ):
+ """
+ Given the email address of a user, ensure that the given
+ Volume exists and is owned by that user.
+ Do not try to ensure that the user exists.
+
+ Return the Volume if we created it, or return None if we did not.
+ Raise an exception on error.
+ """
+ client = connect_syndicate()
+
+ try:
+ volume = client.read_volume( opencloud_volume.name )
+ except Exception, e:
+ # transport error
+ logger.exception(e)
+ raise e
+
+ if volume is None:
+ # the volume does not exist....try to create it
+ vol_name = opencloud_volume.name
+ vol_blocksize = opencloud_volume.blocksize
+ vol_description = opencloud_volume.description
+ vol_private = opencloud_volume.private
+ vol_archive = opencloud_volume.archive
+ vol_default_gateway_caps = opencloud_caps_to_syndicate_caps( opencloud_volume.cap_read_data, opencloud_volume.cap_write_data, opencloud_volume.cap_host_data )
+
+ try:
+ vol_info = client.create_volume( user_email, vol_name, vol_description, vol_blocksize,
+ private=vol_private,
+ archive=vol_archive,
+ active=True,
+ default_gateway_caps=vol_default_gateway_caps,
+ store_private_key=False,
+ metadata_private_key="MAKE_METADATA_KEY" )
+
+ except Exception, e:
+ # transport error
+ logger.exception(e)
+ raise e
+
+ else:
+ # successfully created the volume!
+ return vol_info
+
+ else:
+
+ # volume already exists. Verify its owned by this user.
+ if user is None:
+ try:
+ user = client.read_user( volume['owner_id'] )
+ except Exception, e:
+ # transport error, or user doesn't exist (either is unacceptable)
+ logger.exception(e)
+ raise e
+
+ if user is None or user['email'] != user_email:
+ raise Exception("Volume '%s' already exists, but is NOT owned by '%s'" % (opencloud_volume.name, user_email) )
+
+ # we're good!
+ return None
+
+
+#-------------------------------
+def ensure_volume_absent( volume_name ):
+ """
+ Given an OpenCloud volume, ensure that the corresponding Syndicate
+ Volume does not exist.
+ """
+
+ client = connect_syndicate()
+
+ # this is idempotent, and returns True even if the Volume doesn't exist
+ return client.delete_volume( volume_name )
+
+
+#-------------------------------
+def update_volume( opencloud_volume ):
+ """
+ Update a Syndicate Volume from an OpenCloud Volume model.
+ Fails if the Volume does not exist in Syndicate.
+ """
+
+ client = connect_syndicate()
+
+ vol_name = opencloud_volume.name
+ vol_description = opencloud_volume.description
+ vol_private = opencloud_volume.private
+ vol_archive = opencloud_volume.archive
+ vol_default_gateway_caps = opencloud_caps_to_syndicate_caps( opencloud_volume.cap_read_data, opencloud_volume.cap_write_data, opencloud_volume.cap_host_data )
+
+ try:
+ rc = client.update_volume( vol_name,
+ description=vol_description,
+ private=vol_private,
+ archive=vol_archive,
+ default_gateway_caps=vol_default_gateway_caps )
+
+ if not rc:
+ raise Exception("update_volume(%s) failed!" % vol_name )
+
+ except Exception, e:
+ # transort or method error
+ logger.exception(e)
+ return False
+
+ else:
+ return True
+
+
+#-------------------------------
+def ensure_volume_access_right_exists( user_email, volume_name, caps, allowed_gateways=[msconfig.GATEWAY_TYPE_UG] ):
+ """
+ Ensure that a particular user has particular access to a particular volume.
+ Do not try to ensure that the user or volume exist, however!
+ """
+ client = connect_syndicate()
+ return syndicate_provisioning.ensure_volume_access_right_exists( client, user_email, volume_name, caps, allowed_gateways )
+
+#-------------------------------
+def ensure_volume_access_right_absent( user_email, volume_name ):
+ """
+ Ensure that acess to a particular volume is revoked.
+ """
+ client = connect_syndicate()
+ return syndicate_provisioning.ensure_volume_access_right_absent( client, user_email, volume_name )
+
+
+#-------------------------------
+def setup_volume_access( user_email, volume_name, caps, RG_port, slice_secret, RG_closure=None ):
+ """
+ Set up the Volume to allow the slice to provision UGs in it, and to fire up RGs.
+ * create the Volume Access Right for the user, so (s)he can create Gateways.
+ * provision a single Replica Gateway, serving on localhost.
+ """
+ client = connect_syndicate()
+
+ try:
+ rc = ensure_volume_access_right_exists( user_email, volume_name, caps )
+ assert rc is True, "Failed to create access right for %s in %s" % (user_email, volume_name)
+
+ except Exception, e:
+ logger.exception(e)
+ return False
+
+ RG_name = syndicate_provisioning.make_gateway_name( "OpenCloud", "RG", volume_name, "localhost" )
+ RG_key_password = syndicate_provisioning.make_gateway_private_key_password( RG_name, slice_secret )
+
+ try:
+ rc = syndicate_provisioning.ensure_RG_exists( client, user_email, volume_name, RG_name, "localhost", RG_port, RG_key_password, closure=RG_closure )
+ except Exception, e:
+ logger.exception(e)
+ return False
+
+ return True
+
+
+#-------------------------------
+def teardown_volume_access( user_email, volume_name ):
+ """
+ Revoke access to a Volume for a User.
+ * remove the user's Volume Access Right
+ * remove the use'rs gateways
+ """
+ client = connect_syndicate()
+
+ # block the user from creating more gateways, and delete the gateways
+ try:
+ rc = client.remove_user_from_volume( user_email, volume_name )
+ assert rc is True, "Failed to remove access right for %s in %s" % (user_email, volume_name)
+
+ except Exception, e:
+ logger.exception(e)
+ return False
+
+ return True
+
+
+#-------------------------------
+def create_sealed_and_signed_blob( private_key_pem, secret, data ):
+ """
+ Create a sealed and signed message.
+ """
+
+ # seal it with the password
+ logger.info("Sealing credential data")
+
+ rc, sealed_data = c_syndicate.password_seal( data, secret )
+ if rc != 0:
+ logger.error("Failed to seal data with the secret, rc = %s" % rc)
+ return None
+
+ msg = syndicate_crypto.sign_and_serialize_json( private_key_pem, sealed_data )
+ if msg is None:
+ logger.error("Failed to sign credential")
+ return None
+
+ return msg
+
+
+#-------------------------------
+def verify_and_unseal_blob( public_key_pem, secret, blob_data ):
+ """
+ verify and unseal a serialized string of JSON
+ """
+
+ # verify it
+ rc, sealed_data = syndicate_crypto.verify_and_parse_json( public_key_pem, blob_data )
+ if rc != 0:
+ logger.error("Failed to verify and parse blob, rc = %s" % rc)
+ return None
+
+ logger.info("Unsealing credential data")
+
+ rc, data = c_syndicate.password_unseal( sealed_data, secret )
+ if rc != 0:
+ logger.error("Failed to unseal blob, rc = %s" % rc )
+ return None
+
+ return data
+
+
+#-------------------------------
+def create_volume_list_blob( private_key_pem, slice_secret, volume_list ):
+ """
+ Create a sealed volume list, signed with the private key.
+ """
+ list_data = {
+ "volumes": volume_list
+ }
+
+ list_data_str = json.dumps( list_data )
+
+ msg = create_sealed_and_signed_blob( private_key_pem, slice_secret, list_data_str )
+ if msg is None:
+ logger.error("Failed to seal volume list")
+ return None
+
+ return msg
+
+
+#-------------------------------
+def create_slice_credential_blob( private_key_pem, slice_name, slice_secret, syndicate_url, volume_name, volume_owner, UG_port, user_pkey_pem ):
+ """
+ Create a sealed, signed, encoded slice credentials blob.
+ """
+
+ # create and serialize the data
+ cred_data = {
+ "syndicate_url": syndicate_url,
+ "volume_name": volume_name,
+ "volume_owner": volume_owner,
+ "slice_name": slice_name,
+ "slice_UG_port": UG_port,
+ "principal_pkey_pem": user_pkey_pem,
+ }
+
+ cred_data_str = json.dumps( cred_data )
+
+ msg = create_sealed_and_signed_blob( private_key_pem, slice_secret, cred_data_str )
+ if msg is None:
+ logger.error("Failed to seal volume list")
+ return None
+
+ return msg
+
+
+#-------------------------------
+def put_principal_data( user_email, observer_secret, public_key_pem, private_key_pem ):
+ """
+ Seal and store the principal's private key into the database, in a SyndicatePrincipal object,
+ so the instance-side Syndicate daemon syndicated.py can get them later.
+ Overwrite an existing principal if one exists.
+ """
+
+ sealed_private_key = create_sealed_and_signed_blob( private_key_pem, observer_secret, private_key_pem )
+ if sealed_private_key is None:
+ return False
+
+ try:
+ sp = models.SyndicatePrincipal( sealed_private_key=sealed_private_key, public_key_pem=public_key_pem, principal_id=user_email )
+ sp.save()
+ except IntegrityError, e:
+ logger.error("WARN: overwriting existing principal %s" % user_email)
+ sp.delete()
+ sp.save()
+
+ return True
+
+
+#-------------------------------
+def delete_principal_data( user_email ):
+ """
+ Delete an OpenCloud SyndicatePrincipal object.
+ """
+
+ sp = get_principal_data( user_email )
+ if sp is not None:
+ sp.delete()
+
+ return True
+
+
+#-------------------------------
+def get_principal_data( user_email ):
+ """
+ Get a SyndicatePrincipal record from the database
+ """
+
+ try:
+ sp = models.SyndicatePrincipal.objects.get( principal_id=user_email )
+ return sp
+ except ObjectDoesNotExist:
+ logger.error("No SyndicatePrincipal record for %s" % user_email)
+ return None
+
+
+
+#-------------------------------
+def get_principal_pkey( user_email, observer_secret ):
+ """
+ Fetch and unseal the private key of a SyndicatePrincipal.
+ """
+
+ sp = get_principal_data( user_email )
+ if sp is None:
+ logger.error("Failed to find private key for principal %s" % user_email )
+ return None
+
+ public_key_pem = sp.public_key_pem
+ sealed_private_key_pem = sp.sealed_private_key
+
+ # unseal
+ private_key_pem = verify_and_unseal_blob(public_key_pem, observer_secret, sealed_private_key_pem)
+ if private_key_pem is None:
+ logger.error("Failed to unseal private key")
+
+ return private_key_pem
+
+
+#-------------------------------
+def get_private_key_pem( pkey_path ):
+ """
+ Get a private key from storage, PEM-encoded.
+ """
+
+ # get the OpenCloud private key
+ observer_pkey = syndicate_storage.read_private_key( pkey_path )
+ if observer_pkey is None:
+ logger.error("Failed to load Observer private key")
+ return None
+
+ observer_pkey_pem = observer_pkey.exportKey()
+
+ return observer_pkey_pem
+
+
+#-------------------------------
+def encrypt_slice_secret( observer_pkey_pem, slice_secret ):
+ """
+ Encrypt and serialize the slice secret with the Observer private key
+ """
+
+ # get the public key
+ try:
+ observer_pubkey_pem = CryptoKey.importKey( observer_pkey_pem ).publickey().exportKey()
+ except Exception, e:
+ logger.exception(e)
+ logger.error("Failed to derive public key from private key")
+ return None
+
+ # encrypt the data
+ rc, sealed_slice_secret = c_syndicate.encrypt_data( observer_pkey_pem, observer_pubkey_pem, slice_secret )
+
+ if rc != 0:
+ logger.error("Failed to encrypt slice secret")
+ return None
+
+ sealed_slice_secret_b64 = base64.b64encode( sealed_slice_secret )
+
+ return sealed_slice_secret_b64
+
+
+#-------------------------------
+def decrypt_slice_secret( observer_pkey_pem, sealed_slice_secret_b64 ):
+ """
+ Unserialize and decrypt a slice secret
+ """
+
+ # get the public key
+ try:
+ observer_pubkey_pem = CryptoKey.importKey( observer_pkey_pem ).publickey().exportKey()
+ except Exception, e:
+ logger.exception(e)
+ logger.error("Failed to derive public key from private key")
+ return None
+
+ sealed_slice_secret = base64.b64decode( sealed_slice_secret_b64 )
+
+ # decrypt it
+ rc, slice_secret = c_syndicate.decrypt_data( observer_pubkey_pem, observer_pkey_pem, sealed_slice_secret )
+
+ if rc != 0:
+ logger.error("Failed to decrypt '%s', rc = %d" % (sealed_slice_secret_b64, rc))
+ return None
+
+ return slice_secret
+
+
+#--------------------------------
+def get_slice_secret( observer_pkey_pem, slice_name, slice_fk=None ):
+ """
+ Get the shared secret for a slice.
+ """
+
+ ss = None
+
+ # get the sealed slice secret from Django
+ try:
+ if slice_fk is not None:
+ ss = models.SliceSecret.objects.get( slice_id=slice_fk )
+ else:
+ ss = models.SliceSecret.objects.get( slice_id__name=slice_name )
+ except ObjectDoesNotExist, e:
+ logger.error("Failed to load slice secret for (%s, %s)" % (slice_fk, slice_name) )
+ return None
+
+ return ss.secret
+
+
+#-------------------------------
+def put_slice_secret( observer_pkey_pem, slice_name, slice_secret, slice_fk=None, opencloud_slice=None ):
+ """
+ Put the shared secret for a slice, encrypting it first.
+ """
+
+ ss = None
+
+ if opencloud_slice is None:
+ # look up the slice
+ try:
+ if slice_fk is None:
+ opencloud_slice = models.Slice.objects.get( name=slice_name )
+ else:
+ opencloud_slice = models.Slice.objects.get( id=slice_fk.id )
+ except Exception, e:
+ logger.exception(e)
+ logger.error("Failed to load slice (%s, %s)" % (slice_fk, slice_name) )
+ return False
+
+ ss = models.SliceSecret( slice_id=opencloud_slice, secret=slice_secret )
+
+ ss.save()
+
+ return True
+
+
+#-------------------------------
+def get_or_create_slice_secret( observer_pkey_pem, slice_name, slice_fk=None ):
+ """
+ Get a slice secret if it already exists, or generate a slice secret if one does not.
+ """
+
+ slice_secret = get_slice_secret( observer_pkey_pem, slice_name, slice_fk=slice_fk )
+ if slice_secret is None or len(slice_secret) == 0:
+
+ # generate a slice secret
+ slice_secret = "".join( random.sample("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 32) )
+
+ # store it
+ rc = put_slice_secret( observer_pkey_pem, slice_name, slice_secret, slice_fk=slice_fk )
+
+ if not rc:
+ raise SyndicateObserverError("Failed to create slice secret for (%s, %s)" % (slice_fk, slice_name))
+
+ return slice_secret
+
+
+#-------------------------------
+def generate_slice_credentials( observer_pkey_pem, syndicate_url, user_email, volume_name, slice_name, observer_secret, slice_secret, UG_port, existing_user=None ):
+ """
+ Generate and return the set of credentials to be sent off to the slice VMs.
+ exisitng_user is a Syndicate user, as a dictionary.
+
+ Return None on failure
+ """
+
+ # get the user's private key
+ logger.info("Obtaining private key for %s" % user_email)
+
+ # it might be in the existing_user...
+ user_pkey_pem = None
+ if existing_user is not None:
+ user_pkey_pem = existing_user.get('signing_private_key', None)
+
+ # no luck?
+ if user_pkey_pem is None:
+ try:
+ # get it from Django DB
+ user_pkey_pem = get_principal_pkey( user_email, observer_secret )
+ assert user_pkey_pem is not None, "No private key for %s" % user_email
+
+ except:
+ traceback.print_exc()
+ logger.error("Failed to get private key; cannot generate credentials for %s in %s" % (user_email, volume_name) )
+ return None
+
+ # generate a credetials blob
+ logger.info("Generating credentials for %s's slice" % (user_email))
+ try:
+ creds = create_slice_credential_blob( observer_pkey_pem, slice_name, slice_secret, syndicate_url, volume_name, user_email, UG_port, user_pkey_pem )
+ assert creds is not None, "Failed to create credentials for %s" % user_email
+
+ except:
+ traceback.print_exc()
+ logger.error("Failed to generate credentials for %s in %s" % (user_email, volume_name))
+ return None
+
+ return creds
+
+
+#-------------------------------
+def save_slice_credentials( observer_pkey_pem, syndicate_url, user_email, volume_name, slice_name, observer_secret, slice_secret, UG_port, existing_user=None ):
+ """
+ Create and save a credentials blob to a VolumeSlice.
+ Return the creds on success.
+ Return None on failure
+ """
+
+ creds = generate_slice_credentials( observer_pkey_pem, syndicate_url, user_email, volume_name, slice_name, observer_secret, slice_secret, UG_port, existing_user=existing_user )
+ ret = None
+
+ if creds is not None:
+ # save it
+ vs = get_volumeslice( volume_name, slice_name )
+
+ if vs is not None:
+ vs.credentials_blob = creds
+ vs.save()
+
+ # success!
+ ret = creds
+ else:
+ logger.error("Failed to look up VolumeSlice(%s, %s)" % (volume_name, slice_name))
+
+ else:
+ logger.error("Failed to generate credentials for %s, %s" % (volume_name, slice_name))
+
+ return ret
+
+
+#-------------------------------
+def get_volumeslice_volume_names( slice_name ):
+ """
+ Get the list of Volume names from the datastore.
+ """
+ try:
+ all_vs = models.VolumeSlice.objects.filter( slice_id__name = slice_name )
+ volume_names = []
+ for vs in all_vs:
+ volume_names.append( vs.volume_id.name )
+
+ return volume_names
+ except Exception, e:
+ logger.exception(e)
+ logger.error("Failed to query datastore for volumes mounted in %s" % slice_name)
+ return None
+
+
+#-------------------------------
+def get_volumeslice( volume_name, slice_name ):
+ """
+ Get a volumeslice record from the datastore.
+ """
+ try:
+ vs = models.VolumeSlice.objects.get( volume_id__name = volume_name, slice_id__name = slice_name )
+ return vs
+ except Exception, e:
+ logger.exception(e)
+ logger.error("Failed to query datastore for volumes (mounted in %s)" % (slice_name if (slice_name is not None or len(slice_name) > 0) else "UNKNOWN"))
+ return None
+
+
+#-------------------------------
+def do_push( instance_hosts, portnum, payload ):
+ """
+ Push a payload to a list of instances.
+ NOTE: this has to be done in one go, since we can't import grequests
+ into the global namespace (without wrecking havoc on the credential server),
+ but it has to stick around for the push to work.
+ """
+
+ global TESTING, CONFIG
+
+ from gevent import monkey
+
+ if TESTING:
+ monkey.patch_all()
+
+ else:
+ # make gevents runnabale from multiple threads (or Django will complain)
+ monkey.patch_all(socket=True, dns=True, time=True, select=True, thread=False, os=True, ssl=True, httplib=False, aggressive=True)
+
+ import grequests
+
+ # fan-out
+ requests = []
+ for sh in instance_hosts:
+ rs = grequests.post( "http://" + sh + ":" + str(portnum), data={"observer_message": payload}, timeout=getattr(CONFIG, "SYNDICATE_HTTP_PUSH_TIMEOUT", 60) )
+ requests.append( rs )
+
+ # fan-in
+ responses = grequests.map( requests )
+
+ assert len(responses) == len(requests), "grequests error: len(responses) != len(requests)"
+
+ for i in xrange(0,len(requests)):
+ resp = responses[i]
+ req = requests[i]
+
+ if resp is None:
+ logger.error("Failed to connect to %s" % (req.url))
+ continue
+
+ # verify they all worked
+ if resp.status_code != 200:
+ logger.error("Failed to POST to %s, status code = %s" % (resp.url, resp.status_code))
+ continue
+
+ return True
+
+
+#-------------------------------
+def get_slice_hostnames( slice_name ):
+ """
+ Query the Django DB and get the list of hosts running in a slice.
+ """
+
+ openstack_slice = Slice.objects.get( name=slice_name )
+ if openstack_slice is None:
+ logger.error("No such slice '%s'" % slice_name)
+ return None
+
+ hostnames = [s.node.name for s in openstack_slice.instances.all()]
+
+ return hostnames
+
+
+#-------------------------------
+def push_credentials_to_slice( slice_name, payload ):
+ """
+ Push a credentials payload to the VMs in a slice.
+ """
+ hostnames = get_slice_hostnames( slice_name )
+ return do_push( hostnames, CONFIG.SYNDICATE_SLIVER_PORT, payload )
+
+
+#-------------------------------
+class CredentialServerHandler( BaseHTTPServer.BaseHTTPRequestHandler ):
+ """
+ HTTP server handler that allows syndicated.py instances to poll
+ for volume state.
+
+ NOTE: this is a fall-back mechanism. The observer should push new
+ volume state to the slices' instances. However, if that fails, the
+ instances are configured to poll for volume state periodically. This
+ server allows them to do just that.
+
+ Responses:
+ GET /<slicename> -- Reply with the signed sealed list of volume names, encrypted by the slice secret
+ GET /<slicename>/<volumename> -- Reply with the signed sealed volume access credentials, encrypted by the slice secret
+
+ !!! TEMPORARY !!!
+ GET /<slicename>/SYNDICATE_SLICE_SECRET -- Reply with the slice secret (TEMPORARY)
+
+
+ NOTE: We want to limit who can learn which Volumes a slice can access, so we'll seal its instances'
+ credentials with the SliceSecret secret. The instances (which have the slice-wide secret) can then decrypt it.
+ However, sealing the listing is a time-consuming process (on the order of 10s), so we only want
+ to do it when we have to. Since *anyone* can ask for the ciphertext of the volume list,
+ we will cache the list ciphertext for each slice for a long-ish amount of time, so we don't
+ accidentally DDoS this server. This necessarily means that the instance might see a stale
+ volume listing, but that's okay, since the Observer is eventually consistent anyway.
+ """
+
+ cached_volumes_json = {} # map slice_name --> (volume name, timeout)
+ cached_volumes_json_lock = threading.Lock()
+
+ CACHED_VOLUMES_JSON_LIFETIME = 3600 # one hour
+
+ SLICE_SECRET_NAME = "SYNDICATE_SLICE_SECRET"
+
+ def parse_request_path( self, path ):
+ """
+ Parse the URL path into a slice name and (possibly) a volume name or SLICE_SECRET_NAME
+ """
+ path_parts = path.strip("/").split("/")
+
+ if len(path_parts) == 0:
+ # invalid
+ return (None, None)
+
+ if len(path_parts) > 2:
+ # invalid
+ return (None, None)
+
+ slice_name = path_parts[0]
+ if len(slice_name) == 0:
+ # empty string is invalid
+ return (None, None)
+
+ volume_name = None
+
+ if len(path_parts) > 1:
+ volume_name = path_parts[1]
+
+ return slice_name, volume_name
+
+
+ def reply_data( self, data, datatype="application/json" ):
+ """
+ Give back a 200 response with data.
+ """
+ self.send_response( 200 )
+ self.send_header( "Content-Type", datatype )
+ self.send_header( "Content-Length", len(data) )
+ self.end_headers()
+
+ self.wfile.write( data )
+ return
+
+
+ def get_volumes_message( self, private_key_pem, observer_secret, slice_name ):
+ """
+ Get the json-ized list of volumes this slice is attached to.
+ Check the cache, evict stale data if necessary, and on miss,
+ regenerate the slice volume list.
+ """
+
+ # block the cache.
+ # NOTE: don't release the lock until we've generated credentials.
+ # Chances are, there's a thundering herd of instances coming online.
+ # Block them all until we've generated their slice's credentials,
+ # and then serve them the cached one.
+
+ self.cached_volumes_json_lock.acquire()
+
+ ret = None
+ volume_list_json, cache_timeout = self.cached_volumes_json.get( slice_name, (None, None) )
+
+ if (cache_timeout is not None) and cache_timeout < time.time():
+ # expired
+ volume_list_json = None
+
+ if volume_list_json is None:
+ # generate a new list and cache it.
+
+ volume_names = get_volumeslice_volume_names( slice_name )
+ if volume_names is None:
+ # nothing to do...
+ ret = None
+
+ else:
+ # get the slice secret
+ slice_secret = get_slice_secret( private_key_pem, slice_name )
+
+ if slice_secret is None:
+ # no such slice
+ logger.error("No slice secret for %s" % slice_name)
+ ret = None
+
+ else:
+ # seal and sign
+ ret = create_volume_list_blob( private_key_pem, slice_secret, volume_names )
+
+ # cache this
+ if ret is not None:
+ self.cached_volumes_json[ slice_name ] = (ret, time.time() + self.CACHED_VOLUMES_JSON_LIFETIME )
+
+ else:
+ # hit the cache
+ ret = volume_list_json
+
+ self.cached_volumes_json_lock.release()
+
+ return ret
+
+
+ def do_GET( self ):
+ """
+ Handle one GET
+ """
+ slice_name, volume_name = self.parse_request_path( self.path )
+
+ # valid request?
+ if volume_name is None and slice_name is None:
+ self.send_error( 400 )
+
+ # slice secret request?
+ elif volume_name == self.SLICE_SECRET_NAME and slice_name is not None:
+
+ # get the slice secret
+ ret = get_slice_secret( self.server.private_key_pem, slice_name )
+
+ if ret is not None:
+ self.reply_data( ret )
+ return
+ else:
+ self.send_error( 404 )
+
+ # volume list request?
+ elif volume_name is None and slice_name is not None:
+
+ # get the list of volumes for this slice
+ ret = self.get_volumes_message( self.server.private_key_pem, self.server.observer_secret, slice_name )
+
+ if ret is not None:
+ self.reply_data( ret )
+ return
+ else:
+ self.send_error( 404 )
+
+ # volume credential request?
+ elif volume_name is not None and slice_name is not None:
+
+ # get the VolumeSlice record
+ vs = get_volumeslice( volume_name, slice_name )
+ if vs is None:
+ # not found
+ self.send_error( 404 )
+ return
+
+ else:
+ ret = vs.credentials_blob
+ if ret is not None:
+ self.reply_data( vs.credentials_blob )
+ else:
+ # not generated???
+ print ""
+ print vs
+ print ""
+ self.send_error( 503 )
+ return
+
+ else:
+ # shouldn't get here...
+ self.send_error( 500 )
+ return
+
+
+#-------------------------------
+class CredentialServer( BaseHTTPServer.HTTPServer ):
+
+ def __init__(self, private_key_pem, observer_secret, server, req_handler ):
+ self.private_key_pem = private_key_pem
+ self.observer_secret = observer_secret
+ BaseHTTPServer.HTTPServer.__init__( self, server, req_handler )
+
+
+#-------------------------------
+def credential_server_spawn( old_exit_status ):
+ """
+ Start our credential server (i.e. in a separate process, started by the watchdog)
+ """
+
+ setproctitle.setproctitle( "syndicate-credential-server" )
+
+ private_key = syndicate_storage.read_private_key( CONFIG.SYNDICATE_PRIVATE_KEY )
+ if private_key is None:
+ # exit code 255 will be ignored...
+ logger.error("Cannot load private key. Exiting...")
+ sys.exit(255)
+
+ logger.info("Starting Syndicate Observer credential server on port %s" % CONFIG.SYNDICATE_HTTP_PORT)
+
+ srv = CredentialServer( private_key.exportKey(), CONFIG.SYNDICATE_OPENCLOUD_SECRET, ('', CONFIG.SYNDICATE_HTTP_PORT), CredentialServerHandler)
+ srv.serve_forever()
+
+
+#-------------------------------
+def ensure_credential_server_running( foreground=False, run_once=False ):
+ """
+ Instantiate our credential server and keep it running.
+ """
+
+ # is the watchdog running?
+ pids = syndicate_watchdog.find_by_attrs( "syndicate-credential-server-watchdog", {} )
+ if len(pids) > 0:
+ # it's running
+ return True
+
+ if foreground:
+ # run in foreground
+
+ if run_once:
+ return credential_server_spawn( 0 )
+
+ else:
+ return syndicate_watchdog.main( credential_server_spawn, respawn_exit_statuses=range(1,254) )
+
+
+ # not running, and not foregrounding. fork a new one
+ try:
+ watchdog_pid = os.fork()
+ except OSError, oe:
+ logger.error("Failed to fork, errno = %s" % oe.errno)
+ return False
+
+ if watchdog_pid != 0:
+
+ # child--become watchdog
+ setproctitle.setproctitle( "syndicate-credential-server-watchdog" )
+
+ if run_once:
+ syndicate_daemon.daemonize( lambda: credential_server_spawn(0), logfile_path=getattr(CONFIG, "SYNDICATE_HTTP_LOGFILE", None) )
+
+ else:
+ syndicate_daemon.daemonize( lambda: syndicate_watchdog.main( credential_server_spawn, respawn_exit_statuses=range(1,254) ), logfile_path=getattr(CONFIG, "SYNDICATE_HTTP_LOGFILE", None) )
+
+
+#-------------------------------
+# Begin functional tests.
+# Any method starting with ft_ is a functional test.
+#-------------------------------
+
+#-------------------------------
+def ft_syndicate_access():
+ """
+ Functional tests for ensuring objects exist and don't exist in Syndicate.
+ """
+
+ fake_user = FakeObject()
+ fake_user.email = "fakeuser@opencloud.us"
+
+ print "\nensure_user_exists(%s)\n" % fake_user.email
+ ensure_user_exists( fake_user.email, is_admin=False, max_UGs=1100, max_RGs=1 )
+
+ print "\nensure_user_exists(%s)\n" % fake_user.email
+ ensure_user_exists( fake_user.email, is_admin=False, max_UGs=1100, max_RGs=1 )
+
+ fake_volume = FakeObject()
+ fake_volume.name = "fakevolume"
+ fake_volume.description = "This is a fake volume, created for funtional testing"
+ fake_volume.blocksize = 1024
+ fake_volume.cap_read_data = True
+ fake_volume.cap_write_data = True
+ fake_volume.cap_host_data = False
+ fake_volume.archive = False
+ fake_volume.private = True
+
+ # test idempotency
+ print "\nensure_volume_exists(%s)\n" % fake_volume.name
+ ensure_volume_exists( fake_user.email, fake_volume )
+
+ print "\nensure_volume_exists(%s)\n" % fake_volume.name
+ ensure_volume_exists( fake_user.email, fake_volume )
+
+ print "\nensure_volume_access_right_exists(%s,%s)\n" % (fake_user.email, fake_volume.name)
+ ensure_volume_access_right_exists( fake_user.email, fake_volume.name, 31 )
+
+ print "\nensure_volume_access_right_exists(%s,%s)\n" % (fake_user.email, fake_volume.name)
+ ensure_volume_access_right_exists( fake_user.email, fake_volume.name, 31 )
+
+ print "\nensure_volume_access_right_absent(%s,%s)\n" % (fake_user.email, fake_volume.name)
+ ensure_volume_access_right_absent( fake_user.email, fake_volume.name )
+
+ print "\nensure_volume_access_right_absent(%s,%s)\n" % (fake_user.email, fake_volume.name)
+ ensure_volume_access_right_absent( fake_user.email, fake_volume.name )
+
+ print "\nensure_volume_absent(%s)\n" % fake_volume.name
+ ensure_volume_absent( fake_volume.name )
+
+ print "\nensure_volume_absent(%s)\n" % fake_volume.name
+ ensure_volume_absent( fake_volume.name )
+
+ print "\nensure_user_absent(%s)\n" % fake_user.email
+ ensure_user_absent( fake_user.email )
+
+ print "\nensure_user_absent(%s)\n" % fake_user.email
+ ensure_user_absent( fake_user.email )
+
+
+
+
+ print "\nensure_principal_exists(%s)\n" % fake_user.email
+ ensure_principal_exists( fake_user.email, "asdf", is_admin=False, max_UGs=1100, max_RGs=1 )
+
+ print "\nensure_principal_exists(%s)\n" % fake_user.email
+ ensure_principal_exists( fake_user.email, "asdf", is_admin=False, max_UGs=1100, max_RGs=1 )
+
+ print "\nensure_volume_exists(%s)\n" % fake_volume.name
+ ensure_volume_exists( fake_user.email, fake_volume )
+
+ print "\nsetup_volume_access(%s, %s)\n" % (fake_user.email, fake_volume.name)
+ setup_volume_access( fake_user.email, fake_volume.name, 31, 38800, "abcdef" )
+
+ print "\nsetup_volume_access(%s, %s)\n" % (fake_user.email, fake_volume.name)
+ setup_volume_access( fake_user.email, fake_volume.name, 31, 38800, "abcdef" )
+
+ print "\nteardown_volume_access(%s, %s)\n" % (fake_user.email, fake_volume.name )
+ teardown_volume_access( fake_user.email, fake_volume.name )
+
+ print "\nteardown_volume_access(%s, %s)\n" % (fake_user.email, fake_volume.name )
+ teardown_volume_access( fake_user.email, fake_volume.name )
+
+ print "\nensure_volume_absent(%s)\n" % fake_volume.name
+ ensure_volume_absent( fake_volume.name )
+
+ print "\nensure_principal_absent(%s)\n" % fake_user.email
+ ensure_principal_absent( fake_user.email )
+
+
+
+#-------------------------------
+def ft_volumeslice( slice_name ):
+ """
+ Functional tests for reading VolumeSlice information
+ """
+ print "slice: %s" % slice_name
+
+ volumes = get_volumeslice_volume_names( slice_name )
+
+ print "volumes mounted in slice %s:" % slice_name
+ for v in volumes:
+ print " %s:" % v
+
+ vs = get_volumeslice( v, slice_name )
+
+ print " %s" % dir(vs)
+
+
+#-------------------------------
+def ft_get_slice_hostnames( slice_name ):
+ """
+ Functional tests for getting slice hostnames
+ """
+
+ print "Get slice hostnames for %s" % slice_name
+
+ hostnames = get_slice_hostnames( slice_name )
+ import pprint
+
+ pp = pprint.PrettyPrinter()
+
+ pp.pprint( hostnames )
+
+
+#-------------------------------
+def ft_syndicate_principal():
+ """
+ Functional tests for creating, reading, and deleting SyndicatePrincipals.
+ """
+ print "generating key pair"
+ pubkey_pem, privkey_pem = api.generate_key_pair( 4096 )
+
+ user_email = "fakeuser@opencloud.us"
+
+ print "saving principal"
+ put_principal_data( user_email, "asdf", pubkey_pem, privkey_pem )
+
+ print "fetching principal private key"
+ saved_privkey_pem = get_principal_pkey( user_email, "asdf" )
+
+ assert saved_privkey_pem is not None, "Could not fetch saved private key"
+ assert saved_privkey_pem == privkey_pem, "Saved private key does not match actual private key"
+
+ print "delete principal"
+
+ delete_principal_data( user_email )
+
+ print "make sure its deleted..."
+
+ saved_privkey_pem = get_principal_pkey( user_email, "asdf" )
+
+ assert saved_privkey_pem is None, "Principal key not deleted"
+
+
+#-------------------------------
+def ft_credential_server():
+ """
+ Functional test for the credential server
+ """
+ ensure_credential_server_running( run_once=True, foreground=True )
+
+
+#-------------------------------
+def ft_seal_and_unseal():
+ """
+ Functional test for sealing/unsealing data
+ """
+ print "generating key pair"
+ pubkey_pem, privkey_pem = api.generate_key_pair( 4096 )
+
+ sealed_buf = create_sealed_and_signed_blob( privkey_pem, "foo", "hello world")
+ print "sealed data is:\n\n%s\n\n" % sealed_buf
+
+ buf = verify_and_unseal_blob( pubkey_pem, "foo", sealed_buf )
+ print "unsealed data is: \n\n%s\n\n" % buf
+
+
+# run functional tests
+if __name__ == "__main__":
+ sys.path.append("/opt/xos")
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+
+ if len(sys.argv) < 2:
+ print "Usage: %s testname [args]" % sys.argv[0]
+
+ # call a method starting with ft_, and then pass the rest of argv as its arguments
+ testname = sys.argv[1]
+ ft_testname = "ft_%s" % testname
+
+ test_call = "%s(%s)" % (ft_testname, ",".join(sys.argv[2:]))
+
+ print "calling %s" % test_call
+
+ rc = eval( test_call )
+
+ print "result = %s" % rc
+
+
diff --git a/xos/observers/syndicate/syndicatelib_config/__init__.py b/xos/observers/syndicate/syndicatelib_config/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib_config/__init__.py
diff --git a/xos/observers/syndicate/syndicatelib_config/config-jude.py b/xos/observers/syndicate/syndicatelib_config/config-jude.py
new file mode 100644
index 0000000..9e0f1fd
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib_config/config-jude.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+# configuration for syndicatelib
+SYNDICATE_SMI_URL="http://localhost:8080"
+
+SYNDICATE_OPENID_TRUSTROOT="http://localhost:8081"
+
+SYNDICATE_OPENCLOUD_USER="judecn@gmail.com"
+SYNDICATE_OPENCLOUD_PASSWORD="nya"
+
+SYNDICATE_PYTHONPATH="/home/jude/Desktop/research/git/syndicate/build/out/python"
+
+SYNDICATE_PRIVATE_KEY="/home/jude/Desktop/research/git/syndicate/opencloud/observers/syndicate/syndicatelib_config/pollserver.pem"
+SYNDICATE_OPENCLOUD_SECRET="e4988309a5005edb8ea185f16f607938c0fb7657e4d7609853bcb7c4884d1c92"
+
+SYNDICATE_HTTP_PORT=65321
+
+SYNDICATE_RG_CLOSURE="/home/jude/Desktop/research/git/syndicate/build/out/python/syndicate/rg/drivers/disk"
+SYNDICATE_RG_DEFAULT_PORT=38800
+
+DEBUG=True
diff --git a/xos/observers/syndicate/syndicatelib_config/config-opencloud.py b/xos/observers/syndicate/syndicatelib_config/config-opencloud.py
new file mode 100644
index 0000000..b3add16
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib_config/config-opencloud.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+
+# ---------------------------------
+# This is the configuration file used by the Syndicate observer.
+# It is a well-formed Python file, and will be imported into the
+# observer as a Python module. This means you can run any config-
+# generation code here you like, but all of the following global
+# variables must be defined.
+# ---------------------------------
+
+# URL to the Syndicate SMI. For example, https://syndicate-metadata.appspot.com
+SYNDICATE_SMI_URL="http://localhost:8080"
+
+# If you are going to use OpenID to authenticate the Syndicate instance daemon,
+# this is the OpenID provider URL. It is currently used only to generate
+# identity pages for users, so you can put whatever you want here for now.
+SYNDICATE_OPENID_TRUSTROOT="http://localhost:8081"
+
+# This is the observer's user account on Syndicate. You must create it out-of-band
+# prior to using the observer, and it must be an admin user since it will
+# create other users (i.e. for slices).
+SYNDICATE_OPENCLOUD_USER="jcnelson@cs.princeton.edu"
+
+# This is the password for the observer to authenticate itself to Syndicate.
+SYNDICATE_OPENCLOUD_PASSWORD="nya"
+
+# If the observer uses public-key authentication with Syndicate, you will
+# need to identify the absolute path to its private key here. It must be
+# a 4096-bit PEM-encoded RSA key, and the Syndicate observer's user account
+# must have been given the public key on activation.
+SYNDICATE_OPENCLOUD_PKEY=None
+
+# This is the location on disk where Syndicate observer code can be found,
+# if it is not already in the Python path. This is optional.
+SYNDICATE_PYTHONPATH="/root/syndicate/build/out/python"
+
+# This is the location of the observer's private key. It must be an absolute
+# path, and refer to a 4096-bit PEM-encoded RSA key.
+SYNDICATE_PRIVATE_KEY="/opt/xos/observers/syndicate/syndicatelib_config/pollserver.pem"
+
+# This is the master secret used to generate secrets to seal sensitive information sent to the
+# Syndicate instance mount daemons. It is also used to seal sensitive information
+# stored to the Django database.
+# TODO: think of a way to not have to store this on disk. Maybe we feed into the
+# observer when it starts up?
+SYNDICATE_OPENCLOUD_SECRET="e4988309a5005edb8ea185f16f607938c0fb7657e4d7609853bcb7c4884d1c92"
+
+# This is the default port number on which a Syndicate Replica Gateway
+# will be provisioned. It's a well-known port, and can be the same across
+# instances, since in OpenCloud, an RG instance only listens to localhost.
+SYNDICATE_RG_DEFAULT_PORT=38800
+
+# This is the absolute path to the RG's storage driver (which will be automatically
+# pushed to instances by Syndicate). See https://github.com/jcnelson/syndicate/wiki/Replica-Gateways
+SYNDICATE_RG_CLOSURE=None
+
+# This is the port number the observer listens on for GETs from the Syndicate instance mount
+# daemons. Normally, the oserver pushes (encrypted) commands to the daemons, but if the
+# daemons are NAT'ed or temporarily partitioned, they will pull commands instead.
+SYNDICATE_HTTP_PORT=65321
+
+# This is the path to the logfile for the observer's HTTP server.
+SYNDICATE_HTTP_LOGFILE="/tmp/syndicate-observer.log"
+
+# This is the number of seconds to wait for pushing a slice credential before timing out.
+SYNDICATE_HTTP_PUSH_TIMEOUT=60
+
+# This is the port number the Syndicate instance mount daemons listen on. The observer will
+# push commands to them on this port.
+SYNDICATE_SLIVER_PORT=65322
+
+# If true, print verbose debug messages.
+DEBUG=True
diff --git a/xos/observers/syndicate/syndicatelib_config/config.py b/xos/observers/syndicate/syndicatelib_config/config.py
new file mode 100644
index 0000000..b3add16
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib_config/config.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+
+# ---------------------------------
+# This is the configuration file used by the Syndicate observer.
+# It is a well-formed Python file, and will be imported into the
+# observer as a Python module. This means you can run any config-
+# generation code here you like, but all of the following global
+# variables must be defined.
+# ---------------------------------
+
+# URL to the Syndicate SMI. For example, https://syndicate-metadata.appspot.com
+SYNDICATE_SMI_URL="http://localhost:8080"
+
+# If you are going to use OpenID to authenticate the Syndicate instance daemon,
+# this is the OpenID provider URL. It is currently used only to generate
+# identity pages for users, so you can put whatever you want here for now.
+SYNDICATE_OPENID_TRUSTROOT="http://localhost:8081"
+
+# This is the observer's user account on Syndicate. You must create it out-of-band
+# prior to using the observer, and it must be an admin user since it will
+# create other users (i.e. for slices).
+SYNDICATE_OPENCLOUD_USER="jcnelson@cs.princeton.edu"
+
+# This is the password for the observer to authenticate itself to Syndicate.
+SYNDICATE_OPENCLOUD_PASSWORD="nya"
+
+# If the observer uses public-key authentication with Syndicate, you will
+# need to identify the absolute path to its private key here. It must be
+# a 4096-bit PEM-encoded RSA key, and the Syndicate observer's user account
+# must have been given the public key on activation.
+SYNDICATE_OPENCLOUD_PKEY=None
+
+# This is the location on disk where Syndicate observer code can be found,
+# if it is not already in the Python path. This is optional.
+SYNDICATE_PYTHONPATH="/root/syndicate/build/out/python"
+
+# This is the location of the observer's private key. It must be an absolute
+# path, and refer to a 4096-bit PEM-encoded RSA key.
+SYNDICATE_PRIVATE_KEY="/opt/xos/observers/syndicate/syndicatelib_config/pollserver.pem"
+
+# This is the master secret used to generate secrets to seal sensitive information sent to the
+# Syndicate instance mount daemons. It is also used to seal sensitive information
+# stored to the Django database.
+# TODO: think of a way to not have to store this on disk. Maybe we feed into the
+# observer when it starts up?
+SYNDICATE_OPENCLOUD_SECRET="e4988309a5005edb8ea185f16f607938c0fb7657e4d7609853bcb7c4884d1c92"
+
+# This is the default port number on which a Syndicate Replica Gateway
+# will be provisioned. It's a well-known port, and can be the same across
+# instances, since in OpenCloud, an RG instance only listens to localhost.
+SYNDICATE_RG_DEFAULT_PORT=38800
+
+# This is the absolute path to the RG's storage driver (which will be automatically
+# pushed to instances by Syndicate). See https://github.com/jcnelson/syndicate/wiki/Replica-Gateways
+SYNDICATE_RG_CLOSURE=None
+
+# This is the port number the observer listens on for GETs from the Syndicate instance mount
+# daemons. Normally, the oserver pushes (encrypted) commands to the daemons, but if the
+# daemons are NAT'ed or temporarily partitioned, they will pull commands instead.
+SYNDICATE_HTTP_PORT=65321
+
+# This is the path to the logfile for the observer's HTTP server.
+SYNDICATE_HTTP_LOGFILE="/tmp/syndicate-observer.log"
+
+# This is the number of seconds to wait for pushing a slice credential before timing out.
+SYNDICATE_HTTP_PUSH_TIMEOUT=60
+
+# This is the port number the Syndicate instance mount daemons listen on. The observer will
+# push commands to them on this port.
+SYNDICATE_SLIVER_PORT=65322
+
+# If true, print verbose debug messages.
+DEBUG=True
diff --git a/xos/observers/syndicate/syndicatelib_config/pollserver.pem b/xos/observers/syndicate/syndicatelib_config/pollserver.pem
new file mode 100644
index 0000000..cb50de7
--- /dev/null
+++ b/xos/observers/syndicate/syndicatelib_config/pollserver.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKwIBAAKCAgEA6yeoVsm+Yf4sgW/9kzk2NfwkbHloIKXUOpi5x5LkEbnohNRC
+RIGjiMJJp/OefduU3c14h/K6Qefi9j4dw4pyvh2QP36K4lJObIpKAdohHjVjxHqK
+12bVXvCpCAbJHkiX8VK5HGJPDr6sJke1vPTrP6RSWxT7ZRawIBInKuT/OVshskzh
+kOwVXb5ct0AXjRZ6FBdXvNcJnNONKRCIuFHOx2roWLsdgTlPI3+bJim2dQ0JKyHh
+uaTPguZ4s23sCuSKyXCh/X9yVloxIraY6KdaAKQZLyANLfXQdsyyH69kQvvBEZ2R
+EXD0c1qIZwuIm68UH+60BwTPdXGWSL73C0Zsq36vZOadFPb0pmz/o4CuguILNA3i
+560MKcVvQ8HVqA56z+v8pE0TRp0ajTDtpW2ee+t1cXE8VzCwthkIxmneFk+ityoD
+o1N+fBUu4lXJ3kl2qGk+0KECqJ6sc/QN+Ft97JTTRshSzn1kqIlKQoZo3u0Jeo/G
+PFZ0b13/DxYA7nvjt2az48h0VL4mNf5tzDr8GxOK4lYoWIGzKjZLDeJRyxLOCK5j
+F/AvbbSnegT0O/vamn5EoN8HfooH5qiJdPDerriPsgN3HlcY6QjrY5phmAFiGa5T
+X1j1VNb5oamRslgPv3rwTvioTaY/wUmrHLBuU6Sqg/WGrLO2Bocg0USMbG8CAwEA
+AQKCAgEArxuO7WG5lXsSZSih6Rm3Vqf175jQg085JJFJ9mVZ1CFeFluBJUZsIpCb
+DKgLI6l5x1kUIhgLvrwQdFF5FH1qSEv3eHCgtzuXDphD1/E4rCgRrOObtB7tUI9h
+L4ruBNEF5Dw3f/1s5Yvy4WaQ3K58551TfmO3eGVWresWo4h2zZ0hEIbTiXljx7TT
+kdn2L6fHLGLdgM+YZuHZwfR/+tFga3sencRoiivE1KhXPindpngYlbfbQMSLiexZ
+gTOfi9T3zF1FI2HeIJN092aFounLyaJo5oC1j732iCCRm6qdvIuAD8AHoLc+MQ//
+dsxN47CSCd1Uzc01Nz1oLa+WgxzkGbsGNO2eVkRj/xzB0Rld/+Q1YQvn1TnWZFuG
+nXXKi+VwSX8htpDaZL5+hWVy39YXUQcBkTzBIS69tdfd7HCZS0JnKeOMYQFgvANH
+0/J529l8m0oHpex4DdW1scHXgcBOq6oD6KVkiNurfWZu/mWwdEnxAKtRFZc/xFfh
+a4kbTgNk3unGL+gZzeWL1YuIM843Ia4J8V0PYH7GueeZBaXyiT7r4x5hgQ57ObkX
+K9wlgrvSHBNq4OhzygTNs37vJwu38ry2AGmA8LuiFBeVCsVhMk3yVz4A6fXjwWH8
+266gNuODIiHelahvz/IBGLdrjnbA4SYaYQh1SYKfCRdlA2yNlBECggEBAPcqiWP/
+XUCsCy9vMbCvZNCUyr4nqTSvWkGsie6t+yIC0pYCynKdrL9WNF5zee9SVmtcBO0Q
+z+aff8TZAax3tWvD5GzlQOQh1l4QBj30uatklQ0nvwbw+gf5EFG2QerPakwyfC59
+dSagxchzpjloBGniq7jqc6vL3xlZ62vnOLHf+nOQXzDcZ7QK/uLRKj2r01D5+9lh
+08Ah42QID5VQL/NMyg2lylXaPXx6TnSMjJVjzNmLRCIRlOstAOt3isBJ21sT0LOk
+lCGvuF//cwS7VABRMa0TspSEkuMbgFw0XEZStkh68eEUVqax+HHfa1rlxobSIwib
+1Oa9s7KbQNaozgUCggEBAPOPOSLazItJ4OOFu8/69M33Ag8hpPZNkMw1fw99p2fD
+KnZYlEWHgF4Z76gkozHh+nk8HhohvYfIhmFYEHQOXfmgljsz3jFJKwTEnfN7IsZA
+C3TVl6OVuY2rjhBOe3137CYHn9d8KRaJAKdyd038LK29Yy+FvUVw6LD4QUoRiA21
+9sOrhO/Wgcohnqk5yVnXtBkp7j7qGN9d+GLZVAVOqKjniQqy9ir3IdLYmB801t9P
+TcbucmgEzs/cmx7d/jv1kx9/O0HHIm959Ox66hPkcG3bssJk41F6PDMOVEWiC/hc
+E5a7Mlr6M4YhuDjO1zoyQpy4Sj/MKpasnotNSL51JuMCggEBALhYkYBzximmJ/GJ
+DZaqOpcXYt/Q1PLmlnrFJVtPiC8ly8r26efykhVjRkvr9NX6o1oPl9z43Rc1fyZi
+dE0eO8HUqVpO4sdENY6ShRVQoeqjakgVjPSwZsvrh7BqL1/is3WBcf16tRXKc7m+
+CAxo+GHBHjMdKojH1e4ikuQ34KFKXJI068qVmQM/8DtbphW5QjLzQFQyEq0KmX7S
+RE0pMZpVe54SOYcu7w0Ya8uhyHjjprXamUaPtnJxbm4xCtvAOksDzHUwGwvE888l
+x7OPxGc4J8TfHCKJfsTEjkg3BVut9Sa6DA3EDZzmwFauPHPfTOLheB/Dmlc+xfhA
+s2tnG8ECggEBAKiLbFaaYwHg1iec3CNI3y/IxzwBZE6tzo4CVzM5GSfM/w12ruSO
+qF52REpvUB+s6dALsikTQD0+nv+uGXS2nIGqh0vg0Nn6cDKUfVmI1L+sgkEPrigd
+7JIFLgJKzVo+KsUGca6E1Uoq9LDrnXPyFlkEviacviXXxK7ynPvMtgIG8gTmJNBz
++M0QBuPEgXoSsybWxW/0P9ITDVgaXPJvRHfeAg/NWFzTOCzYhizSO/+8uW34hGNH
+MHbXiuEJbm2/u1gIi9ExJLtQAhXD2Uh6xPLBHis39bbkh9QtDlRBl1b/IO8mC+q5
+Sf6ARyPIv1gef8pEHd2YQ8CRJAXyLWzfVVECggEBANrvnE2hQaYqe/BM9QGN9Cam
+CUTTBhvQDTBnpBYv8iQCgy0nmmVZ07j0yjR/I5wipcWAB7Bskv1rfh/3VpWUGCcR
+2MnPobZnvL1Dl22G7P8HBUiIA+NGWNdR5FyIL/yLy2BVEs7dNeK5WolD8IQP+fTw
+E9Mvd6ns2TIveXMZFtqRja3H426iv38QqWg0RmmhcmnSkD7SqAZWGI+OoRsUJ2Et
+bg4N9Cb46Gjqdh8SQF+rXYfL1AWnjMM7/AhJLMoWWb0sBzqA7UeJxLlAt1Lgnnsl
+P2nszH+Ia9V0dSlr79haGo80FALM8TiKBAQ/bTktqP5vOWSlCzHj7K30Bil36TQ=
+-----END RSA PRIVATE KEY-----
diff --git a/xos/observers/vbng/model-deps b/xos/observers/vbng/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/observers/vbng/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/observers/vbng/run.sh b/xos/observers/vbng/run.sh
new file mode 100755
index 0000000..efb586f
--- /dev/null
+++ b/xos/observers/vbng/run.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vbng-observer.py ]]; then
+# ln -s ../../xos-observer.py vbng-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+python vbng-observer.py -C $XOS_DIR/observers/vbng/vbng_observer_config
diff --git a/xos/observers/vbng/start.sh b/xos/observers/vbng/start.sh
new file mode 100755
index 0000000..98008f4
--- /dev/null
+++ b/xos/observers/vbng/start.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vbng-observer.py ]]; then
+# ln -s ../../xos-observer.py vbng-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+nohup python vbng-observer.py -C $XOS_DIR/observers/vbng/vbng_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/vbng/steps/sync_vbngtenant.py b/xos/observers/vbng/steps/sync_vbngtenant.py
new file mode 100644
index 0000000..c997f4f
--- /dev/null
+++ b/xos/observers/vbng/steps/sync_vbngtenant.py
@@ -0,0 +1,142 @@
+import os
+import requests
+import socket
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from core.models import Service
+from services.cord.models import VCPEService, VCPETenant, VBNGTenant, VBNGService
+from services.hpc.models import HpcService, CDNPrefix
+from util.logger import Logger, logging
+
+# VBNG_API = "http://10.0.3.136:8181/onos/virtualbng/privateip/"
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+class SyncVBNGTenant(SyncStep):
+ provides=[VCPETenant]
+ observes=VCPETenant
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+
+ def fetch_pending(self, deleted):
+ if (not deleted):
+ objs = VBNGTenant.get_tenant_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
+ else:
+ objs = VBNGTenant.get_deleted_tenant_objects()
+
+ return objs
+
+ def defer_sync(self, o, reason):
+ logger.info("defer object %s due to %s" % (str(o), reason))
+ raise Exception("defer object %s due to %s" % (str(o), reason))
+
+ def get_vbng_service(self, o):
+ if not o.provider_service:
+ raise Exception("vBNG tenant %s has no provider_service" % str(o.id))
+ services = VBNGService.get_service_objects().filter(id = o.provider_service.id)
+ if not services:
+ raise Exception("vBNG tenant %s is associated with the wrong kind of provider_service" % str(o.id))
+ return services[0]
+
+ def get_vbng_url(self, o):
+ service = self.get_vbng_service(o)
+
+ # if the service object specifies a vbng_url, then use it
+ if service.vbng_url:
+ return service.vbng_url
+
+ # otherwise, see if the service has tenancy in ONOS
+ for tenant in service.subscribed_tenants.all():
+ if tenant.provider_service and tenant.provider_service.kind == "onos":
+ onos_service = tenant.provider_service
+ if not onos_service.slices.exists():
+ raise Exception("vBNG service is linked to an ONOSApp, but the App's Service has no slices")
+ onos_slice = onos_service.slices.all()[0]
+ if not onos_slice.instances.exists():
+ raise Exception("vBNG service is linked to an ONOSApp, but the App's Service's Slice has no instances")
+ instance = onos_slice.instances.all()[0]
+
+ #onos_app = ONOSApp.objects.filter(id = tenant.id)
+ #instance = onos_app.instance
+ #if not instance:
+ # raise Exception("ONOSApp has no instance")
+
+ if not instance.instance_name:
+ raise Exception("vBNG service is linked to an ONOSApp, but the App's Service's Slice's first instance is not instantiated")
+ ip = instance.get_network_ip("nat")
+ if not ip:
+ raise Exception("vBNG service is linked to an ONOSApp, but the App's Service's Slice's first instance does not have an ip")
+
+ logger.info("Using ip %s from ONOS Instance %s" % (ip, instance))
+
+ return "http://%s:8181/onos/virtualbng/" % ip
+
+ raise Exception("vBNG service does not have vbng_url set, and is not linked to an ONOSApp")
+
+ def get_private_interface(self, o):
+ vcpes = VCPETenant.get_tenant_objects().all()
+ vcpes = [x for x in vcpes if (x.vbng is not None) and (x.vbng.id == o.id)]
+ if not vcpes:
+ raise Exception("No vCPE tenant is associated with vBNG %s" % str(o.id))
+ if len(vcpes)>1:
+ raise Exception("More than one vCPE tenant is associated with vBNG %s" % str(o.id))
+
+ vcpe = vcpes[0]
+ instance = vcpe.instance
+
+ if not instance:
+ raise Exception("No instance associated with vBNG %s" % str(o.id))
+
+ if not vcpe.wan_ip:
+ self.defer_sync(o, "does not have a WAN IP yet")
+
+ if not vcpe.wan_container_mac:
+ # this should never happen; container MAC is computed from WAN IP
+ self.defer_sync(o, "does not have a WAN container MAC yet")
+
+ return (vcpe.wan_ip, vcpe.wan_container_mac, vcpe.instance.node.name)
+
+ def sync_record(self, o):
+ logger.info("sync'ing VBNGTenant %s" % str(o))
+
+ if not o.routeable_subnet:
+ (private_ip, private_mac, private_hostname) = self.get_private_interface(o)
+ logger.info("contacting vBNG service to request mapping for private ip %s mac %s host %s" % (private_ip, private_mac, private_hostname) )
+
+ url = self.get_vbng_url(o) + "privateip/%s/%s/%s" % (private_ip, private_mac, private_hostname)
+ logger.info( "vbng url: %s" % url )
+ r = requests.post(url )
+ if (r.status_code != 200):
+ raise Exception("Received error from bng service (%d)" % r.status_code)
+ logger.info("received public IP %s from private IP %s" % (r.text, private_ip))
+
+ if r.text == "0":
+ raise Exception("VBNG service failed to return a routeable_subnet (probably ran out)")
+
+ o.routeable_subnet = r.text
+ o.mapped_ip = private_ip
+ o.mapped_mac = private_mac
+ o.mapped_hostname = private_hostname
+
+ o.save()
+
+ def delete_record(self, o):
+ logger.info("deleting VBNGTenant %s" % str(o))
+
+ if o.mapped_ip:
+ private_ip = o.mapped_ip
+ logger.info("contacting vBNG service to delete private ip %s" % private_ip)
+ r = requests.delete(self.get_vbng_url(o) + "privateip/%s" % private_ip, )
+ if (r.status_code != 200):
+ raise Exception("Received error from bng service (%d)" % r.status_code)
+
diff --git a/xos/observers/vbng/stop.sh b/xos/observers/vbng/stop.sh
new file mode 100755
index 0000000..d49591e
--- /dev/null
+++ b/xos/observers/vbng/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f vbng-observer.py
diff --git a/xos/observers/vbng/supervisor/vbng-observer.conf b/xos/observers/vbng/supervisor/vbng-observer.conf
new file mode 100644
index 0000000..cff77b8
--- /dev/null
+++ b/xos/observers/vbng/supervisor/vbng-observer.conf
@@ -0,0 +1,2 @@
+[program:vbng-observer]
+command=python /opt/xos/observers/vbng/vbng-observer.py -C /opt/xos/observers/vbng/vbng_observer_config
diff --git a/xos/observers/vbng/vbng-observer.py b/xos/observers/vbng/vbng-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/vbng/vbng-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+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/vbng/vbng_observer_config b/xos/observers/vbng/vbng_observer_config
new file mode 100644
index 0000000..b75d498
--- /dev/null
+++ b/xos/observers/vbng/vbng_observer_config
@@ -0,0 +1,38 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=vbng
+dependency_graph=/opt/xos/observers/vbng/model-deps
+steps_dir=/opt/xos/observers/vbng/steps
+sys_dir=/opt/xos/observers/vbng/sys
+deleters_dir=/opt/xos/observers/vbng/deleters
+log_file=console
+#/var/log/hpc.log
+driver=None
+pretend=False
+backoff_disabled=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/vcpe/broadbandshield.py b/xos/observers/vcpe/broadbandshield.py
new file mode 100644
index 0000000..dd2f00b
--- /dev/null
+++ b/xos/observers/vcpe/broadbandshield.py
@@ -0,0 +1,396 @@
+import requests
+import logging
+import json
+import sys
+from rest_framework.exceptions import APIException
+
+""" format of settings
+
+ ["settings"]
+ ["watershed"]
+ ["rating"]
+ ["categories"]
+ ["blocklist"]
+ ["allowlist"]
+
+ ["users"]
+ array
+ ["account_id"] - 58
+ ["reporting"] - False
+ ["name"] - Scott1
+ ["devices"]
+ ["settings"] -
+ ["watershed"]
+ ["rating"]
+ ["categories"]
+ ["blocklist"]
+ ["allowlist"]
+
+ ["devices"]
+ array
+ ["username"] - "Scott1" or "" if whole-house
+ ["uuid"] - empty
+ ["mac_address"] - mac address as hex digits in ascii
+ ["type"] - "laptop"
+ ["name"] - human readable name of device ("Scott's laptop")
+ ["settings"]
+ ["watershed"]
+ array
+ array
+ ["rating"]
+ ["category"]
+ ["rating"] - ["G" | "NONE"]
+ ["categories"] - list of categories set by rating
+ ["blocklist"] - []
+ ["allowlist"] - []
+"""
+
+class BBS_Failure(APIException):
+ status_code=400
+ def __init__(self, why="broadbandshield error", fields={}):
+ APIException.__init__(self, {"error": "BBS_Failure",
+ "specific_error": why,
+ "fields": fields})
+
+
+class BBS:
+ level_map = {"PG_13": "PG13",
+ "NONE": "OFF",
+ "ALL": "NONE",
+ None: "NONE"}
+
+ def __init__(self, username, password, bbs_hostname=None, bbs_port=None):
+ self.username = username
+ self.password = password
+
+ # XXX not tested on port 80
+ #self.bbs_hostname = "www.broadbandshield.com"
+ #self.bbs_port = 80
+
+ if not bbs_hostname:
+ bbs_hostname = "cordcompute01.onlab.us"
+ if not bbs_port:
+ bbs_port = 8018
+
+ self.bbs_hostname = bbs_hostname
+ self.bbs_port = int(bbs_port)
+
+ self.api = "http://%s:%d/api" % (self.bbs_hostname, self.bbs_port)
+ self.nic_update = "http://%s:%d/nic/update" % (self.bbs_hostname, self.bbs_port)
+
+ self.session = None
+ self.settings = None
+
+ def login(self):
+ self.session = requests.Session()
+ r = self.session.post(self.api + "/login", data = json.dumps({"email": self.username, "password": self.password}))
+ if (r.status_code != 200):
+ raise BBS_Failure("Failed to login (%d)" % r.status_code)
+
+ def get_account(self):
+ if not self.session:
+ self.login()
+
+ r = self.session.get(self.api + "/account")
+ if (r.status_code != 200):
+ raise BBS_Failure("Failed to get account settings (%d)" % r.status_code)
+ self.settings = r.json()
+
+ return self.settings
+
+ def post_account(self):
+ if not self.settings:
+ raise XOSProgrammingError("no settings to post")
+
+ r = self.session.post(self.api + "/account/settings", data= json.dumps(self.settings))
+ if (r.status_code != 200):
+ raise BBS_Failure("Failed to set account settings (%d)" % r.status_code)
+
+ def add_device(self, name, mac, type="tablet", username=""):
+ data = {"name": name, "mac_address": mac, "type": type, "username": username}
+ r = self.session.post(self.api + "/device", data = json.dumps(data))
+ if (r.status_code != 200):
+ raise BBS_Failure("Failed to add device (%d)" % r.status_code)
+
+ def delete_device(self, data):
+ r = self.session.delete(self.api + "/device", data = json.dumps(data))
+ if (r.status_code != 200):
+ raise BBS_Failure("Failed to delete device (%d)" % r.status_code)
+
+ def add_user(self, name, rating="NONE", categories=[]):
+ data = {"name": name, "settings": {"rating": rating, "categories": categories}}
+ r = self.session.post(self.api + "/users", data = json.dumps(data))
+ if (r.status_code != 200):
+ raise BBS_Failure("Failed to add user (%d)" % r.status_code)
+
+ def delete_user(self, data):
+ r = self.session.delete(self.api + "/users", data = json.dumps(data))
+ if (r.status_code != 200):
+ raise BBS_Failure("Failed to delete user (%d)" % r.status_code)
+
+ def clear_users_and_devices(self):
+ if not self.settings:
+ self.get_account()
+
+ for device in self.settings["devices"]:
+ self.delete_device(device)
+
+ for user in self.settings["users"]:
+ self.delete_user(user)
+
+ def get_whole_home_level(self):
+ if not self.settings:
+ self.get_account()
+
+ return self.settings["settings"]["rating"]
+
+ def sync(self, whole_home_level, users):
+ if not self.settings:
+ self.get_account()
+
+ vcpe_users = {}
+ for user in users:
+ user = user.copy()
+ user["level"] = self.level_map.get(user["level"], user["level"])
+ user["mac"] = user.get("mac", "")
+ vcpe_users[user["name"]] = user
+
+ whole_home_level = self.level_map.get(whole_home_level, whole_home_level)
+
+ if (whole_home_level != self.settings["settings"]["rating"]):
+ print "*** set whole_home", whole_home_level, "***"
+ self.settings["settings"]["rating"] = whole_home_level
+ self.post_account()
+
+ bbs_usernames = [bbs_user["name"] for bbs_user in self.settings["users"]]
+ bbs_devicenames = [bbs_device["name"] for bbs_device in self.settings["devices"]]
+
+ add_users = []
+ add_devices = []
+ delete_users = []
+ delete_devices = []
+
+ for bbs_user in self.settings["users"]:
+ bbs_username = bbs_user["name"]
+ if bbs_username in vcpe_users.keys():
+ vcpe_user = vcpe_users[bbs_username]
+ if bbs_user["settings"]["rating"] != vcpe_user["level"]:
+ print "set user", vcpe_user["name"], "rating", vcpe_user["level"]
+ #bbs_user["settings"]["rating"] = vcpe_user["level"]
+ # add can be used as an update
+ add_users.append(vcpe_user)
+ else:
+ delete_users.append(bbs_user)
+
+ for bbs_device in self.settings["devices"]:
+ bbs_devicename = bbs_device["name"]
+ if bbs_devicename in vcpe_users.keys():
+ vcpe_user = vcpe_users[bbs_devicename]
+ if bbs_device["mac_address"] != vcpe_user["mac"]:
+ print "set device", vcpe_user["name"], "mac", vcpe_user["mac"]
+ #bbs_device["mac_address"] = vcpe_user["mac"]
+ # add of a device can't be used as an update, as you'll end
+ # up with two of them.
+ delete_devices.append(bbs_device)
+ add_devices.append(vcpe_user)
+ else:
+ delete_devices.append(bbs_device)
+
+ for (username, user) in vcpe_users.iteritems():
+ if not username in bbs_usernames:
+ add_users.append(user)
+ if not username in bbs_devicenames:
+ add_devices.append(user)
+
+ for bbs_user in delete_users:
+ print "delete user", bbs_user["name"]
+ self.delete_user(bbs_user)
+
+ for bbs_device in delete_devices:
+ print "delete device", bbs_device["name"]
+ self.delete_device(bbs_device)
+
+ for vcpe_user in add_users:
+ print "add user", vcpe_user["name"], "level", vcpe_user["level"]
+ self.add_user(vcpe_user["name"], vcpe_user["level"])
+
+ for vcpe_user in add_devices:
+ print "add device", vcpe_user["name"], "mac", vcpe_user["mac"]
+ self.add_device(vcpe_user["name"], vcpe_user["mac"], "tablet", vcpe_user["name"])
+
+ def get_whole_home_rating(self):
+ return self.settings["settings"]["rating"]
+
+ def get_user(self, name):
+ for user in self.settings["users"]:
+ if user["name"]==name:
+ return user
+ return None
+
+ def get_device(self, name):
+ for device in self.settings["devices"]:
+ if device["name"]==name:
+ return device
+ return None
+
+ def dump(self):
+ if not self.settings:
+ self.get_account()
+
+ print "whole_home_rating:", self.settings["settings"]["rating"]
+ print "users:"
+ for user in self.settings["users"]:
+ print " user", user["name"], "rating", user["settings"]["rating"]
+
+ print "devices:"
+ for device in self.settings["devices"]:
+ print " device", device["name"], "user", device["username"], "rating", device["settings"]["rating"], "mac", device["mac_address"]
+
+ def associate(self, ip):
+ bbs_hostname = "cordcompute01.onlab.us"
+ r = requests.get(self.nic_update, params={"hostname": "onlab.us"}, headers={"X-Forwarded-For": ip}, auth=requests.auth.HTTPBasicAuth(self.username,self.password))
+ if (r.status_code != 200):
+ raise BBS_Failure("Failed to associate account with ip (%d)" % r.status_code)
+
+def dump():
+ bbs = BBS(sys.argv[2], sys.argv[3])
+ bbs.dump()
+
+def associate():
+ if len(sys.argv)<5:
+ print "you need to specify IP address"
+ sys.exit(-1)
+
+ bbs = BBS(sys.argv[2], sys.argv[3])
+ bbs.associate(sys.argv[4])
+
+def self_test():
+ bbs = BBS(sys.argv[2], sys.argv[3])
+
+ print "*** initial ***"
+ bbs.dump()
+
+ open("bbs.json","w").write(json.dumps(bbs.settings))
+
+ # a new BBS account will throw a 500 error if it has no rating
+ bbs.settings["settings"]["rating"] = "R"
+ #bbs.settings["settings"]["category"] = [u'PORNOGRAPHY', u'ADULT', u'ILLEGAL', u'WEAPONS', u'DRUGS', u'GAMBLING', u'CYBERBULLY', u'ANONYMIZERS', u'SUICIDE', u'MALWARE']
+ #bbs.settings["settings"]["blocklist"] = []
+ #bbs.settings["settings"]["allowlist"] = []
+ #for water in bbs.settings["settings"]["watershed"];
+ # water["categories"]=[]
+ # delete everything
+ bbs.post_account()
+ bbs.clear_users_and_devices()
+
+ print "*** cleared ***"
+ bbs.settings=None
+ bbs.dump()
+
+ users = [{"name": "Moms pc", "level": "R", "mac": "010203040506"},
+ {"name": "Dads pc", "level": "R", "mac": "010203040507"},
+ {"name": "Jacks ipad", "level": "PG", "mac": "010203040508"},
+ {"name": "Jills iphone", "level": "G", "mac": "010203040509"}]
+
+ print "*** syncing mom-R, Dad-R, jack-PG, Jill-G, wholehome-PG-13 ***"
+
+ bbs.settings = None
+ bbs.sync("PG-13", users)
+
+ print "*** after sync ***"
+ bbs.settings=None
+ bbs.dump()
+ assert(bbs.get_whole_home_rating() == "PG-13")
+ assert(bbs.get_user("Moms pc")["settings"]["rating"] == "R")
+ assert(bbs.get_user("Dads pc")["settings"]["rating"] == "R")
+ assert(bbs.get_user("Jacks ipad")["settings"]["rating"] == "PG")
+ assert(bbs.get_user("Jills iphone")["settings"]["rating"] == "G")
+ assert(bbs.get_device("Moms pc")["mac_address"] == "010203040506")
+ assert(bbs.get_device("Dads pc")["mac_address"] == "010203040507")
+ assert(bbs.get_device("Jacks ipad")["mac_address"] == "010203040508")
+ assert(bbs.get_device("Jills iphone")["mac_address"] == "010203040509")
+
+ print "*** update whole home level ***"
+ bbs.settings=None
+ bbs.get_account()
+ bbs.settings["settings"]["rating"] = "PG"
+ bbs.post_account()
+
+ print "*** after sync ***"
+ bbs.settings=None
+ bbs.dump()
+ assert(bbs.get_whole_home_rating() == "PG")
+ assert(bbs.get_user("Moms pc")["settings"]["rating"] == "R")
+ assert(bbs.get_user("Dads pc")["settings"]["rating"] == "R")
+ assert(bbs.get_user("Jacks ipad")["settings"]["rating"] == "PG")
+ assert(bbs.get_user("Jills iphone")["settings"]["rating"] == "G")
+ assert(bbs.get_device("Moms pc")["mac_address"] == "010203040506")
+ assert(bbs.get_device("Dads pc")["mac_address"] == "010203040507")
+ assert(bbs.get_device("Jacks ipad")["mac_address"] == "010203040508")
+ assert(bbs.get_device("Jills iphone")["mac_address"] == "010203040509")
+
+ print "*** delete dad, change moms IP, change jills level to PG, change whole home to PG-13 ***"
+ users = [{"name": "Moms pc", "level": "R", "mac": "010203040511"},
+ {"name": "Jacks ipad", "level": "PG", "mac": "010203040508"},
+ {"name": "Jills iphone", "level": "PG", "mac": "010203040509"}]
+
+ bbs.settings = None
+ bbs.sync("PG-13", users)
+
+ print "*** after sync ***"
+ bbs.settings=None
+ bbs.dump()
+ assert(bbs.get_whole_home_rating() == "PG-13")
+ assert(bbs.get_user("Moms pc")["settings"]["rating"] == "R")
+ assert(bbs.get_user("Dads pc") == None)
+ assert(bbs.get_user("Jacks ipad")["settings"]["rating"] == "PG")
+ assert(bbs.get_user("Jills iphone")["settings"]["rating"] == "PG")
+ assert(bbs.get_device("Moms pc")["mac_address"] == "010203040511")
+ assert(bbs.get_device("Dads pc") == None)
+ assert(bbs.get_device("Jacks ipad")["mac_address"] == "010203040508")
+
+ print "add dad's laptop"
+ users = [{"name": "Moms pc", "level": "R", "mac": "010203040511"},
+ {"name": "Dads laptop", "level": "PG-13", "mac": "010203040512"},
+ {"name": "Jacks ipad", "level": "PG", "mac": "010203040508"},
+ {"name": "Jills iphone", "level": "PG", "mac": "010203040509"}]
+
+ bbs.settings = None
+ bbs.sync("PG-13", users)
+
+ print "*** after sync ***"
+ bbs.settings=None
+ bbs.dump()
+ assert(bbs.get_whole_home_rating() == "PG-13")
+ assert(bbs.get_user("Moms pc")["settings"]["rating"] == "R")
+ assert(bbs.get_user("Dads pc") == None)
+ assert(bbs.get_user("Dads laptop")["settings"]["rating"] == "PG-13")
+ assert(bbs.get_user("Jacks ipad")["settings"]["rating"] == "PG")
+ assert(bbs.get_user("Jills iphone")["settings"]["rating"] == "PG")
+ assert(bbs.get_device("Moms pc")["mac_address"] == "010203040511")
+ assert(bbs.get_device("Dads pc") == None)
+ assert(bbs.get_device("Dads laptop")["mac_address"] == "010203040512")
+ assert(bbs.get_device("Jacks ipad")["mac_address"] == "010203040508")
+
+ #bbs.add_user("tom", "G", [u'PORNOGRAPHY', u'ADULT', u'ILLEGAL', u'WEAPONS', u'DRUGS', u'GAMBLING', u'SOCIAL', u'CYBERBULLY', u'GAMES', u'ANONYMIZERS', u'SUICIDE', u'MALWARE'])
+ #bbs.add_device(name="tom's iphone", mac="010203040506", type="tablet", username="tom")
+
+def main():
+ if len(sys.argv)<4:
+ print "syntax: broadbandshield.py <operation> <email> <password>"
+ print " operation = [dump | selftest | assocate"
+ sys.exit(-1)
+
+ operation = sys.argv[1]
+
+ if operation=="dump":
+ dump()
+ elif operation=="selftest":
+ self_test()
+ elif operation=="associate":
+ associate()
+
+if __name__ == "__main__":
+ main()
+
+
diff --git a/xos/observers/vcpe/files/docker.list b/xos/observers/vcpe/files/docker.list
new file mode 100644
index 0000000..0ee9ae0
--- /dev/null
+++ b/xos/observers/vcpe/files/docker.list
@@ -0,0 +1 @@
+deb https://get.docker.com/ubuntu docker main
diff --git a/xos/observers/vcpe/files/etc/rc.local b/xos/observers/vcpe/files/etc/rc.local
new file mode 100755
index 0000000..24c8f8d
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/rc.local
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+#
+# rc.local
+#
+# This script is executed at the end of each multiuser runlevel.
+# Make sure that the script will "exit 0" on success or any other
+# value on error.
+#
+# In order to enable or disable this script just change the execution
+# bits.
+#
+# By default this script does nothing.
+
+ufw enable
+ufw allow bootps
+ufw allow from 192.168.0.0/24
+ufw route allow in on eth1 out on eth0
+ufw route allow in on eth1 out on eth2
+
+# service dnsmasq start
+
+exit 0
diff --git a/xos/observers/vcpe/files/etc/ufw/after.init b/xos/observers/vcpe/files/etc/ufw/after.init
new file mode 100644
index 0000000..e89217d
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/after.init
@@ -0,0 +1,40 @@
+#!/bin/sh
+#
+# after.init: if executable, called by ufw-init. See 'man ufw-framework' for
+# details. Note that output from these scripts is not seen via the
+# the ufw command, but instead via ufw-init.
+#
+# Copyright 2013 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3,
+# as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+set -e
+
+case "$1" in
+start)
+ # typically required
+ ;;
+stop)
+ # typically required
+ ;;
+status)
+ # optional
+ ;;
+flush-all)
+ # optional
+ ;;
+*)
+ echo "'$1' not supported"
+ echo "Usage: after.init {start|stop|flush-all|status}"
+ ;;
+esac
diff --git a/xos/observers/vcpe/files/etc/ufw/after.rules b/xos/observers/vcpe/files/etc/ufw/after.rules
new file mode 100644
index 0000000..0d6c646
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/after.rules
@@ -0,0 +1,30 @@
+#
+# rules.input-after
+#
+# Rules that should be run after the ufw command line added rules. Custom
+# rules should be added to one of these chains:
+# ufw-after-input
+# ufw-after-output
+# ufw-after-forward
+#
+
+# Don't delete these required lines, otherwise there will be errors
+*filter
+:ufw-after-input - [0:0]
+:ufw-after-output - [0:0]
+:ufw-after-forward - [0:0]
+# End required lines
+
+# don't log noisy services by default
+-A ufw-after-input -p udp --dport 137 -j ufw-skip-to-policy-input
+-A ufw-after-input -p udp --dport 138 -j ufw-skip-to-policy-input
+-A ufw-after-input -p tcp --dport 139 -j ufw-skip-to-policy-input
+-A ufw-after-input -p tcp --dport 445 -j ufw-skip-to-policy-input
+-A ufw-after-input -p udp --dport 67 -j ufw-skip-to-policy-input
+-A ufw-after-input -p udp --dport 68 -j ufw-skip-to-policy-input
+
+# don't log noisy broadcast
+-A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input
+
+# don't delete the 'COMMIT' line or these rules won't be processed
+COMMIT
diff --git a/xos/observers/vcpe/files/etc/ufw/after6.rules b/xos/observers/vcpe/files/etc/ufw/after6.rules
new file mode 100644
index 0000000..0d99672
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/after6.rules
@@ -0,0 +1,27 @@
+#
+# rules.input-after
+#
+# Rules that should be run after the ufw command line added rules. Custom
+# rules should be added to one of these chains:
+# ufw6-after-input
+# ufw6-after-output
+# ufw6-after-forward
+#
+
+# Don't delete these required lines, otherwise there will be errors
+*filter
+:ufw6-after-input - [0:0]
+:ufw6-after-output - [0:0]
+:ufw6-after-forward - [0:0]
+# End required lines
+
+# don't log noisy services by default
+-A ufw6-after-input -p udp --dport 137 -j ufw6-skip-to-policy-input
+-A ufw6-after-input -p udp --dport 138 -j ufw6-skip-to-policy-input
+-A ufw6-after-input -p tcp --dport 139 -j ufw6-skip-to-policy-input
+-A ufw6-after-input -p tcp --dport 445 -j ufw6-skip-to-policy-input
+-A ufw6-after-input -p udp --dport 546 -j ufw6-skip-to-policy-input
+-A ufw6-after-input -p udp --dport 547 -j ufw6-skip-to-policy-input
+
+# don't delete the 'COMMIT' line or these rules won't be processed
+COMMIT
diff --git a/xos/observers/vcpe/files/etc/ufw/applications.d/openssh-server b/xos/observers/vcpe/files/etc/ufw/applications.d/openssh-server
new file mode 100644
index 0000000..9bbe906
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/applications.d/openssh-server
@@ -0,0 +1,4 @@
+[OpenSSH]
+title=Secure shell server, an rshd replacement
+description=OpenSSH is a free implementation of the Secure Shell protocol.
+ports=22/tcp
diff --git a/xos/observers/vcpe/files/etc/ufw/before.init b/xos/observers/vcpe/files/etc/ufw/before.init
new file mode 100644
index 0000000..1348cb1
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/before.init
@@ -0,0 +1,40 @@
+#!/bin/sh
+#
+# before.init: if executable, called by ufw-init. See 'man ufw-framework' for
+# details. Note that output from these scripts is not seen via the
+# the ufw command, but instead via ufw-init.
+#
+# Copyright 2013 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3,
+# as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+set -e
+
+case "$1" in
+start)
+ # typically required
+ ;;
+stop)
+ # typically required
+ ;;
+status)
+ # optional
+ ;;
+flush-all)
+ # optional
+ ;;
+*)
+ echo "'$1' not supported"
+ echo "Usage: before.init {start|stop|flush-all|status}"
+ ;;
+esac
diff --git a/xos/observers/vcpe/files/etc/ufw/before6.rules b/xos/observers/vcpe/files/etc/ufw/before6.rules
new file mode 100644
index 0000000..0b26ed8
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/before6.rules
@@ -0,0 +1,73 @@
+#
+# rules.before
+#
+# Rules that should be run before the ufw command line added rules. Custom
+# rules should be added to one of these chains:
+# ufw6-before-input
+# ufw6-before-output
+# ufw6-before-forward
+#
+
+# Don't delete these required lines, otherwise there will be errors
+*filter
+:ufw6-before-input - [0:0]
+:ufw6-before-output - [0:0]
+:ufw6-before-forward - [0:0]
+# End required lines
+
+
+# allow all on loopback
+-A ufw6-before-input -i lo -j ACCEPT
+-A ufw6-before-output -o lo -j ACCEPT
+
+# drop packets with RH0 headers
+-A ufw6-before-input -m rt --rt-type 0 -j DROP
+-A ufw6-before-forward -m rt --rt-type 0 -j DROP
+-A ufw6-before-output -m rt --rt-type 0 -j DROP
+
+# for stateless autoconfiguration (restrict NDP messages to hop limit of 255)
+-A ufw6-before-input -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT
+-A ufw6-before-output -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT
+-A ufw6-before-output -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type router-solicitation -m hl --hl-eq 255 -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT
+
+# quickly process packets for which we already have a connection
+-A ufw6-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+-A ufw6-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+-A ufw6-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+
+# for multicast ping replies from link-local addresses (these don't have an
+# associated connection and would otherwise be marked INVALID)
+-A ufw6-before-input -p icmpv6 --icmpv6-type echo-reply -s fe80::/10 -j ACCEPT
+
+# drop INVALID packets (logs these in loglevel medium and higher)
+-A ufw6-before-input -m conntrack --ctstate INVALID -j ufw6-logging-deny
+-A ufw6-before-input -m conntrack --ctstate INVALID -j DROP
+
+# ok icmp codes for INPUT
+-A ufw6-before-input -p icmpv6 --icmpv6-type destination-unreachable -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type packet-too-big -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type time-exceeded -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type parameter-problem -j ACCEPT
+-A ufw6-before-input -p icmpv6 --icmpv6-type echo-request -j ACCEPT
+
+# ok icmp code for FORWARD
+-A ufw6-before-forward -p icmpv6 --icmpv6-type destination-unreachable -j ACCEPT
+-A ufw6-before-forward -p icmpv6 --icmpv6-type packet-too-big -j ACCEPT
+-A ufw6-before-forward -p icmpv6 --icmpv6-type time-exceeded -j ACCEPT
+-A ufw6-before-forward -p icmpv6 --icmpv6-type parameter-problem -j ACCEPT
+-A ufw6-before-forward -p icmpv6 --icmpv6-type echo-request -j ACCEPT
+
+# allow dhcp client to work
+-A ufw6-before-input -p udp -s fe80::/10 --sport 547 -d fe80::/10 --dport 546 -j ACCEPT
+
+# allow MULTICAST mDNS for service discovery
+-A ufw6-before-input -p udp -d ff02::fb --dport 5353 -j ACCEPT
+
+# allow MULTICAST UPnP for service discovery
+-A ufw6-before-input -p udp -d ff02::f --dport 1900 -j ACCEPT
+
+# don't delete the 'COMMIT' line or these rules won't be processed
+COMMIT
diff --git a/xos/observers/vcpe/files/etc/ufw/sysctl.conf b/xos/observers/vcpe/files/etc/ufw/sysctl.conf
new file mode 100644
index 0000000..8707032
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/sysctl.conf
@@ -0,0 +1,57 @@
+#
+# Configuration file for setting network variables. Please note these settings
+# override /etc/sysctl.conf. If you prefer to use /etc/sysctl.conf, please
+# adjust IPT_SYSCTL in /etc/default/ufw.
+#
+
+# Uncomment this to allow this host to route packets between interfaces
+#net/ipv4/ip_forward=1
+#net/ipv6/conf/default/forwarding=1
+#net/ipv6/conf/all/forwarding=1
+
+# Turn on Source Address Verification in all interfaces to prevent some
+# spoofing attacks
+net/ipv4/conf/default/rp_filter=1
+net/ipv4/conf/all/rp_filter=1
+
+# Do not accept IP source route packets (we are not a router)
+net/ipv4/conf/default/accept_source_route=0
+net/ipv4/conf/all/accept_source_route=0
+net/ipv6/conf/default/accept_source_route=0
+net/ipv6/conf/all/accept_source_route=0
+
+# Disable ICMP redirects. ICMP redirects are rarely used but can be used in
+# MITM (man-in-the-middle) attacks. Disabling ICMP may disrupt legitimate
+# traffic to those sites.
+net/ipv4/conf/default/accept_redirects=0
+net/ipv4/conf/all/accept_redirects=0
+net/ipv6/conf/default/accept_redirects=0
+net/ipv6/conf/all/accept_redirects=0
+
+# Ignore bogus ICMP errors
+net/ipv4/icmp_echo_ignore_broadcasts=1
+net/ipv4/icmp_ignore_bogus_error_responses=1
+net/ipv4/icmp_echo_ignore_all=0
+
+# Don't log Martian Packets (impossible packets)
+net/ipv4/conf/default/log_martians=0
+net/ipv4/conf/all/log_martians=0
+
+# Change to '1' to enable TCP/IP SYN cookies This disables TCP Window Scaling
+# (http://lkml.org/lkml/2008/2/5/167)
+net/ipv4/tcp_syncookies=0
+
+#net/ipv4/tcp_fin_timeout=30
+#net/ipv4/tcp_keepalive_intvl=1800
+
+# normally allowing tcp_sack is ok, but if going through OpenBSD 3.8 RELEASE or
+# earlier pf firewall, should set this to 0
+net/ipv4/tcp_sack=1
+
+# Uncomment this to turn off ipv6 autoconfiguration
+#net/ipv6/conf/default/autoconf=0
+#net/ipv6/conf/all/autoconf=0
+
+# Uncomment this to enable ipv6 privacy addressing
+#net/ipv6/conf/default/use_tempaddr=2
+#net/ipv6/conf/all/use_tempaddr=2
diff --git a/xos/observers/vcpe/files/etc/ufw/ufw.conf b/xos/observers/vcpe/files/etc/ufw/ufw.conf
new file mode 100644
index 0000000..28fe534
--- /dev/null
+++ b/xos/observers/vcpe/files/etc/ufw/ufw.conf
@@ -0,0 +1,10 @@
+# /etc/ufw/ufw.conf
+#
+
+# Set to yes to start on boot. If setting this remotely, be sure to add a rule
+# to allow your remote connection before starting ufw. Eg: 'ufw allow 22/tcp'
+ENABLED=yes
+
+# Please use the 'ufw' command to set the loglevel. Eg: 'ufw logging medium'.
+# See 'man ufw' for details.
+LOGLEVEL=low
diff --git a/xos/observers/vcpe/files/vcpe.conf b/xos/observers/vcpe/files/vcpe.conf
new file mode 100644
index 0000000..752c57a
--- /dev/null
+++ b/xos/observers/vcpe/files/vcpe.conf
@@ -0,0 +1,10 @@
+# Upstart script for vCPE
+description "vCPE container"
+author "andy@onlab.us"
+start on filesystem and started docker
+stop on runlevel [!2345]
+respawn
+
+script
+ /usr/local/sbin/start-vcpe.sh
+end script
diff --git a/xos/observers/vcpe/files/vcpe.dnsmasq b/xos/observers/vcpe/files/vcpe.dnsmasq
new file mode 100644
index 0000000..2b2687b
--- /dev/null
+++ b/xos/observers/vcpe/files/vcpe.dnsmasq
@@ -0,0 +1,2 @@
+listen-address=192.168.0.1
+dhcp-range=192.168.0.2,192.168.0.254,6
diff --git a/xos/observers/vcpe/files/vm-resolv.conf b/xos/observers/vcpe/files/vm-resolv.conf
new file mode 100644
index 0000000..cae093a
--- /dev/null
+++ b/xos/observers/vcpe/files/vm-resolv.conf
@@ -0,0 +1 @@
+nameserver 8.8.8.8
diff --git a/xos/observers/vcpe/model-deps b/xos/observers/vcpe/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/observers/vcpe/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/observers/vcpe/observer_ansible_test.py b/xos/observers/vcpe/observer_ansible_test.py
new file mode 100644
index 0000000..b28da63
--- /dev/null
+++ b/xos/observers/vcpe/observer_ansible_test.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+import os
+import sys
+
+sys.path.append("../..")
+import synchronizers.base.ansible
+
+print sys.argv
+
+private_key="""-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAtJiuarud5S4Y2quDeWyaS0UCQGQtfuSzzNhplFwujYnJGL65
+e14REtv+UuHGymyr/SfkTrBd8vH5NI2UZ/4sZW13ieI/1d97OeVe2+ct0Y4BaFEI
+3Hja6DIpsY3Q2cBQsWUwcQzbMIF9jIq8SzwR1zk8UtZi09fNxqjCchRPlBvbiLKX
+g0/yirN237WbaKzK++8EPy3nuv83216MXHFFSjuxfICe/RhjaqMzVp7isSbv1suU
+geyvNNzU71c/K13gTggdcIXeRQBiJYio2Sn3h2nsV6AFqFH4fjERxWG55Q4e3jeE
+tWM/Dw+hqYKg/25UcmM862a6sUmuDCmM5S3VEQIDAQABAoIBACH88iZoNOki6c6N
+pOq/Q7KSxD+2GtHc3PHacNRZHPHKUqxziJjtNS1lddHHaBBEr4GvdkpQ6v2ECLCZ
+TKrdrSFRnsO2bukjbB+TSWz9byQXI7CsP4yuuhQlDK+7zuiMRyN7tcgw8TeJx0Uh
+/xnxrjHhNbcpXeQcoz+WFzI9HFT1MEGmMS4Lyp/zLB/pmfY9h7V9d+EeRZDi78jq
+Vir6MI6iCTa0T02dvHUFOg+wXLb0nb8V1xKDL+6cAJla7LzwoG8lTnvp5DSYCojI
+5JrILYafeO8RbBV2GWmaE5mkHgeBkFZ+qZQ7K0MjR30Yh6tajB7P3+F/Max8FUgW
+xLHr8AECgYEA2+o0ge3HtZcepEFBKKYnLTwoEpPCfLElWZHzUJYDz259s4JLsfak
+tROANFEdsJUjpmWG52MCL+bgKFFOedDkt4p1jgcIneaHk0jvoU11wG7W3jZZVy1q
+WjQNH5vDU+hg5tm/CREwm7lbUxR9Xuj9K63CNAAGp8KO7h2tOH8woIECgYEA0jrb
+LUg30RxO3+vrq9dUYohrDRisk5zKXuRLfxRA+E+ruvZ7CctG2OpM+658/qZM/w95
+7pOj6zz3//w7tAvH9erY+JOISnzaYKx04sYC1MfbFiFkq5j0gpuYm/MULDYNvFqr
+NU2Buj4dW+ZB+SeficsQOqm5QeNxh1kgiDCs7JECgYEAjSLGCAzeesA9vhTTCI95
+3SIaZbHGw9e8rLtqeHGOiHXU3nvksJYmJsAZK3pTn5xXgNbvuVhlcvCtM7LatntG
+DjUiNMB22z+0CuZoRBE+XP3FkF84/yX6d2Goenyw4wzkA8QDQoJxu789yRgBTgQh
+VwLw/AZ4PvoyWMdbAENApgECgYEAvFikosYP09XTyIPaKaOKY5iqqBoSC1GucSOB
+jAG+T3k5dxB6nQS0nYQUomvqak7drqnT6O33Lrr5ySrW5nCjnmvgJZwvv+Rp1bDM
+K5uRT8caPpJ+Wcp4TUdPi3BVA2MOHVDyEJg3AH/D1+DL/IgGQ/JcwOHsKt61iLhO
+EBXj5zECgYEAk+HuwksUPkSxg/AiJGbapGDK6XGymEUzo2duWlnofRqGcZ3NT3bB
+/kDI1KxQdlpODXSi4/BuTpbQiFOrzcEq5e5ytoMxlCHh3Fl3Jxl+JlgO21vAUvP6
+4SET7Q/6LxmfBlCVRg0dXDwcfJLgbnWxyvprIcz4e0FSFVZTBs/6tFk=
+-----END RSA PRIVATE KEY-----
+"""
+
+observer.ansible.run_template_ssh("test.yaml",
+ {"instance_name": "onlab_test405-378",
+ "instance_id": "instance-0000004d",
+ "hostname": "node67.washington.vicci.org",
+ "private_key": private_key})
+
diff --git a/xos/observers/vcpe/run.sh b/xos/observers/vcpe/run.sh
new file mode 100755
index 0000000..f180e66
--- /dev/null
+++ b/xos/observers/vcpe/run.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vcpe-observer.py ]]; then
+# ln -s ../../xos-observer.py vcpe-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+python vcpe-observer.py -C $XOS_DIR/observers/vcpe/vcpe_observer_config
diff --git a/xos/observers/vcpe/start-bbs.sh b/xos/observers/vcpe/start-bbs.sh
new file mode 100755
index 0000000..c8ee147
--- /dev/null
+++ b/xos/observers/vcpe/start-bbs.sh
@@ -0,0 +1,14 @@
+#! /bin/bash
+
+# put this in /opt/xerocole/start-bbs.sh
+# make sure it's executable
+# set it up in crontab
+# @reboot /opt/xerocole/start-bbs.sh
+
+ulimit -n 200000
+cd /opt/xerocole/answerx
+/opt/xerocole/answerx/startStop checkconfig answerx
+/opt/xerocole/answerx/startStop start answerx
+cd /opt/xerocole/namecontrols
+nohup /opt/xerocole/namecontrols/broadbandshield &
+nohup socat TCP-LISTEN:80,bind=0.0.0.0,fork TCP4:127.0.0.1:8018 &
diff --git a/xos/observers/vcpe/start.sh b/xos/observers/vcpe/start.sh
new file mode 100755
index 0000000..b402e5d
--- /dev/null
+++ b/xos/observers/vcpe/start.sh
@@ -0,0 +1,6 @@
+#if [[ ! -e ./vcpe-observer.py ]]; then
+# ln -s ../../xos-observer.py vcpe-observer.py
+#fi
+
+export XOS_DIR=/opt/xos
+nohup python vcpe-observer.py -C $XOS_DIR/observers/vcpe/vcpe_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/vcpe/steps/ansible_test/README b/xos/observers/vcpe/steps/ansible_test/README
new file mode 100644
index 0000000..d3b2c54
--- /dev/null
+++ b/xos/observers/vcpe/steps/ansible_test/README
@@ -0,0 +1,4 @@
+Some scripts used while testing the Ansible instance configuraiton observer
+
+xos.py was probably the prototype of an XOS SSH module for Ansible, that understood how to SSH into the instances
+without needing to play config file and environment tricks.
diff --git a/xos/observers/vcpe/steps/ansible_test/inventory.txt b/xos/observers/vcpe/steps/ansible_test/inventory.txt
new file mode 100644
index 0000000..bd5b542
--- /dev/null
+++ b/xos/observers/vcpe/steps/ansible_test/inventory.txt
@@ -0,0 +1,16 @@
+[onlab_hpc-355]
+node67.washington.vicci.org instance_id=instance-00000045 instance_name=onlab_hpc-355
+
+[onlab_test405-372]
+node67.washington.vicci.org instance_id=instance-0000004c instance_name=onlab_test405-372
+
+[onlab_test405-376]
+node1.cs.arizona.edu
+
+[onlab_test405-378]
+node67.washington.vicci.org ansible_ssh_private_key_file=/home/smbaker/.ssh/id_rsa
+#/home/smbaker/projects/vicci/keys/test_service_key_rsa
+
+[mysite_test2-48]
+cordcompute02.onlab.us ansible_ssh_private_key_file=/home/smbaker/projects/vicci/keys/demo_admin.rsa
+
diff --git a/xos/observers/vcpe/steps/ansible_test/test.sh b/xos/observers/vcpe/steps/ansible_test/test.sh
new file mode 100755
index 0000000..157ba9c
--- /dev/null
+++ b/xos/observers/vcpe/steps/ansible_test/test.sh
@@ -0,0 +1,2 @@
+#! /bin/bash
+ansible-playbook --private-key /home/smbaker/.ssh/id_rsa -i ./inventory.txt test.yaml
diff --git a/xos/observers/vcpe/steps/ansible_test/test.yaml b/xos/observers/vcpe/steps/ansible_test/test.yaml
new file mode 100644
index 0000000..6a29d56
--- /dev/null
+++ b/xos/observers/vcpe/steps/ansible_test/test.yaml
@@ -0,0 +1,12 @@
+---
+- hosts: onlab_test405-372
+ connection: xos
+ user: ubuntu
+ vars:
+ foo: 25
+# instance_name: instance-00000045
+# slice_name: onlab_hpc-355
+
+ tasks:
+ - name: foobar
+ shell: echo foo > /tmp/foobar
diff --git a/xos/observers/vcpe/steps/ansible_test/xos.py b/xos/observers/vcpe/steps/ansible_test/xos.py
new file mode 100755
index 0000000..eb4f3eb
--- /dev/null
+++ b/xos/observers/vcpe/steps/ansible_test/xos.py
@@ -0,0 +1,444 @@
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import re
+import subprocess
+import shlex
+import pipes
+import random
+import select
+import fcntl
+import hmac
+import pwd
+import gettext
+import pty
+from hashlib import sha1
+import ansible.constants as C
+from ansible.callbacks import vvv
+from ansible import errors
+from ansible import utils
+
+class Connection(object):
+ ''' ssh based connections '''
+
+ def __init__(self, runner, host, port, user, password, private_key_file, *args, **kwargs):
+ self.runner = runner
+ self.host = host
+ self.ipv6 = ':' in self.host
+ self.port = port
+ self.user = str(user)
+ self.password = password
+ self.private_key_file = private_key_file
+ self.HASHED_KEY_MAGIC = "|1|"
+ self.has_pipelining = True
+ #self.instance_id = "instance-00000045" # C.get_config(C.p, "xos", "instance_id", "INSTANCE_ID", None)
+ #self.instance_name = "onlab_hpc-355" # C.get_config(C.p, "xos", "instance_name", "SLIVER_NAME", None)
+
+ inject={}
+ inject= utils.combine_vars(inject, self.runner.inventory.get_variables(self.host))
+
+ self.instance_id = inject["instance_id"]
+ self.instance_name = inject["instance_name"]
+
+ fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
+ self.cp_dir = utils.prepare_writeable_dir('$HOME/.ansible/cp',mode=0700)
+ fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
+
+ def connect(self):
+ ''' connect to the remote host '''
+
+ vvv("ESTABLISH CONNECTION FOR USER: %s" % self.user, host=self.host)
+
+ self.common_args = []
+ extra_args = C.ANSIBLE_SSH_ARGS
+ if extra_args is not None:
+ # make sure there is no empty string added as this can produce weird errors
+ self.common_args += [x.strip() for x in shlex.split(extra_args) if x.strip()]
+ else:
+ self.common_args += ["-o", "ControlMaster=auto",
+ "-o", "ControlPersist=60s",
+ "-o", "ControlPath=%s" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self.cp_dir))]
+
+ self.common_args += ["-o", "ProxyCommand ssh -q -i %s %s@%s" % (self.private_key_file, self.instance_id, self.host)]
+
+ cp_in_use = False
+ cp_path_set = False
+ for arg in self.common_args:
+ if "ControlPersist" in arg:
+ cp_in_use = True
+ if "ControlPath" in arg:
+ cp_path_set = True
+
+ if cp_in_use and not cp_path_set:
+ self.common_args += ["-o", "ControlPath=%s" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self.cp_dir))]
+
+ if not C.HOST_KEY_CHECKING:
+ self.common_args += ["-o", "StrictHostKeyChecking=no"]
+
+ if self.port is not None:
+ self.common_args += ["-o", "Port=%d" % (self.port)]
+ if self.private_key_file is not None:
+ self.common_args += ["-o", "IdentityFile=\"%s\"" % os.path.expanduser(self.private_key_file)]
+ elif self.runner.private_key_file is not None:
+ self.common_args += ["-o", "IdentityFile=\"%s\"" % os.path.expanduser(self.runner.private_key_file)]
+ if self.password:
+ self.common_args += ["-o", "GSSAPIAuthentication=no",
+ "-o", "PubkeyAuthentication=no"]
+ else:
+ self.common_args += ["-o", "KbdInteractiveAuthentication=no",
+ "-o", "PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey",
+ "-o", "PasswordAuthentication=no"]
+ if self.user != pwd.getpwuid(os.geteuid())[0]:
+ self.common_args += ["-o", "User="+self.user]
+ self.common_args += ["-o", "ConnectTimeout=%d" % self.runner.timeout]
+
+ return self
+
+ def _run(self, cmd, indata):
+ if indata:
+ # do not use pseudo-pty
+ p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdin = p.stdin
+ else:
+ # try to use upseudo-pty
+ try:
+ # Make sure stdin is a proper (pseudo) pty to avoid: tcgetattr errors
+ master, slave = pty.openpty()
+ p = subprocess.Popen(cmd, stdin=slave,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdin = os.fdopen(master, 'w', 0)
+ os.close(slave)
+ except:
+ p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdin = p.stdin
+
+ return (p, stdin)
+
+ def _password_cmd(self):
+ if self.password:
+ try:
+ p = subprocess.Popen(["sshpass"], stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ p.communicate()
+ except OSError:
+ raise errors.AnsibleError("to use the 'ssh' connection type with passwords, you must install the sshpass program")
+ (self.rfd, self.wfd) = os.pipe()
+ return ["sshpass", "-d%d" % self.rfd]
+ return []
+
+ def _send_password(self):
+ if self.password:
+ os.close(self.rfd)
+ os.write(self.wfd, "%s\n" % self.password)
+ os.close(self.wfd)
+
+ def _communicate(self, p, stdin, indata, su=False, sudoable=False, prompt=None):
+ fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
+ fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
+ # We can't use p.communicate here because the ControlMaster may have stdout open as well
+ stdout = ''
+ stderr = ''
+ rpipes = [p.stdout, p.stderr]
+ if indata:
+ try:
+ stdin.write(indata)
+ stdin.close()
+ except:
+ raise errors.AnsibleError('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
+ # Read stdout/stderr from process
+ while True:
+ rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
+
+ # fail early if the sudo/su password is wrong
+ if self.runner.sudo and sudoable:
+ if self.runner.sudo_pass:
+ incorrect_password = gettext.dgettext(
+ "sudo", "Sorry, try again.")
+ if stdout.endswith("%s\r\n%s" % (incorrect_password,
+ prompt)):
+ raise errors.AnsibleError('Incorrect sudo password')
+
+ if stdout.endswith(prompt):
+ raise errors.AnsibleError('Missing sudo password')
+
+ if self.runner.su and su and self.runner.su_pass:
+ incorrect_password = gettext.dgettext(
+ "su", "Sorry")
+ if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
+ raise errors.AnsibleError('Incorrect su password')
+
+ if p.stdout in rfd:
+ dat = os.read(p.stdout.fileno(), 9000)
+ stdout += dat
+ if dat == '':
+ rpipes.remove(p.stdout)
+ if p.stderr in rfd:
+ dat = os.read(p.stderr.fileno(), 9000)
+ stderr += dat
+ if dat == '':
+ rpipes.remove(p.stderr)
+ # only break out if no pipes are left to read or
+ # the pipes are completely read and
+ # the process is terminated
+ if (not rpipes or not rfd) and p.poll() is not None:
+ break
+ # No pipes are left to read but process is not yet terminated
+ # Only then it is safe to wait for the process to be finished
+ # NOTE: Actually p.poll() is always None here if rpipes is empty
+ elif not rpipes and p.poll() == None:
+ p.wait()
+ # The process is terminated. Since no pipes to read from are
+ # left, there is no need to call select() again.
+ break
+ # close stdin after process is terminated and stdout/stderr are read
+ # completely (see also issue #848)
+ stdin.close()
+ return (p.returncode, stdout, stderr)
+
+ def not_in_host_file(self, host):
+ if 'USER' in os.environ:
+ user_host_file = os.path.expandvars("~${USER}/.ssh/known_hosts")
+ else:
+ user_host_file = "~/.ssh/known_hosts"
+ user_host_file = os.path.expanduser(user_host_file)
+
+ host_file_list = []
+ host_file_list.append(user_host_file)
+ host_file_list.append("/etc/ssh/ssh_known_hosts")
+ host_file_list.append("/etc/ssh/ssh_known_hosts2")
+
+ hfiles_not_found = 0
+ for hf in host_file_list:
+ if not os.path.exists(hf):
+ hfiles_not_found += 1
+ continue
+ try:
+ host_fh = open(hf)
+ except IOError, e:
+ hfiles_not_found += 1
+ continue
+ else:
+ data = host_fh.read()
+ host_fh.close()
+
+ for line in data.split("\n"):
+ if line is None or " " not in line:
+ continue
+ tokens = line.split()
+ if tokens[0].find(self.HASHED_KEY_MAGIC) == 0:
+ # this is a hashed known host entry
+ try:
+ (kn_salt,kn_host) = tokens[0][len(self.HASHED_KEY_MAGIC):].split("|",2)
+ hash = hmac.new(kn_salt.decode('base64'), digestmod=sha1)
+ hash.update(host)
+ if hash.digest() == kn_host.decode('base64'):
+ return False
+ except:
+ # invalid hashed host key, skip it
+ continue
+ else:
+ # standard host file entry
+ if host in tokens[0]:
+ return False
+
+ if (hfiles_not_found == len(host_file_list)):
+ vvv("EXEC previous known host file not found for %s" % host)
+ return True
+
+ def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su_user=None, su=False):
+ ''' run a command on the remote host '''
+
+ ssh_cmd = self._password_cmd()
+ ssh_cmd += ["ssh", "-C"]
+ if not in_data:
+ # we can only use tty when we are not pipelining the modules. piping data into /usr/bin/python
+ # inside a tty automatically invokes the python interactive-mode but the modules are not
+ # compatible with the interactive-mode ("unexpected indent" mainly because of empty lines)
+ ssh_cmd += ["-tt"]
+ if utils.VERBOSITY > 3:
+ ssh_cmd += ["-vvv"]
+ else:
+ ssh_cmd += ["-q"]
+ ssh_cmd += self.common_args
+
+ if self.ipv6:
+ ssh_cmd += ['-6']
+ #ssh_cmd += [self.host]
+ ssh_cmd += [self.instance_name]
+
+ if su and su_user:
+ sudocmd, prompt, success_key = utils.make_su_cmd(su_user, executable, cmd)
+ prompt_re = re.compile(prompt)
+ ssh_cmd.append(sudocmd)
+ elif not self.runner.sudo or not sudoable:
+ prompt = None
+ if executable:
+ ssh_cmd.append(executable + ' -c ' + pipes.quote(cmd))
+ else:
+ ssh_cmd.append(cmd)
+ else:
+ sudocmd, prompt, success_key = utils.make_sudo_cmd(sudo_user, executable, cmd)
+ ssh_cmd.append(sudocmd)
+
+ vvv("EXEC %s" % ssh_cmd, host=self.host)
+
+ not_in_host_file = self.not_in_host_file(self.host)
+
+ if C.HOST_KEY_CHECKING and not_in_host_file:
+ # lock around the initial SSH connectivity so the user prompt about whether to add
+ # the host to known hosts is not intermingled with multiprocess output.
+ fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
+ fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
+
+ # create process
+ (p, stdin) = self._run(ssh_cmd, in_data)
+
+ self._send_password()
+
+ if (self.runner.sudo and sudoable and self.runner.sudo_pass) or \
+ (self.runner.su and su and self.runner.su_pass):
+ # several cases are handled for sudo privileges with password
+ # * NOPASSWD (tty & no-tty): detect success_key on stdout
+ # * without NOPASSWD:
+ # * detect prompt on stdout (tty)
+ # * detect prompt on stderr (no-tty)
+ fcntl.fcntl(p.stdout, fcntl.F_SETFL,
+ fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
+ fcntl.fcntl(p.stderr, fcntl.F_SETFL,
+ fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
+ sudo_output = ''
+ sudo_errput = ''
+
+ while True:
+ if success_key in sudo_output or \
+ (self.runner.sudo_pass and sudo_output.endswith(prompt)) or \
+ (self.runner.su_pass and prompt_re.match(sudo_output)):
+ break
+
+ rfd, wfd, efd = select.select([p.stdout, p.stderr], [],
+ [p.stdout], self.runner.timeout)
+ if p.stderr in rfd:
+ chunk = p.stderr.read()
+ if not chunk:
+ raise errors.AnsibleError('ssh connection closed waiting for sudo or su password prompt')
+ sudo_errput += chunk
+ incorrect_password = gettext.dgettext(
+ "sudo", "Sorry, try again.")
+ if sudo_errput.strip().endswith("%s%s" % (prompt, incorrect_password)):
+ raise errors.AnsibleError('Incorrect sudo password')
+ elif sudo_errput.endswith(prompt):
+ stdin.write(self.runner.sudo_pass + '\n')
+
+ if p.stdout in rfd:
+ chunk = p.stdout.read()
+ if not chunk:
+ raise errors.AnsibleError('ssh connection closed waiting for sudo or su password prompt')
+ sudo_output += chunk
+
+ if not rfd:
+ # timeout. wrap up process communication
+ stdout = p.communicate()
+ raise errors.AnsibleError('ssh connection error waiting for sudo or su password prompt')
+
+ if success_key not in sudo_output:
+ if sudoable:
+ stdin.write(self.runner.sudo_pass + '\n')
+ elif su:
+ stdin.write(self.runner.su_pass + '\n')
+
+ (returncode, stdout, stderr) = self._communicate(p, stdin, in_data, su=su, sudoable=sudoable, prompt=prompt)
+
+ if C.HOST_KEY_CHECKING and not_in_host_file:
+ # lock around the initial SSH connectivity so the user prompt about whether to add
+ # the host to known hosts is not intermingled with multiprocess output.
+ fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_UN)
+ fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
+ controlpersisterror = 'Bad configuration option: ControlPersist' in stderr or \
+ 'unknown configuration option: ControlPersist' in stderr
+
+ if C.HOST_KEY_CHECKING:
+ if ssh_cmd[0] == "sshpass" and p.returncode == 6:
+ raise errors.AnsibleError('Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this. Please add this host\'s fingerprint to your known_hosts file to manage this host.')
+
+ if p.returncode != 0 and controlpersisterror:
+ raise errors.AnsibleError('using -c ssh on certain older ssh versions may not support ControlPersist, set ANSIBLE_SSH_ARGS="" (or ssh_args in [ssh_connection] section of the config file) before running again')
+ if p.returncode == 255 and (in_data or self.runner.module_name == 'raw'):
+ raise errors.AnsibleError('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
+
+ return (p.returncode, '', stdout, stderr)
+
+ def put_file(self, in_path, out_path):
+ ''' transfer a file from local to remote '''
+ vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
+ if not os.path.exists(in_path):
+ raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
+ cmd = self._password_cmd()
+
+ host = self.host
+ if self.ipv6:
+ host = '[%s]' % host
+
+ if C.DEFAULT_SCP_IF_SSH:
+ cmd += ["scp"] + self.common_args
+ cmd += [in_path,host + ":" + pipes.quote(out_path)]
+ indata = None
+ else:
+ cmd += ["sftp"] + self.common_args + [host]
+ indata = "put %s %s\n" % (pipes.quote(in_path), pipes.quote(out_path))
+
+ (p, stdin) = self._run(cmd, indata)
+
+ self._send_password()
+
+ (returncode, stdout, stderr) = self._communicate(p, stdin, indata)
+
+ if returncode != 0:
+ raise errors.AnsibleError("failed to transfer file to %s:\n%s\n%s" % (out_path, stdout, stderr))
+
+ def fetch_file(self, in_path, out_path):
+ ''' fetch a file from remote to local '''
+ vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
+ cmd = self._password_cmd()
+
+ host = self.host
+ if self.ipv6:
+ host = '[%s]' % host
+
+ if C.DEFAULT_SCP_IF_SSH:
+ cmd += ["scp"] + self.common_args
+ cmd += [host + ":" + in_path, out_path]
+ indata = None
+ else:
+ cmd += ["sftp"] + self.common_args + [host]
+ indata = "get %s %s\n" % (in_path, out_path)
+
+ p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ self._send_password()
+ stdout, stderr = p.communicate(indata)
+
+ if p.returncode != 0:
+ raise errors.AnsibleError("failed to transfer file from %s:\n%s\n%s" % (in_path, stdout, stderr))
+
+ def close(self):
+ ''' not applicable since we're executing openssh binaries '''
+ pass
+
diff --git a/xos/observers/vcpe/steps/sync_vcpetenant.py b/xos/observers/vcpe/steps/sync_vcpetenant.py
new file mode 100644
index 0000000..d35a628
--- /dev/null
+++ b/xos/observers/vcpe/steps/sync_vcpetenant.py
@@ -0,0 +1,235 @@
+import hashlib
+import os
+import socket
+import sys
+import base64
+import time
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from synchronizers.base.ansible import run_template_ssh
+from observers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
+from core.models import Service, Slice
+from services.cord.models import VCPEService, VCPETenant, VOLTTenant
+from services.hpc.models import HpcService, CDNPrefix
+from util.logger import Logger, logging
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from broadbandshield import BBS
+
+logger = Logger(level=logging.INFO)
+
+PARENTAL_MECHANISM="dnsmasq"
+ENABLE_QUICK_UPDATE=False
+
+class SyncVCPETenant(SyncInstanceUsingAnsible):
+ provides=[VCPETenant]
+ observes=VCPETenant
+ requested_interval=0
+ template_name = "sync_vcpetenant.yaml"
+ service_key_name = "/opt/xos/observers/vcpe/vcpe_private_key"
+
+ def __init__(self, *args, **kwargs):
+ super(SyncVCPETenant, self).__init__(*args, **kwargs)
+
+ def fetch_pending(self, deleted):
+ if (not deleted):
+ objs = VCPETenant.get_tenant_objects().filter(Q(enacted__lt=F('updated')) | Q(enacted=None),Q(lazy_blocked=False))
+ else:
+ objs = VCPETenant.get_deleted_tenant_objects()
+
+ return objs
+
+ def get_vcpe_service(self, o):
+ if not o.provider_service:
+ return None
+
+ vcpes = VCPEService.get_service_objects().filter(id=o.provider_service.id)
+ if not vcpes:
+ return None
+
+ return vcpes[0]
+
+ def get_extra_attributes(self, o):
+ # This is a place to include extra attributes that aren't part of the
+ # object itself. In the case of vCPE, we need to know:
+ # 1) the addresses of dnsdemux, to setup dnsmasq in the vCPE
+ # 2) CDN prefixes, so we know what URLs to send to dnsdemux
+ # 3) BroadBandShield server addresses, for parental filtering
+ # 4) vlan_ids, for setting up networking in the vCPE VM
+
+ vcpe_service = self.get_vcpe_service(o)
+
+ dnsdemux_ip = None
+ if vcpe_service.backend_network_label:
+ # Connect to dnsdemux using the network specified by
+ # vcpe_service.backend_network_label
+ for service in HpcService.objects.all():
+ for slice in service.slices.all():
+ if "dnsdemux" in slice.name:
+ for instance in slice.instances.all():
+ for ns in instance.ports.all():
+ if ns.ip and ns.network.labels and (vcpe_service.backend_network_label in ns.network.labels):
+ dnsdemux_ip = ns.ip
+ if not dnsdemux_ip:
+ logger.info("failed to find a dnsdemux on network %s" % vcpe_service.backend_network_label)
+ else:
+ # Connect to dnsdemux using the instance's public address
+ for service in HpcService.objects.all():
+ for slice in service.slices.all():
+ if "dnsdemux" in slice.name:
+ for instance in slice.instances.all():
+ if dnsdemux_ip=="none":
+ try:
+ dnsdemux_ip = socket.gethostbyname(instance.node.name)
+ except:
+ pass
+ if not dnsdemux_ip:
+ logger.info("failed to find a dnsdemux with a public address")
+
+ dnsdemux_ip = dnsdemux_ip or "none"
+
+ cdn_prefixes = []
+ for prefix in CDNPrefix.objects.all():
+ cdn_prefixes.append(prefix.prefix)
+
+ # Broadbandshield can either be set up internally, using vcpe_service.bbs_slice,
+ # or it can be setup externally using vcpe_service.bbs_server.
+
+ bbs_addrs = []
+ if vcpe_service.bbs_slice:
+ if vcpe_service.backend_network_label:
+ for bbs_instance in vcpe_service.bbs_slice.instances.all():
+ for ns in bbs_instance.ports.all():
+ if ns.ip and ns.network.labels and (vcpe_service.backend_network_label in ns.network.labels):
+ bbs_addrs.append(ns.ip)
+ else:
+ logger.info("unsupported configuration -- bbs_slice is set, but backend_network_label is not")
+ if not bbs_addrs:
+ logger.info("failed to find any usable addresses on bbs_slice")
+ elif vcpe_service.bbs_server:
+ bbs_addrs.append(vcpe_service.bbs_server)
+ else:
+ logger.info("neither bbs_slice nor bbs_server is configured in the vCPE")
+
+ vlan_ids = []
+ s_tags = []
+ c_tags = []
+ if o.volt:
+ vlan_ids.append(o.volt.vlan_id) # XXX remove this
+ s_tags.append(o.volt.s_tag)
+ c_tags.append(o.volt.c_tag)
+
+ try:
+ full_setup = Config().observer_full_setup
+ except:
+ full_setup = True
+
+ safe_macs=[]
+ if o.volt and o.volt.subscriber:
+ for user in o.volt.subscriber.users:
+ level = user.get("level",None)
+ mac = user.get("mac",None)
+ if level in ["G", "PG"]:
+ if mac:
+ safe_macs.append(mac)
+
+ fields = {"vlan_ids": vlan_ids, # XXX remove this
+ "s_tags": s_tags,
+ "c_tags": c_tags,
+ "dnsdemux_ip": dnsdemux_ip,
+ "cdn_prefixes": cdn_prefixes,
+ "bbs_addrs": bbs_addrs,
+ "full_setup": full_setup,
+ "isolation": o.instance.isolation,
+ "safe_browsing_macs": safe_macs}
+
+ # add in the sync_attributes that come from the SubscriberRoot object
+
+ if o.volt and o.volt.subscriber and hasattr(o.volt.subscriber, "sync_attributes"):
+ for attribute_name in o.volt.subscriber.sync_attributes:
+ fields[attribute_name] = getattr(o.volt.subscriber, attribute_name)
+
+ return fields
+
+ def sync_fields(self, o, fields):
+ # the super causes the playbook to be run
+
+ super(SyncVCPETenant, self).sync_fields(o, fields)
+
+ # now do all of our broadbandshield stuff...
+
+ service = self.get_vcpe_service(o)
+ if not service:
+ # Ansible uses the service's keypair in order to SSH into the
+ # instance. It would be bad if the slice had no service.
+
+ raise Exception("Slice %s is not associated with a service" % instance.slice.name)
+
+ # Make sure the slice is configured properly
+ if (service != o.instance.slice.service):
+ raise Exception("Slice %s is associated with some service that is not %s" % (str(instance.slice), str(service)))
+
+ # only enable filtering if we have a subscriber object (see below)
+ url_filter_enable = False
+
+ # for attributes that come from CordSubscriberRoot
+ if o.volt and o.volt.subscriber:
+ url_filter_enable = o.volt.subscriber.url_filter_enable
+ url_filter_level = o.volt.subscriber.url_filter_level
+ url_filter_users = o.volt.subscriber.users
+
+ if PARENTAL_MECHANISM=="broadbandshield":
+ # disable url_filter if there are no bbs_addrs
+ if url_filter_enable and (not fields.get("bbs_addrs",[])):
+ logger.info("disabling url_filter because there are no bbs_addrs")
+ url_filter_enable = False
+
+ if url_filter_enable:
+ bbs_hostname = None
+ if service.bbs_api_hostname and service.bbs_api_port:
+ bbs_hostname = service.bbs_api_hostname
+ else:
+ # TODO: extract from slice
+ bbs_hostname = "cordcompute01.onlab.us"
+
+ if service.bbs_api_port:
+ bbs_port = service.bbs_api_port
+ else:
+ bbs_port = 8018
+
+ if not bbs_hostname:
+ logger.info("broadbandshield is not configured")
+ else:
+ tStart = time.time()
+ bbs = BBS(o.bbs_account, "123", bbs_hostname, bbs_port)
+ bbs.sync(url_filter_level, url_filter_users)
+
+ if o.hpc_client_ip:
+ logger.info("associate account %s with ip %s" % (o.bbs_account, o.hpc_client_ip))
+ bbs.associate(o.hpc_client_ip)
+ else:
+ logger.info("no hpc_client_ip to associate")
+
+ logger.info("bbs update time %d" % int(time.time()-tStart))
+
+
+ def run_playbook(self, o, fields):
+ ansible_hash = hashlib.md5(repr(sorted(fields.items()))).hexdigest()
+ quick_update = (o.last_ansible_hash == ansible_hash)
+
+ if ENABLE_QUICK_UPDATE and quick_update:
+ logger.info("quick_update triggered; skipping ansible recipe")
+ else:
+ if o.instance.isolation in ["container", "container_vm"]:
+ super(SyncVCPETenant, self).run_playbook(o, fields, "sync_vcpetenant_new.yaml")
+ else:
+ super(SyncVCPETenant, self).run_playbook(o, fields)
+
+ o.last_ansible_hash = ansible_hash
+
+ def delete_record(self, m):
+ pass
diff --git a/xos/observers/vcpe/steps/sync_vcpetenant.yaml b/xos/observers/vcpe/steps/sync_vcpetenant.yaml
new file mode 100644
index 0000000..fdc21da
--- /dev/null
+++ b/xos/observers/vcpe/steps/sync_vcpetenant.yaml
@@ -0,0 +1,174 @@
+---
+- hosts: {{ instance_name }}
+ gather_facts: False
+ connection: ssh
+ user: ubuntu
+ sudo: yes
+ vars:
+ cdn_enable: {{ cdn_enable }}
+ dnsdemux_ip: {{ dnsdemux_ip }}
+ firewall_enable: {{ firewall_enable }}
+ url_filter_enable: {{ url_filter_enable }}
+ vlan_ids:
+ {% for vlan_id in vlan_ids %}
+ - {{ vlan_id }}
+ {% endfor %}
+ c_tags:
+ {% for c_tag in c_tags %}
+ - {{ c_tag }}
+ {% endfor %}
+ s_tags:
+ {% for s_tag in s_tags %}
+ - {{ s_tag }}
+ {% endfor %}
+ firewall_rules:
+ {% for firewall_rule in firewall_rules.split("\n") %}
+ - {{ firewall_rule }}
+ {% endfor %}
+ cdn_prefixes:
+ {% for prefix in cdn_prefixes %}
+ - {{ prefix }}
+ {% endfor %}
+ bbs_addrs:
+ {% for bbs_addr in bbs_addrs %}
+ - {{ bbs_addr }}
+ {% endfor %}
+ nat_ip: {{ nat_ip }}
+ nat_mac: {{ nat_mac }}
+ lan_ip: {{ lan_ip }}
+ lan_mac: {{ lan_mac }}
+ wan_ip: {{ wan_ip }}
+ wan_mac: {{ wan_mac }}
+ wan_container_mac: {{ wan_container_mac }}
+ wan_next_hop: 10.0.1.253 # FIX ME
+ private_ip: {{ private_ip }}
+ private_mac: {{ private_mac }}
+ hpc_client_ip: {{ hpc_client_ip }}
+ hpc_client_mac: {{ hpc_client_mac }}
+ keystone_tenant_id: {{ keystone_tenant_id }}
+ keystone_user_id: {{ keystone_user_id }}
+ rabbit_user: {{ rabbit_user }}
+ rabbit_password: {{ rabbit_password }}
+ rabbit_host: {{ rabbit_host }}
+ safe_browsing:
+ {% for mac in safe_browsing_macs %}
+ - {{ mac }}
+ {% endfor %}
+
+ tasks:
+{% if full_setup %}
+ - name: Docker repository
+ copy: src=/opt/xos/observers/vcpe/files/docker.list
+ dest=/etc/apt/sources.list.d/docker.list
+
+ - name: Import the repository key
+ apt_key: keyserver=keyserver.ubuntu.com id=36A1D7869245C8950F966E92D8576A8BA88D21E9
+
+ - name: install Docker
+ apt: name=lxc-docker state=present update_cache=yes
+
+ - name: install python-setuptools
+ apt: name=python-setuptools state=present
+
+ - name: install pip
+ easy_install: name=pip
+
+ - name: install docker-py
+ pip: name=docker-py version=0.5.3
+
+ - name: install Pipework
+ get_url: url=https://raw.githubusercontent.com/jpetazzo/pipework/master/pipework
+ dest=/usr/local/bin/pipework
+ mode=0755
+
+ - name: make sure /etc/dnsmasq.d exists
+ file: path=/etc/dnsmasq.d state=directory owner=root group=root
+
+ - name: Disable resolvconf service
+ shell: service resolvconf stop
+ shell: echo manual > /etc/init/resolvconf.override
+ shell: rm -f /etc/resolv.conf
+
+ - name: Install resolv.conf
+ copy: src=/opt/xos/observers/vcpe/files/vm-resolv.conf
+ dest=/etc/resolv.conf
+
+ - name: Verify if vcpe_stats_notifier ([] is to avoid capturing the shell process) cron job is already running
+ shell: pgrep -f [v]cpe_stats_notifier | wc -l
+ register: cron_job_pids_count
+
+# - name: DEBUG
+# debug: var=cron_job_pids_count.stdout
+
+ - name: make sure ~/bin exists
+ file: path=~/bin state=directory owner=root group=root
+ when: cron_job_pids_count.stdout == "0"
+
+ - name: Copy cron job to destination
+ copy: src=/opt/xos/observers/vcpe/vcpe_stats_notifier.py
+ dest=~/bin/vcpe_stats_notifier.py
+ when: cron_job_pids_count.stdout == "0"
+
+ - name: install python-kombu
+ apt: name=python-kombu state=present
+ when: cron_job_pids_count.stdout == "0"
+
+ - name: Initiate vcpe_stats_notifier cron job
+ command: python ~/bin/vcpe_stats_notifier.py --keystone_tenant_id={{ keystone_tenant_id }} --keystone_user_id={{ keystone_user_id }} --rabbit_user={{ rabbit_user }} --rabbit_password={{ rabbit_password }} --rabbit_host={{ rabbit_host }} --vcpeservice_rabbit_exchange='vcpeservice'
+ async: 9999999999999999
+ poll: 0
+ when: cron_job_pids_count.stdout == "0"
+{% endif %}
+
+ - name: vCPE upstart
+ template: src=/opt/xos/observers/vcpe/templates/vcpe.conf.j2 dest=/etc/init/vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}.conf
+
+ - name: vCPE startup script
+ template: src=/opt/xos/observers/vcpe/templates/start-vcpe.sh.j2 dest=/usr/local/sbin/start-vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}.sh mode=0755
+ notify:
+# - restart vcpe
+ - stop vcpe
+ - remove container
+ - start vcpe
+
+ - name: create /etc/vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}/dnsmasq.d
+ file: path=/etc/vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}/dnsmasq.d state=directory owner=root group=root
+
+ - name: vCPE basic dnsmasq config
+ copy: src=/opt/xos/observers/vcpe/files/vcpe.dnsmasq dest=/etc/vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}/dnsmasq.d/vcpe.conf owner=root group=root
+ notify:
+ - restart dnsmasq
+
+ - name: dnsmasq config
+ template: src=/opt/xos/observers/vcpe/templates/dnsmasq_servers.j2 dest=/etc/vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}/dnsmasq.d/servers.conf owner=root group=root
+ notify:
+ - restart dnsmasq
+
+# These are samples, not necessary for correct function of demo
+
+# - name: networking info
+# template: src=/opt/xos/observers/vcpe/templates/vlan_sample.j2 dest=/etc/vlan_sample owner=root group=root
+
+# - name: firewall info
+# template: src=/opt/xos/observers/vcpe/templates/firewall_sample.j2 dest=/etc/firewall_sample owner=root group=root
+
+ - name: Make sure vCPE service is running
+ service: name=vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} state=started
+
+ handlers:
+ # Dnsmasq is automatically restarted in the container
+ - name: restart dnsmasq
+ shell: docker exec vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} killall dnsmasq
+
+ - name: restart vcpe
+ shell: service vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} stop; sleep 1; service vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} start
+
+ - name: stop vcpe
+ service: name=vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} state=stopped
+
+ - name: remove container
+ docker: name=vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} state=absent image=docker-vcpe
+
+ - name: start vcpe
+ service: name=vcpe-{{ s_tags[0] }}-{{ c_tags[0] }} state=started
+
diff --git a/xos/observers/vcpe/steps/sync_vcpetenant_new.yaml b/xos/observers/vcpe/steps/sync_vcpetenant_new.yaml
new file mode 100644
index 0000000..6c7166f
--- /dev/null
+++ b/xos/observers/vcpe/steps/sync_vcpetenant_new.yaml
@@ -0,0 +1,131 @@
+---
+- hosts: {{ instance_name }}
+ gather_facts: False
+ connection: ssh
+ user: {{ username }}
+ sudo: yes
+ vars:
+ container_name: {{ container_name }}
+ cdn_enable: {{ cdn_enable }}
+ dnsdemux_ip: {{ dnsdemux_ip }}
+ firewall_enable: {{ firewall_enable }}
+ url_filter_enable: {{ url_filter_enable }}
+ vlan_ids:
+ {% for vlan_id in vlan_ids %}
+ - {{ vlan_id }}
+ {% endfor %}
+ c_tags:
+ {% for c_tag in c_tags %}
+ - {{ c_tag }}
+ {% endfor %}
+ s_tags:
+ {% for s_tag in s_tags %}
+ - {{ s_tag }}
+ {% endfor %}
+ firewall_rules:
+ {% for firewall_rule in firewall_rules.split("\n") %}
+ - {{ firewall_rule }}
+ {% endfor %}
+ cdn_prefixes:
+ {% for prefix in cdn_prefixes %}
+ - {{ prefix }}
+ {% endfor %}
+ bbs_addrs:
+ {% for bbs_addr in bbs_addrs %}
+ - {{ bbs_addr }}
+ {% endfor %}
+ nat_ip: {{ nat_ip }}
+ nat_mac: {{ nat_mac }}
+ lan_ip: {{ lan_ip }}
+ lan_mac: {{ lan_mac }}
+ wan_ip: {{ wan_ip }}
+ wan_mac: {{ wan_mac }}
+ wan_container_mac: {{ wan_container_mac }}
+ wan_next_hop: 10.0.1.253 # FIX ME
+ private_ip: {{ private_ip }}
+ private_mac: {{ private_mac }}
+ hpc_client_ip: {{ hpc_client_ip }}
+ hpc_client_mac: {{ hpc_client_mac }}
+ keystone_tenant_id: {{ keystone_tenant_id }}
+ keystone_user_id: {{ keystone_user_id }}
+ rabbit_user: {{ rabbit_user }}
+ rabbit_password: {{ rabbit_password }}
+ rabbit_host: {{ rabbit_host }}
+ safe_browsing:
+ {% for mac in safe_browsing_macs %}
+ - {{ mac }}
+ {% endfor %}
+
+ tasks:
+ - name: Verify if vcpe_stats_notifier ([] is to avoid capturing the shell process) cron job is already running
+ shell: pgrep -f [v]cpe_stats_notifier | wc -l
+ register: cron_job_pids_count
+
+# - name: DEBUG
+# debug: var=cron_job_pids_count.stdout
+
+ - name: make sure ~/bin exists
+ file: path=~/bin state=directory owner=root group=root
+ when: cron_job_pids_count.stdout == "0"
+
+ - name: Copy cron job to destination
+ copy: src=/opt/xos/observers/vcpe/vcpe_stats_notifier.py
+ dest=~/bin/vcpe_stats_notifier.py
+ when: cron_job_pids_count.stdout == "0"
+
+ - name: install python-kombu
+ apt: name=python-kombu state=present
+ when: cron_job_pids_count.stdout == "0"
+
+ - name: Initiate vcpe_stats_notifier cron job
+ command: python ~/bin/vcpe_stats_notifier.py --keystone_tenant_id={{ keystone_tenant_id }} --keystone_user_id={{ keystone_user_id }} --rabbit_user={{ rabbit_user }} --rabbit_password={{ rabbit_password }} --rabbit_host={{ rabbit_host }} --vcpeservice_rabbit_exchange='vcpeservice'
+ async: 9999999999999999
+ poll: 0
+ when: cron_job_pids_count.stdout == "0"
+
+ - name: vCPE basic dnsmasq config
+ copy: src=/opt/xos/observers/vcpe/files/vcpe.dnsmasq dest=/var/container_volumes/{{ container_name }}/etc/dnsmasq.d/vcpe.conf owner=root group=root
+ notify:
+ - restart dnsmasq
+
+ - name: dnsmasq config
+ template: src=/opt/xos/observers/vcpe/templates/dnsmasq_servers.j2 dest=/var/container_volumes/{{ container_name }}/etc/dnsmasq.d/servers.conf owner=root group=root
+ notify:
+ - restart dnsmasq
+
+ - name: create directory for "safe" config
+ file: path=/var/container_volumes/{{ container_name }}/etc/dnsmasq.d/safe state=directory
+
+ - name: dnsmasq "safe" config
+ template: src=/opt/xos/observers/vcpe/templates/dnsmasq_safe_servers.j2 dest=/var/container_volumes/{{ container_name }}/etc/dnsmasq.d/safe/servers.conf owner=root group=root
+ notify:
+ - restart dnsmasq
+
+ - name: copy base ufw files
+ synchronize: src=/opt/xos/observers/vcpe/files/etc/ufw/ dest=/var/container_volumes/{{ container_name }}/etc/ufw/
+ notify:
+ - reload ufw
+
+ - name: redirection rules for safe DNS
+ template: src=/opt/xos/observers/vcpe/templates/before.rules.j2 dest=/var/container_volumes/{{ container_name }}/etc/ufw/before.rules owner=root group=root
+ notify:
+ - reload ufw
+
+ - name: base ufw setup uses /etc/rc.local
+ copy: src=/opt/xos/observers/vcpe/files/etc/rc.local dest=/var/container_volumes/{{ container_name }}/etc/ owner=root group=root
+ notify:
+ - copy in /etc/rc.local
+
+ handlers:
+ # Dnsmasq is automatically restarted in the container
+ - name: restart dnsmasq
+ shell: docker exec {{ container_name }} /usr/bin/killall dnsmasq
+
+ - name: reload ufw
+ shell: docker exec {{ container_name }} bash -c "/sbin/iptables -t nat -F PREROUTING; /usr/sbin/ufw reload"
+
+ # Use docker cp instead of single-file volume
+ # The reason is that changes to external file volume don't show up inside the container
+ # Probably Ansible deletes and then recreates the external file, and container has old version
+ - name: copy in /etc/rc.local
+ shell: docker cp /var/container_volumes/{{ container_name }}/etc/rc.local {{ container_name }}:/etc/
diff --git a/xos/observers/vcpe/steps/test.yaml b/xos/observers/vcpe/steps/test.yaml
new file mode 100644
index 0000000..fc8251d
--- /dev/null
+++ b/xos/observers/vcpe/steps/test.yaml
@@ -0,0 +1,7 @@
+---
+- hosts: {{ instance_name }}
+ connection: ssh
+ user: ubuntu
+ tasks:
+ - name: foobar
+ shell: echo foo > /tmp/foobar
diff --git a/xos/observers/vcpe/stop.sh b/xos/observers/vcpe/stop.sh
new file mode 100755
index 0000000..e90e16c
--- /dev/null
+++ b/xos/observers/vcpe/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f vcpe-observer.py
diff --git a/xos/observers/vcpe/supervisor/vcpe-observer.conf b/xos/observers/vcpe/supervisor/vcpe-observer.conf
new file mode 100644
index 0000000..27d2796
--- /dev/null
+++ b/xos/observers/vcpe/supervisor/vcpe-observer.conf
@@ -0,0 +1,2 @@
+[program:vcpe-observer]
+command=python /opt/xos/observers/vcpe/vcpe-observer.py -C /opt/xos/observers/vcpe/vcpe_observer_config
diff --git a/xos/observers/vcpe/templates/before.rules.j2 b/xos/observers/vcpe/templates/before.rules.j2
new file mode 100644
index 0000000..e6f7d4a
--- /dev/null
+++ b/xos/observers/vcpe/templates/before.rules.j2
@@ -0,0 +1,97 @@
+#
+# rules.before
+#
+# Rules that should be run before the ufw command line added rules. Custom
+# rules should be added to one of these chains:
+# ufw-before-input
+# ufw-before-output
+# ufw-before-forward
+#
+
+# nat Table rules
+*nat
+:POSTROUTING ACCEPT [0:0]
+
+# Forward traffic from eth1 through eth0.
+-A POSTROUTING -o eth0 -j MASQUERADE
+
+# Set up NAT for CDN services
+-A POSTROUTING -o eth2 -j MASQUERADE
+
+# DNS safe browsing
+{% if safe_browsing %}
+{% for mac in safe_browsing %}
+-A PREROUTING -i eth1 -m mac --mac-source {{ mac }} -p udp --dport 53 -j REDIRECT --to-port 5353
+-A PREROUTING -i eth1 -m mac --mac-source {{ mac }} -p tcp --dport 53 -j REDIRECT --to-port 5353
+{% endfor %}
+{% endif %}
+
+# don't delete the 'COMMIT' line or these nat table rules won't be processed
+COMMIT
+
+# Don't delete these required lines, otherwise there will be errors
+*filter
+:ufw-before-input - [0:0]
+:ufw-before-output - [0:0]
+:ufw-before-forward - [0:0]
+:ufw-not-local - [0:0]
+# End required lines
+
+# allow all on loopback
+-A ufw-before-input -i lo -j ACCEPT
+-A ufw-before-output -o lo -j ACCEPT
+
+# quickly process packets for which we already have a connection
+-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+-A ufw-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+-A ufw-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+
+# drop INVALID packets (logs these in loglevel medium and higher)
+-A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny
+-A ufw-before-input -m conntrack --ctstate INVALID -j DROP
+
+# ok icmp codes for INPUT
+-A ufw-before-input -p icmp --icmp-type destination-unreachable -j ACCEPT
+-A ufw-before-input -p icmp --icmp-type source-quench -j ACCEPT
+-A ufw-before-input -p icmp --icmp-type time-exceeded -j ACCEPT
+-A ufw-before-input -p icmp --icmp-type parameter-problem -j ACCEPT
+-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT
+
+# ok icmp code for FORWARD
+-A ufw-before-forward -p icmp --icmp-type destination-unreachable -j ACCEPT
+-A ufw-before-forward -p icmp --icmp-type source-quench -j ACCEPT
+-A ufw-before-forward -p icmp --icmp-type time-exceeded -j ACCEPT
+-A ufw-before-forward -p icmp --icmp-type parameter-problem -j ACCEPT
+-A ufw-before-forward -p icmp --icmp-type echo-request -j ACCEPT
+
+# allow dhcp client to work
+-A ufw-before-input -p udp --sport 67 --dport 68 -j ACCEPT
+
+#
+# ufw-not-local
+#
+-A ufw-before-input -j ufw-not-local
+
+# if LOCAL, RETURN
+-A ufw-not-local -m addrtype --dst-type LOCAL -j RETURN
+
+# if MULTICAST, RETURN
+-A ufw-not-local -m addrtype --dst-type MULTICAST -j RETURN
+
+# if BROADCAST, RETURN
+-A ufw-not-local -m addrtype --dst-type BROADCAST -j RETURN
+
+# all other non-local packets are dropped
+-A ufw-not-local -m limit --limit 3/min --limit-burst 10 -j ufw-logging-deny
+-A ufw-not-local -j DROP
+
+# allow MULTICAST mDNS for service discovery (be sure the MULTICAST line above
+# is uncommented)
+-A ufw-before-input -p udp -d 224.0.0.251 --dport 5353 -j ACCEPT
+
+# allow MULTICAST UPnP for service discovery (be sure the MULTICAST line above
+# is uncommented)
+-A ufw-before-input -p udp -d 239.255.255.250 --dport 1900 -j ACCEPT
+
+# don't delete the 'COMMIT' line or these rules won't be processed
+COMMIT
diff --git a/xos/observers/vcpe/templates/dnsmasq_safe_servers.j2 b/xos/observers/vcpe/templates/dnsmasq_safe_servers.j2
new file mode 100644
index 0000000..2f93777
--- /dev/null
+++ b/xos/observers/vcpe/templates/dnsmasq_safe_servers.j2
@@ -0,0 +1,14 @@
+# This file autogenerated by vCPE observer
+# It contains a list of DNS servers for dnsmasq to use.
+no-resolv
+
+{% if cdn_enable %}
+# CDN
+{% for prefix in cdn_prefixes %}
+server=/{{ prefix }}/{{ dnsdemux_ip }}
+{% endfor %}
+{% endif %}
+
+# use OpenDNS service
+server=208.67.222.123
+server=208.67.220.123
diff --git a/xos/observers/vcpe/templates/dnsmasq_servers.j2 b/xos/observers/vcpe/templates/dnsmasq_servers.j2
new file mode 100644
index 0000000..c89c762
--- /dev/null
+++ b/xos/observers/vcpe/templates/dnsmasq_servers.j2
@@ -0,0 +1,14 @@
+# This file autogenerated by vCPE observer
+# It contains a list of DNS servers for dnsmasq to use.
+no-resolv
+
+{% if cdn_enable %}
+# CDN
+{% for prefix in cdn_prefixes %}
+server=/{{ prefix }}/{{ dnsdemux_ip }}
+{% endfor %}
+{% endif %}
+
+# use google's DNS service
+server=8.8.8.8
+server=8.8.4.4
diff --git a/xos/observers/vcpe/templates/firewall_sample.j2 b/xos/observers/vcpe/templates/firewall_sample.j2
new file mode 100644
index 0000000..ce85e68
--- /dev/null
+++ b/xos/observers/vcpe/templates/firewall_sample.j2
@@ -0,0 +1,5 @@
+firewall_enable = {{ firewall_enable }}
+
+{% for firewall_rule in firewall_rules %}
+{{ firewall_rule }}
+{% endfor %}
diff --git a/xos/observers/vcpe/templates/start-vcpe.sh.j2 b/xos/observers/vcpe/templates/start-vcpe.sh.j2
new file mode 100755
index 0000000..c4128f3
--- /dev/null
+++ b/xos/observers/vcpe/templates/start-vcpe.sh.j2
@@ -0,0 +1,50 @@
+#!/bin/bash
+
+function mac_to_iface {
+ MAC=$1
+ ifconfig|grep $MAC| awk '{print $1}'|grep -v '\.'
+}
+
+iptables -L > /dev/null
+ip6tables -L > /dev/null
+
+STAG={{ s_tags[0] }}
+CTAG={{ c_tags[0] }}
+VCPE=vcpe-$STAG-$CTAG
+
+docker inspect $VCPE > /dev/null 2>&1
+if [ "$?" == 1 ]
+then
+ docker pull andybavier/docker-vcpe
+ docker run -d --name=$VCPE --privileged=true --net=none -v /etc/$VCPE/dnsmasq.d:/etc/dnsmasq.d andybavier/docker-vcpe
+else
+ docker start $VCPE
+fi
+
+# Set up networking via pipework
+WAN_IFACE=$( mac_to_iface {{ wan_mac }} )
+docker exec $VCPE ifconfig eth0 >> /dev/null || pipework $WAN_IFACE -i eth0 $VCPE {{ wan_ip }}/24@{{ wan_next_hop }} {{ wan_container_mac }}
+
+# LAN_IFACE=$( mac_to_iface {{ lan_mac }} )
+# Need to encapsulate VLAN traffic so that Neutron doesn't eat it
+# Assumes that br-lan has been set up appropriately by a previous step
+LAN_IFACE=br-lan
+ifconfig $LAN_IFACE >> /dev/null
+if [ "$?" == 0 ]
+then
+ ifconfig $LAN_IFACE.$STAG >> /dev/null || ip link add link $LAN_IFACE name $LAN_IFACE.$STAG type vlan id $STAG
+ ifconfig $LAN_IFACE.$STAG up
+ docker exec $VCPE ifconfig eth1 >> /dev/null || pipework $LAN_IFACE.$STAG -i eth1 $VCPE 192.168.0.1/24 @$CTAG
+fi
+
+#HPC_IFACE=$( mac_to_iface {{ hpc_client_mac }} )
+#docker exec $VCPE ifconfig eth2 >> /dev/null || pipework $HPC_IFACE -i eth2 $VCPE {{ hpc_client_ip }}/24
+
+# Make sure VM's eth0 (hpc_client) has no IP address
+#ifconfig $HPC_IFACE 0.0.0.0
+
+# Now can start up dnsmasq
+docker exec $VCPE service dnsmasq start
+
+# Attach to container
+docker start -a $VCPE
diff --git a/xos/observers/vcpe/templates/vcpe.conf.j2 b/xos/observers/vcpe/templates/vcpe.conf.j2
new file mode 100644
index 0000000..fa7885e
--- /dev/null
+++ b/xos/observers/vcpe/templates/vcpe.conf.j2
@@ -0,0 +1,10 @@
+# Upstart script for vCPE
+description "vCPE container"
+author "andy@onlab.us"
+start on filesystem and started docker
+stop on runlevel [!2345]
+respawn
+
+script
+ /usr/local/sbin/start-vcpe-{{ s_tags[0] }}-{{ c_tags[0] }}.sh
+end script
diff --git a/xos/observers/vcpe/templates/vlan_sample.j2 b/xos/observers/vcpe/templates/vlan_sample.j2
new file mode 100644
index 0000000..a26c840
--- /dev/null
+++ b/xos/observers/vcpe/templates/vlan_sample.j2
@@ -0,0 +1,5 @@
+# below is a list of all vlan_ids associated with this vcpe
+
+{% for vlan_id in vlan_ids %}
+{{ vlan_id }}
+{% endfor %}
diff --git a/xos/observers/vcpe/vcpe-observer.py b/xos/observers/vcpe/vcpe-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/vcpe/vcpe-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+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/vcpe/vcpe_observer_config b/xos/observers/vcpe/vcpe_observer_config
new file mode 100644
index 0000000..d2c9239
--- /dev/null
+++ b/xos/observers/vcpe/vcpe_observer_config
@@ -0,0 +1,42 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=vcpe
+dependency_graph=/opt/xos/observers/vcpe/model-deps
+steps_dir=/opt/xos/observers/vcpe/steps
+sys_dir=/opt/xos/observers/vcpe/sys
+deleters_dir=/opt/xos/observers/vcpe/deleters
+log_file=console
+#/var/log/hpc.log
+driver=None
+pretend=False
+backoff_disabled=True
+save_ansible_output=True
+# set proxy_ssh to false on cloudlab
+proxy_ssh=False
+full_setup=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/observers/vcpe/vcpe_stats_notifier.py b/xos/observers/vcpe/vcpe_stats_notifier.py
new file mode 100644
index 0000000..d726e3c
--- /dev/null
+++ b/xos/observers/vcpe/vcpe_stats_notifier.py
@@ -0,0 +1,268 @@
+import six
+import uuid
+import datetime
+from kombu.connection import BrokerConnection
+from kombu.messaging import Exchange, Queue, Consumer, Producer
+import subprocess
+import re
+import time, threading
+import sys, getopt
+import logging
+
+
+logfile = "vcpe_stats_notifier.log"
+level=logging.INFO
+logger=logging.getLogger('vcpe_stats_notifier')
+logger.setLevel(level)
+# create formatter
+formatter = logging.Formatter("%(asctime)s;%(levelname)s;%(message)s")
+handler=logging.handlers.RotatingFileHandler(logfile,maxBytes=1000000, backupCount=1)
+# add formatter to handler
+handler.setFormatter(formatter)
+logger.addHandler(handler)
+
+def extract_dns_stats_from_all_vcpes():
+ p = subprocess.Popen('docker ps', shell=True, stdout=subprocess.PIPE)
+ firstline = True
+ dockercontainers = {}
+ while True:
+ out = p.stdout.readline()
+ if out == '' and p.poll() != None:
+ break
+ if out != '':
+ if firstline is True:
+ firstline = False
+ else:
+ fields = out.split()
+ container_fields = {}
+ container_fields['id'] = fields[0]
+ dockercontainers[fields[-1]] = container_fields
+ for k,v in dockercontainers.iteritems():
+ cmd = 'docker exec ' + v['id'] + ' killall -10 dnsmasq'
+ p = subprocess.Popen (cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ (output, error) = p.communicate()
+ if error:
+ logger.error("killall dnsmasq command failed with error = %s",error)
+ continue
+ cmd = 'docker exec ' + v['id'] + ' tail -7 /var/log/syslog'
+ p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+ (output, error) = p.communicate()
+ if error:
+ logger.error("tail on dnsmasq log command failed with error = %s",error)
+ continue
+ log_list = output.splitlines()
+ i = 0
+ while i < len(log_list):
+ m = re.search('(?<=:\scache size\s)(\S*)(?=,\s),\s(\S*)(?=/)/(\S*)(?=\scache insertions re-used unexpired cache entries)', log_list[i])
+ if m == None:
+ i = i+1
+ continue;
+ v['cache_size'] = m.group(1)
+ v['replaced_unexpired_entries'] = m.group(2)
+ v['total_inserted_entries'] = m.group(3)
+ i = i+1
+ m = re.search('(?<=:\squeries forwarded\s)(\S*)(?=,),\squeries answered locally\s(\S*)(?=$)', log_list[i])
+ v['queries_forwarded'] = m.group(1)
+ v['queries_answered_locally'] = m.group(2)
+ break;
+ i = i+2
+ v['server_stats'] = []
+ while i < len(log_list):
+ m = re.search('(?<=:\sserver\s)(\S*)(?=#)#\d*:\squeries sent\s(\S*)(?=,),\sretried or failed\s(\S*)(?=$)', log_list[i])
+ if m == None:
+ i = i+1
+ continue
+ dns_server = {}
+ dns_server['id'] = m.group(1)
+ dns_server['queries_sent'] = m.group(2)
+ dns_server['queries_failed'] = m.group(3)
+ v['server_stats'].append(dns_server)
+ i = i+1
+ return dockercontainers
+
+
+keystone_tenant_id='3a397e70f64e4e40b69b6266c634d9d0'
+keystone_user_id='1e3ce043029547f1a61c1996d1a531a2'
+rabbit_user='openstack'
+rabbit_password='80608318c273f348a7c3'
+rabbit_host='10.11.10.1'
+vcpeservice_rabbit_exchange='vcpeservice'
+cpe_publisher_id='vcpe_publisher'
+
+producer = None
+
+def setup_rabbit_mq_channel():
+ global producer
+ global rabbit_user, rabbit_password, rabbit_host, vcpeservice_rabbit_exchange,cpe_publisher_id
+ vcpeservice_exchange = Exchange(vcpeservice_rabbit_exchange, "topic", durable=False)
+ # connections/channels
+ connection = BrokerConnection(rabbit_host, rabbit_user, rabbit_password)
+ logger.info('Connection to RabbitMQ server successful')
+ channel = connection.channel()
+ # produce
+ producer = Producer(channel, exchange=vcpeservice_exchange, routing_key='notifications.info')
+ p = subprocess.Popen('hostname', shell=True, stdout=subprocess.PIPE)
+ (hostname, error) = p.communicate()
+ cpe_publisher_id = cpe_publisher_id + '_on_' + hostname
+ logger.info('cpe_publisher_id=%s',cpe_publisher_id)
+
+def publish_cpe_stats():
+ global producer
+ global keystone_tenant_id, keystone_user_id, cpe_publisher_id
+
+ logger.debug('publish_cpe_stats invoked')
+
+ cpe_container_stats = extract_dns_stats_from_all_vcpes()
+
+ for k,v in cpe_container_stats.iteritems():
+ msg = {'event_type': 'vcpe',
+ 'message_id':six.text_type(uuid.uuid4()),
+ 'publisher_id': cpe_publisher_id,
+ 'timestamp':datetime.datetime.now().isoformat(),
+ 'priority':'INFO',
+ 'payload': {'vcpe_id':k,
+ 'user_id':keystone_user_id,
+ 'tenant_id':keystone_tenant_id
+ }
+ }
+ producer.publish(msg)
+ logger.debug('Publishing vcpe event: %s', msg)
+
+ if 'cache_size' in v:
+ msg = {'event_type': 'vcpe.dns.cache.size',
+ 'message_id':six.text_type(uuid.uuid4()),
+ 'publisher_id': cpe_publisher_id,
+ 'timestamp':datetime.datetime.now().isoformat(),
+ 'priority':'INFO',
+ 'payload': {'vcpe_id':k,
+ 'user_id':keystone_user_id,
+ 'tenant_id':keystone_tenant_id,
+ 'cache_size':v['cache_size']
+ }
+ }
+ producer.publish(msg)
+ logger.debug('Publishing vcpe.dns.cache.size event: %s', msg)
+
+ if 'total_inserted_entries' in v:
+ msg = {'event_type': 'vcpe.dns.total_inserted_entries',
+ 'message_id':six.text_type(uuid.uuid4()),
+ 'publisher_id': cpe_publisher_id,
+ 'timestamp':datetime.datetime.now().isoformat(),
+ 'priority':'INFO',
+ 'payload': {'vcpe_id':k,
+ 'user_id':keystone_user_id,
+ 'tenant_id':keystone_tenant_id,
+ 'total_inserted_entries':v['total_inserted_entries']
+ }
+ }
+ producer.publish(msg)
+ logger.debug('Publishing vcpe.dns.total_inserted_entries event: %s', msg)
+
+ if 'replaced_unexpired_entries' in v:
+ msg = {'event_type': 'vcpe.dns.replaced_unexpired_entries',
+ 'message_id':six.text_type(uuid.uuid4()),
+ 'publisher_id': cpe_publisher_id,
+ 'timestamp':datetime.datetime.now().isoformat(),
+ 'priority':'INFO',
+ 'payload': {'vcpe_id':k,
+ 'user_id':keystone_user_id,
+ 'tenant_id':keystone_tenant_id,
+ 'replaced_unexpired_entries':v['replaced_unexpired_entries']
+ }
+ }
+ producer.publish(msg)
+ logger.debug('Publishing vcpe.dns.replaced_unexpired_entries event: %s', msg)
+
+ if 'queries_forwarded' in v:
+ msg = {'event_type': 'vcpe.dns.queries_forwarded',
+ 'message_id':six.text_type(uuid.uuid4()),
+ 'publisher_id': cpe_publisher_id,
+ 'timestamp':datetime.datetime.now().isoformat(),
+ 'priority':'INFO',
+ 'payload': {'vcpe_id':k,
+ 'user_id':keystone_user_id,
+ 'tenant_id':keystone_tenant_id,
+ 'queries_forwarded':v['queries_forwarded']
+ }
+ }
+ producer.publish(msg)
+ logger.debug('Publishing vcpe.dns.queries_forwarded event: %s', msg)
+
+ if 'queries_answered_locally' in v:
+ msg = {'event_type': 'vcpe.dns.queries_answered_locally',
+ 'message_id':six.text_type(uuid.uuid4()),
+ 'publisher_id': cpe_publisher_id,
+ 'timestamp':datetime.datetime.now().isoformat(),
+ 'priority':'INFO',
+ 'payload': {'vcpe_id':k,
+ 'user_id':keystone_user_id,
+ 'tenant_id':keystone_tenant_id,
+ 'queries_answered_locally':v['queries_answered_locally']
+ }
+ }
+ producer.publish(msg)
+ logger.debug('Publishing vcpe.dns.queries_answered_locally event: %s', msg)
+
+ if 'server_stats' in v:
+ for server in v['server_stats']:
+ msg = {'event_type': 'vcpe.dns.server.queries_sent',
+ 'message_id':six.text_type(uuid.uuid4()),
+ 'publisher_id': cpe_publisher_id,
+ 'timestamp':datetime.datetime.now().isoformat(),
+ 'priority':'INFO',
+ 'payload': {'vcpe_id':k,
+ 'user_id':keystone_user_id,
+ 'tenant_id':keystone_tenant_id,
+ 'upstream_server':server['id'],
+ 'queries_sent':server['queries_sent']
+ }
+ }
+ producer.publish(msg)
+ logger.debug('Publishing vcpe.dns.server.queries_sent event: %s', msg)
+
+ msg = {'event_type': 'vcpe.dns.server.queries_failed',
+ 'message_id':six.text_type(uuid.uuid4()),
+ 'publisher_id': cpe_publisher_id,
+ 'timestamp':datetime.datetime.now().isoformat(),
+ 'priority':'INFO',
+ 'payload': {'vcpe_id':k,
+ 'user_id':keystone_user_id,
+ 'tenant_id':keystone_tenant_id,
+ 'upstream_server':server['id'],
+ 'queries_failed':server['queries_failed']
+ }
+ }
+ producer.publish(msg)
+ logger.debug('Publishing vcpe.dns.server.queries_failed event: %s', msg)
+
+def periodic_publish():
+ publish_cpe_stats()
+ #Publish every 5minutes
+ threading.Timer(300, periodic_publish).start()
+
+def main(argv):
+ global keystone_tenant_id, keystone_user_id, rabbit_user, rabbit_password, rabbit_host, vcpeservice_rabbit_exchange
+ try:
+ opts, args = getopt.getopt(argv,"",["keystone_tenant_id=","keystone_user_id=","rabbit_host=","rabbit_user=","rabbit_password=","vcpeservice_rabbit_exchange="])
+ except getopt.GetoptError:
+ print 'vcpe_stats_notifier.py keystone_tenant_id=<keystone_tenant_id> keystone_user_id=<keystone_user_id> rabbit_host=<IP addr> rabbit_user=<user> rabbit_password=<password> vcpeservice_rabbit_exchange=<exchange name>'
+ sys.exit(2)
+ for opt, arg in opts:
+ if opt in ("--keystone_tenant_id"):
+ keystone_tenant_id = arg
+ elif opt in ("--keystone_user_id"):
+ keystone_user_id = arg
+ elif opt in ("--rabbit_user"):
+ rabbit_user = arg
+ elif opt in ("--rabbit_password"):
+ rabbit_password = arg
+ elif opt in ("--rabbit_host"):
+ rabbit_host = arg
+ elif opt in ("--vcpeservice_rabbit_exchange"):
+ vcpeservice_rabbit_exchange = arg
+ logger.info("vcpe_stats_notifier args:keystone_tenant_id=%s keystone_user_id=%s rabbit_user=%s rabbit_host=%s vcpeservice_rabbit_exchange=%s",keystone_tenant_id,keystone_user_id,rabbit_user,rabbit_host,vcpeservice_rabbit_exchange)
+ setup_rabbit_mq_channel()
+ periodic_publish()
+
+if __name__ == "__main__":
+ main(sys.argv[1:])
diff --git a/xos/observers/vtn/model-deps b/xos/observers/vtn/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/observers/vtn/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/observers/vtn/run.sh b/xos/observers/vtn/run.sh
new file mode 100755
index 0000000..d7e5025
--- /dev/null
+++ b/xos/observers/vtn/run.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+python vtn-observer.py -C $XOS_DIR/observers/vtn/vtn_observer_config
diff --git a/xos/observers/vtn/start.sh b/xos/observers/vtn/start.sh
new file mode 100755
index 0000000..168074d
--- /dev/null
+++ b/xos/observers/vtn/start.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+nohup python vtn-observer.py -C $XOS_DIR/observers/vtn/vtn_observer_config > /dev/null 2>&1 &
diff --git a/xos/observers/vtn/steps/sync_tenant.py b/xos/observers/vtn/steps/sync_tenant.py
new file mode 100644
index 0000000..77e6a93
--- /dev/null
+++ b/xos/observers/vtn/steps/sync_tenant.py
@@ -0,0 +1,95 @@
+import os
+import requests
+import socket
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep
+from core.models import Service
+from core.models.service import COARSE_KIND
+from services.cord.models import Tenant
+from util.logger import Logger, logging
+from requests.auth import HTTPBasicAuth
+
+# hpclibrary will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+logger = Logger(level=logging.INFO)
+
+# XXX should save and load this
+glo_saved_vtn_maps = []
+
+class SyncTenant(SyncStep):
+ provides=[Tenant]
+ observes=Tenant
+ requested_interval=0
+
+ def __init__(self, **args):
+ SyncStep.__init__(self, **args)
+
+ def get_vtn_onos_service(self):
+# vtn_tenant = Tenant.objects.filter(name="VTN_ONOS_app") # XXX fixme - hardcoded
+# if not vtn_tenant:
+# raise "No VTN Onos App found"
+# vtn_tenant = vtn_tenant[0]
+#
+# vtn_service = vtn_tenant.provider_service
+ vtn_service = Service.objects.filter(name="service_ONOS_VTN") # XXX fixme - harcoded
+ if not vtn_service:
+ raise "No VTN Onos Service"
+
+ return vtn_service[0]
+
+ def get_vtn_addr(self):
+ vtn_service = self.get_vtn_onos_service()
+
+ if not vtn_service.slices.exists():
+ raise "VTN Service has no slices"
+
+ vtn_slice = vtn_service.slices.all()[0]
+
+ if not vtn_slice.instances.exists():
+ raise "VTN Slice has no instances"
+
+ vtn_instance = vtn_slice.instances.all()[0]
+
+ return vtn_instance.node.name
+
+ def call(self, **args):
+ global glo_saved_vtn_maps
+
+ logger.info("sync'ing vtn services")
+
+ vtn_maps = []
+ for service in Service.objects.all():
+ for id in service.get_vtn_src_ids():
+ dependencies = service.get_vtn_dependencies_ids()
+ if dependencies:
+ for dependency in dependencies:
+ vtn_maps.append( (id, dependency) )
+
+ for vtn_map in vtn_maps:
+ if not (vtn_map in glo_saved_vtn_maps):
+ # call vtn rest api to add map
+ url = "http://" + self.get_vtn_addr() + ":8181/onos/cordvtn/service-dependency/%s/%s" % (vtn_map[0], vtn_map[1])
+
+ print "POST %s" % url
+ r = requests.post(url, auth=HTTPBasicAuth('karaf', 'karaf') ) # XXX fixme - hardcoded auth
+ if (r.status_code != 200):
+ raise Exception("Received error from vtn service (%d)" % r.status_code)
+
+ for vtn_map in glo_saved_vtn_maps:
+ if not vtn_map in vtn_maps:
+ # call vtn rest api to delete map
+ url = "http://" + self.get_vtn_addr() + ":8181/onos/cordvtn/service-dependency/%s/%s" % (vtn_map[0],vtn_map[1])
+
+ print "DELETE %s" % url
+ r = requests.delete(url, auth=HTTPBasicAuth('karaf', 'karaf') ) # XXX fixme - hardcoded auth
+ if (r.status_code != 200):
+ raise Exception("Received error from vtn service (%d)" % r.status_code)
+
+ glo_saved_vtn_maps = vtn_maps
+ # TODO: save this
+
diff --git a/xos/observers/vtn/stop.sh b/xos/observers/vtn/stop.sh
new file mode 100755
index 0000000..261f029
--- /dev/null
+++ b/xos/observers/vtn/stop.sh
@@ -0,0 +1 @@
+pkill -9 -f vtn-observer.py
diff --git a/xos/observers/vtn/supervisor/vtn-observer.conf b/xos/observers/vtn/supervisor/vtn-observer.conf
new file mode 100644
index 0000000..714afa7
--- /dev/null
+++ b/xos/observers/vtn/supervisor/vtn-observer.conf
@@ -0,0 +1,2 @@
+[program:vtn-observer]
+command=python /opt/xos/observers/vbng/vtn-observer.py -C /opt/xos/observers/vbng/vtn_observer_config
diff --git a/xos/observers/vtn/vtn-observer.py b/xos/observers/vtn/vtn-observer.py
new file mode 100755
index 0000000..d6a71ff
--- /dev/null
+++ b/xos/observers/vtn/vtn-observer.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+# This imports and runs ../../xos-observer.py
+
+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/vtn/vtn_observer_config b/xos/observers/vtn/vtn_observer_config
new file mode 100644
index 0000000..19e9a39
--- /dev/null
+++ b/xos/observers/vtn/vtn_observer_config
@@ -0,0 +1,38 @@
+
+[plc]
+name=plc
+deployment=VICCI
+
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+[api]
+host=128.112.171.237
+port=8000
+ssl_key=None
+ssl_cert=None
+ca_ssl_cert=None
+ratelimit_enabled=0
+omf_enabled=0
+mail_support_address=support@localhost
+nova_enabled=True
+
+[observer]
+name=vtn
+dependency_graph=/opt/xos/observers/vtn/model-deps
+steps_dir=/opt/xos/observers/vtn/steps
+sys_dir=/opt/xos/observers/vtn/sys
+deleters_dir=/opt/xos/observers/vtn/deleters
+log_file=console
+#/var/log/hpc.log
+driver=None
+pretend=False
+backoff_disabled=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'