Metronet Local Service

Change-Id: I92e13f49bbdfc60d27496b3c11207a72310731d4
diff --git a/xos/__init__.py b/xos/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/__init__.py
diff --git a/xos/admin.py b/xos/admin.py
new file mode 100644
index 0000000..5ffc243
--- /dev/null
+++ b/xos/admin.py
@@ -0,0 +1,47 @@
+from core.admin import XOSBaseAdmin
+from django.contrib import admin
+from services.vnodlocal.models import *
+from django import forms
+
+class VnodLocalServiceAdmin(XOSBaseAdmin):
+    verbose_name = "VNOD Local Service"
+    verbose_name_plural = "VNOD Local Services"
+    list_display = ('servicehandle', 'portid', 'vlanid', 'administrativeState', 'operstate', 'autoattached')
+    list_display_links = ('servicehandle', 'portid', 'vlanid', 'administrativeState', 'operstate', 'autoattached')
+
+    fields = ('id', 'servicehandle', 'portid', 'vlanid', 'administrativeState', 'operstate', 'autoattached')
+    readonly_fields = ('id','autoattached')
+
+
+class VnodLocalSystemAdminForm(forms.ModelForm):
+
+    password = forms.CharField(required=False, widget = forms.PasswordInput(render_value=True))
+
+    class Meta:
+        model = VnodLocalSystem
+        fields = '__all__'
+
+class VnodLocalSystemAdmin(XOSBaseAdmin):
+    verbose_name = "VNOD Local System"
+    verbose_name_plural = "VNOD Local Systems"
+    form = VnodLocalSystemAdminForm
+    list_display = ('name', 'administrativeState', 'restUrl', 'username', 'pseudowireprovider', 'networkControllerUrl')
+    list_display_links = ('name', 'administrativeState', 'restUrl', 'username', 'pseudowireprovider', 'networkControllerUrl')
+
+    fields = ('name', 'administrativeState', 'restUrl', 'username', 'password', 'pseudowireprovider', 'networkControllerUrl')
+
+class VnodLocalPseudowireConnectorServiceAdmin(XOSBaseAdmin):
+    verbose_name = "VNOD Local Pseudowire Connector Service"
+    verbose_name_plural = "VNOD Local Pseudowire Connector Service"
+    list_display = ('servicehandle', 'internalport', 'pseudowirehandle','vnodlocal', 'administrativeState', 'operstate')
+    list_display_links = ('servicehandle', 'internalport', 'pseudowirehandle','vnodlocal', 'administrativeState', 'operstate')
+
+    fields = ('servicehandle', 'internalport', 'pseudowirehandle','vnodlocal', 'administrativeState', 'operstate')
+    readonly_fields = ('vnodlocal', 'operstate', 'pseudowirehandle')
+
+
+admin.site.register(VnodLocalSystem, VnodLocalSystemAdmin)
+admin.site.register(VnodLocalService, VnodLocalServiceAdmin)
+admin.site.register(VnodLocalPseudowireConnectorService, VnodLocalPseudowireConnectorServiceAdmin)
+
+
diff --git a/xos/api/service/vnodlocalservice/vnodlocalservice.py b/xos/api/service/vnodlocalservice/vnodlocalservice.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/api/service/vnodlocalservice/vnodlocalservice.py
diff --git a/xos/make_synchronizer_manifest.sh b/xos/make_synchronizer_manifest.sh
new file mode 100755
index 0000000..4058982
--- /dev/null
+++ b/xos/make_synchronizer_manifest.sh
@@ -0,0 +1,2 @@
+#! /bin/bash
+find synchronizer -type f | cut -b 14- > synchronizer/manifest 
diff --git a/xos/models.py b/xos/models.py
new file mode 100644
index 0000000..1484129
--- /dev/null
+++ b/xos/models.py
@@ -0,0 +1,167 @@
+# models.py -  VNOD Local Service
+
+from django.db import models
+from core.models import Service
+from core.models import PlCoreBase
+
+VNODLOCAL_KIND = "vnodlocal"
+SERVICE_NAME = 'vnodlocal'
+
+class VnodLocalSystem(PlCoreBase):
+    class Meta:
+        app_label = VNODLOCAL_KIND
+        verbose_name = "VNOD Local System"
+
+    ADMINISTRATIVE_STATE = (
+        ('enabled', 'Enabled'),
+        ('disabled', 'Disabled')
+    )
+
+    name = models.CharField(unique=True,
+                        verbose_name="Name",
+                        max_length=256,
+                        editable=True)
+
+    description = models.CharField(verbose_name="Description",
+                               max_length=1024,
+                               editable=True)
+
+    restUrl = models.CharField(verbose_name="MetroNetwork Rest URL",
+                           max_length=256,
+                           editable=True)
+
+    username = models.CharField(verbose_name='Username',
+                                max_length=32,
+                                editable=True,
+                                blank=True)
+
+    password = models.CharField(max_length=32,
+                                verbose_name='Password',
+                                editable=True,
+                                blank=True)
+
+    administrativeState = models.CharField(choices=ADMINISTRATIVE_STATE,
+                                       default='enabled',
+                                       verbose_name="AdministrativeState",
+                                       max_length=16,
+                                       editable=True)
+
+    pseudowireprovider = models.CharField(unique=False,
+                            verbose_name="Pseudowire Provider",
+                            default='none',
+                            max_length=256,
+                            editable=True)
+
+    networkControllerUrl = models.CharField(verbose_name="Network Controller URL",
+                                          blank=True,
+                                          max_length=256,
+                                          editable=True)
+
+    def __init__(self, *args, **kwargs):
+        super(VnodLocalSystem, self).__init__(*args, **kwargs)
+
+
+    def getAdminstrativeState(self):
+        return self.administrativeState
+
+
+    def setAdminstrativeState(self, value):
+        self.administrativeState = value
+
+
+    def getRestUrl(self):
+        return self.restUrl
+
+
+class VnodLocalService(Service):
+
+    class Meta:
+        app_label = VNODLOCAL_KIND
+        verbose_name = "Virtual Network On Demand Local Service"
+
+    ADMINISTRATIVE_STATE = (
+        ('disabled', 'Disabled'),
+        ('configurationrequested', 'ConfigurationRequested'),
+        ('configurationfailed', 'ConfigurationFailed'),
+        ('configured', 'Configured'),
+        ('activationrequested', 'ActivationRequested'),
+        ('activationfailed', 'ActivationFailed'),
+        ('enabled', 'Enabled'),
+        ('deactivationrequested', 'DeactivationRequested')
+    )
+
+    OPERATIONALSTATE = (
+        ('active', 'Active'),
+        ('inactivereported', 'InactiveReported'),
+        ('inactive', 'Inactive'),
+        ('activereported', 'ActiveReported')
+    )
+
+    portid = models.CharField(verbose_name="PortId", blank=True, max_length=256, editable=True)
+    vlanid = models.CharField(verbose_name="VlanId", blank=True, max_length=256, editable=True)
+    servicehandle = models.CharField(verbose_name="Service Handle", max_length=256, editable=True)
+    autoattached = models.BooleanField(verbose_name="Auto-Attached", default=False, editable=True)
+
+    administrativeState = models.CharField(choices=ADMINISTRATIVE_STATE,
+                                           default='disabled',
+                                           verbose_name="AdministrativeState",
+                                           max_length=64,
+                                           editable=True)
+
+    operstate = models.CharField(choices=OPERATIONALSTATE,
+                                 default='inactive',
+                                 verbose_name="OperationalState",
+                                 max_length=64,
+                                 editable=True)
+
+
+    def __init__(self, *args, **kwargs):
+        super(VnodLocalService, self).__init__(*args, **kwargs)
+
+    def __unicode__(self):  return u'%s:%s' % (self.servicehandle, self.portid)
+
+
+class VnodLocalPseudowireConnectorService(Service):
+
+    class Meta:
+        app_label = VNODLOCAL_KIND
+        verbose_name = "Virtual Network On Demand Local Pseudo-wire Connector Service"
+
+    ADMINISTRATIVE_STATE = (
+        ('disabled', 'Disabled'),
+        ('activationrequested', 'ActivationRequested'),
+        ('enabled', 'Enabled'),
+        ('deactivationrequested', 'DeactivationRequested')
+    )
+
+    OPERATIONALSTATE = (
+        ('active', 'Active'),
+        ('inactive', 'Inactive')
+    )
+
+    servicehandle = models.CharField(verbose_name="Service Handle", max_length=256, editable=True)
+    pseudowirehandle = models.CharField(verbose_name="Pseudowirehandle", blank=True, max_length=256, editable=True)
+    internalport = models.CharField(verbose_name="Internal Port", max_length=256, editable=True)
+
+    vnodlocal = models.ForeignKey(VnodLocalService,
+                                  related_name='VnodLocalService',
+                                  verbose_name="VnodLocalService",
+                                  null=True,
+                                  editable=True,
+                                  on_delete=models.CASCADE)
+
+    administrativeState = models.CharField(choices=ADMINISTRATIVE_STATE,
+                                           default='disabled',
+                                           verbose_name="AdministrativeState",
+                                           max_length=64,
+                                           editable=True)
+
+    operstate = models.CharField(choices=OPERATIONALSTATE,
+                                 default='inactive',
+                                 verbose_name="OperationalState",
+                                 max_length=64,
+                                 editable=True)
+
+
+    def __init__(self, *args, **kwargs):
+        super(VnodLocalPseudowireConnectorService, self).__init__(*args, **kwargs)
diff --git a/xos/synchronizer/__init__.py b/xos/synchronizer/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/synchronizer/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/synchronizer/manifest b/xos/synchronizer/manifest
new file mode 100644
index 0000000..fc4907c
--- /dev/null
+++ b/xos/synchronizer/manifest
@@ -0,0 +1,14 @@
+pseudowireproviders/providerfactory.py
+pseudowireproviders/metronetworkpseudowireprovider.py
+pseudowireproviders/__init__.py
+pseudowireproviders/pseudowireprovider.py
+vnodlocal-synchronizer.py
+__init__.py
+model-deps
+vnodlocal-synchronizer-devel.py
+run_devel.sh
+manifest
+steps/sync_vnodlocalpseudowireconnectorservice.py
+steps/sync_vnodlocalservice.py
+vnodlocal_synchronizer_config
+run.sh
\ No newline at end of file
diff --git a/xos/synchronizer/model-deps b/xos/synchronizer/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/synchronizer/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/synchronizer/pseudowireproviders/__init__.py b/xos/synchronizer/pseudowireproviders/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/synchronizer/pseudowireproviders/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/synchronizer/pseudowireproviders/metronetworkpseudowireprovider.py b/xos/synchronizer/pseudowireproviders/metronetworkpseudowireprovider.py
new file mode 100644
index 0000000..f2762e2
--- /dev/null
+++ b/xos/synchronizer/pseudowireproviders/metronetworkpseudowireprovider.py
@@ -0,0 +1,66 @@
+from xos.logger import Logger, logging
+from synchronizers.vnodlocal.pseudowireproviders.pseudowireprovider import PseudowireProvider
+from services.metronetwork.models import NetworkEdgeToEdgePointConnection, NetworkEdgePort
+
+logger = Logger(level=logging.INFO)
+
+class MetronetworkPseudowireProvider(PseudowireProvider):
+
+    def __init__(self, **args):
+        pass
+
+    # Methods to support creation
+    #
+    # Returns: handle
+    #
+    def create(self, port1, port2, vlanid, psuedowireservice):
+        # Create method - create eline with the ports
+        # Vlan is TBD
+        pseudowirename = ("port1: %s, port2: %s, vlan: %s" % (port1, port2, vlanid))
+        logger.info("Metronetwork create called, name: %s" % pseudowirename )
+        # Edge to Edge Point Connectivity creation
+        edgetoedgeconnectivity = NetworkEdgeToEdgePointConnection()
+        uni1port = NetworkEdgePort.objects.filter(pid__icontains=port1)
+        if uni1port:
+            uni1port = uni1port[0]
+        uni2port = NetworkEdgePort.objects.filter(pid__icontains=port2)
+        if uni2port:
+            uni2port = uni2port[0]
+        edgetoedgeconnectivity.uni1 = uni1port
+        edgetoedgeconnectivity.uni2 = uni2port
+        edgetoedgeconnectivity.vlanid = vlanid
+        edgetoedgeconnectivity.type = 'Point_To_Point'
+        edgetoedgeconnectivity.operstate = 'inactive'
+        edgetoedgeconnectivity.adminstate = 'disabled'
+        edgetoedgeconnectivity.sid = pseudowirename
+        edgetoedgeconnectivity.name = 'Metronetwork'
+        edgetoedgeconnectivity.save()
+        return pseudowirename
+
+    # Method to support connect
+    #
+    def connect(self, handle):
+        # Connect method - simply transition the state of the underlying object - the Metronet sync will do the rest
+        logger.info("Metronetwork Pseudowire connect called, handle = %s" % handle)
+        edgetoedgeconnectivity = NetworkEdgeToEdgePointConnection.objects.get(sid=handle)
+        edgetoedgeconnectivity.adminstate = 'activationrequested'
+        edgetoedgeconnectivity.save()
+
+    # Method to support disconnect connect
+    #
+    def disconnect(self, handle):
+        # Connect method - simply transition the state of the underlying object - the Metronet sync will do the rest
+        logger.info("Metronetwork Pseudowire disconnect called, handle = %s" % handle)
+        edgetoedgeconnectivity = NetworkEdgeToEdgePointConnection.objects.get(sid=handle)
+        edgetoedgeconnectivity.adminstate = 'deactivationrequested'
+        edgetoedgeconnectivity.save()
+
+    # Method to support deletion
+    #
+    def delete(self, handle):
+        # Delete method - simply set the state to deleted and the Metronet sync will do the rest
+        logger.info("Metronetwork Pseudowire delete called, handle = %s" % handle)
+        edgetoedgeconnectivity = NetworkEdgeToEdgePointConnection.objects.get(sid=handle)
+        edgetoedgeconnectivity.deleted = True
+        edgetoedgeconnectivity.save()
+
diff --git a/xos/synchronizer/pseudowireproviders/providerfactory.py b/xos/synchronizer/pseudowireproviders/providerfactory.py
new file mode 100644
index 0000000..6735ecc
--- /dev/null
+++ b/xos/synchronizer/pseudowireproviders/providerfactory.py
@@ -0,0 +1,24 @@
+import sys
+
+from services.vnodlocal.models import VnodLocalSystem
+from synchronizers.vnodlocal.pseudowireproviders.metronetworkpseudowireprovider import MetronetworkPseudowireProvider
+from synchronizers.vnodlocal.pseudowireproviders.segmentroutingvlanxconnectpseudowireprovider import SegmentRoutingVlanXconnectPseudowireProvider
+
+
+class ProviderFactory(object):
+    @staticmethod
+    def getprovider():
+
+        # We look up the VnodLocal Configuration to see what to do
+        vnodlocalsystems = VnodLocalSystem.objects.all()
+        if not vnodlocalsystems:
+            return None
+
+        vnodlocalsystem = vnodlocalsystems[0]
+
+        if vnodlocalsystem.pseudowireprovider == 'metronetwork':
+            return MetronetworkPseudowireProvider()
+        elif vnodlocalsystem.pseudowireprovider == 'segmentroutingxconnect':
+            return SegmentRoutingVlanXconnectPseudowireProvider()
+        else:
+            return None
\ No newline at end of file
diff --git a/xos/synchronizer/pseudowireproviders/pseudowireprovider.py b/xos/synchronizer/pseudowireproviders/pseudowireprovider.py
new file mode 100644
index 0000000..5107499
--- /dev/null
+++ b/xos/synchronizer/pseudowireproviders/pseudowireprovider.py
@@ -0,0 +1,36 @@
+from xos.logger import Logger, logging
+
+logger = Logger(level=logging.INFO)
+
+
+class PseudowireProvider(object):
+
+    def __init__(self, **args):
+        pass
+
+    # Methods to support creation
+    #
+    # Returns: handle
+    #
+    def create(self, port1, port2, vlanid, pseudowireservice):
+        # Default method needs to be overriden
+        logger.info("create called - should be overriden")
+
+    # Method to support connection
+    #
+    def connect(self, handle):
+        # Default method needs to be overriden
+        logger.info("connect called - should be overriden")
+        return None
+
+    # Method to support disconnection
+    #
+    def disconnect(self, handle):
+        # Default method needs to be overriden
+        logger.info("discoconnect called - should be overriden")
+
+    # Methods to support deletion
+    #
+    def delete(self, handle):
+        # Default method needs to be overriden
+        logger.info("delete called - should be overriden")
\ No newline at end of file
diff --git a/xos/synchronizer/pseudowireproviders/segmentroutingvlanxconnectpseudowireprovider.py b/xos/synchronizer/pseudowireproviders/segmentroutingvlanxconnectpseudowireprovider.py
new file mode 100644
index 0000000..b98e1c5
--- /dev/null
+++ b/xos/synchronizer/pseudowireproviders/segmentroutingvlanxconnectpseudowireprovider.py
@@ -0,0 +1,94 @@
+from xos.logger import Logger, logging
+from synchronizers.vnodlocal.pseudowireproviders.pseudowireprovider import PseudowireProvider
+from services.metronetwork.models import NetworkEdgeToEdgePointConnection, NetworkEdgePort
+
+import requests, json
+from requests.auth import HTTPBasicAuth
+
+logger = Logger(level=logging.INFO)
+
+class SegmentRoutingVlanXconnectPseudowireProvider(PseudowireProvider):
+
+    def __init__(self, **args):
+        pass
+
+    # Methods to support creation
+    #
+    # Returns: handle
+    #
+    def create(self, port1, port2, vlanid, pseudowireservice):
+        # Create method - create xconnect
+        # Vlan is TBD
+        pseudowirename = ("port1: %s, port2: %s, vlan: %s" % (port1, port2, vlanid))
+        logger.info("SegmentRoutingXConnect create called, name: %s" % pseudowirename )
+
+        # Pull out Ports from FQN
+
+
+        # Pull out Device from FQN
+        # Use default user/password?
+
+        # curl --user onos:rocks -X POST -H "Content-Type: application/json" http://138.120.151.126:8181/onos/v1/network/configuration/apps/org.onosproject.segmentrouting/xconnect -d '{ "of:0000000000000001" : [{"vlan" : "100", "ports" : [1, 2], "name" : "Mike"}] }'
+
+        # Port 1 Device and Num
+        port1IdToken = port1.split('/', 1)
+        port1Devicename = port1IdToken[0]
+        port1Num = port1IdToken[1]
+
+        # Port 2 Device and Num
+        port2IdToken = port2.split('/', 1)
+        port2Devicename = port2IdToken[0]
+        port2Num = port2IdToken[1]
+
+        # Lets make sure the Devices are the same - otherwise its an error - Xconnect must be on same device
+
+        if (port1Devicename != port2Devicename):
+            Exception("XConnect Device must be the same. D1= % D2=%" % (port1Devicename, port2Devicename))
+
+        # Get URL from PwaaS Ojbect
+        restCtrlUrl = pseudowireservice.networkControllerUrl
+
+        data = {
+            port2Devicename : [
+                {
+                    "vlan" : vlanid,
+                    "ports" : [port1Num, port2Num],
+                    "name" : pseudowirename
+                }
+            ]
+           }
+
+        headers = {'Content-Type': 'application/json'}
+
+        resp = requests.post('{}/v1/network/configuration/apps/org.onosproject.segmentrouting/xconnect'.format(restCtrlUrl),
+                             data=json.dumps(data), headers=headers, auth=HTTPBasicAuth('karaf', 'karaf'))
+
+        if resp.status_code == 200:
+            logger.info("SegmentRoutingXConnect create successful")
+            return pseudowirename
+        else:
+            Exception("Pseudowire create failed Error Code: %s" % resp.status_code)
+
+
+    # Method to support connect
+    #
+    def connect(self, handle):
+        # Connect method - this is a no-op for this module, it does not support a complext state machine
+        logger.info("SegmentRoutingXConnect Pseudowire connect called, handle = %s" % handle)
+
+    # Method to support disconnect connect
+    #
+    def disconnect(self, handle):
+        # Disconnect method - impl is TBD
+        logger.info("SegmentRoutingXConnect Pseudowire disconnect called, handle = %s" % handle)
+
+        # Example command line syntax:
+        # curl --user onos:rocks -X DELETE http://138.120.151.126:8181/onos/v1/network/configuration/apps/org.onosproject.segmentrouting/xconnect
+
+    # Method to support deletion
+    #
+    def delete(self, handle):
+        # Delete method - impl is TBD
+        logger.info("SegmentRoutingXConnect Pseudowire delete called, handle = %s" % handle)
+
+
diff --git a/xos/synchronizer/run.sh b/xos/synchronizer/run.sh
new file mode 100755
index 0000000..43c5cc5
--- /dev/null
+++ b/xos/synchronizer/run.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+python vnodlocal-synchronizer.py  -C $XOS_DIR/synchronizers/vnodlocal/vnodlocal_synchronizer_config
diff --git a/xos/synchronizer/run_devel.sh b/xos/synchronizer/run_devel.sh
new file mode 100755
index 0000000..2575b2c
--- /dev/null
+++ b/xos/synchronizer/run_devel.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+python vnodlocal-synchronizer-devel.py  -C $XOS_DIR/synchronizers/vnodlocal/vnodlocal_synchronizer_config
diff --git a/xos/synchronizer/steps/sync_vnodlocalpseudowireconnectorservice.py b/xos/synchronizer/steps/sync_vnodlocalpseudowireconnectorservice.py
new file mode 100644
index 0000000..97b1604
--- /dev/null
+++ b/xos/synchronizer/steps/sync_vnodlocalpseudowireconnectorservice.py
@@ -0,0 +1,196 @@
+import os
+import sys
+
+from synchronizers.base.syncstep import SyncStep
+from synchronizers.vnodlocal.pseudowireproviders.providerfactory import ProviderFactory
+from services.vnodlocal.models import *
+
+from xos.logger import Logger, logging
+
+# vnod local will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+logger = Logger(level=logging.INFO)
+
+
+class SyncVnodLocalPseudowireConnectorServiceSystem(SyncStep):
+    provides = [VnodLocalPseudowireConnectorService]
+    observes = VnodLocalPseudowireConnectorService
+    requested_interval = 0
+    initialized = False
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+
+    def fetch_pending(self, deletion=False):
+        logger.info("VnodLocalPseudowireConnector fetch pending called")
+
+        # Some comments to replace as we write the code
+
+        #    The AdministrativeState state machine:
+        #
+        #
+        #                     Disabled---------DeactivationRequested
+        #                         \                      |
+        #               ActivationRequested              |
+        #               /      /        \                |
+        #              /      /          \               |
+        #     ActivationFailed         Enabled -----------
+        #
+        #
+
+        #  The  OperationalState state machine
+        #
+        #           active
+        #              |
+        #          inactive
+
+        objs = []
+
+
+        # The whole thing needs to be conditional on the VnodLocalSystem existing and being 'enabled'
+        # This is the 'kill switch' in the system that is the first thing to check
+        vnodlocalsystem = self.get_vnodlocal_system()
+        if not vnodlocalsystem:
+            logger.debug("No VnodLocal System Configured, skipping sync")
+            return objs
+
+        # Check to make sure the Metro Network System is enabled
+        if vnodlocalsystem.administrativeState == 'disabled':
+            # Nothing to do
+            logger.debug("VnodLocal System configured - state is Disabled, skipping sync")
+            return objs
+
+
+
+        # Handle call when deletion is False
+        if deletion is False:
+
+            # Check for admin status 'ActivationRequested'
+            activationreqs = VnodLocalPseudowireConnectorService.objects.filter(administrativeState='activationrequested')
+            for activationreq in activationreqs:
+                # Handle the case where we don't yet have a VnodLocalSerive
+                if activationreq.vnodlocal is None:
+                    # Create VnodLocalService
+                    # We save the changes right here in this case to avoid having to to 'pre-save' semnatics
+                    # to cover the foreign key
+                    vnodlocalservice = VnodLocalService()
+                    vnodlocalservice.servicehandle = activationreq.servicehandle
+                    vnodlocalservice.administrativeState = 'configurationrequested'
+                    vnodlocalservice.save()
+                    activationreq.vnodlocal = vnodlocalservice
+                    activationreq.save()
+                elif activationreq.vnodlocal.administrativeState == 'configured':
+                    # Once the underlying VnodLocal is configured then activated it
+                    vnodlocalservice = activationreq.vnodlocal
+                    # Call our underlying provider to connect the pseudo wire
+                    self.activate_pseudowire(activationreq, vnodlocalsystem)
+                    activationreq.administrativeState = 'enabled'
+                    activationreq.operstate = 'active'
+                    objs.append(activationreq)
+                    vnodlocalservice.administrativeState = 'activationrequested'
+                    vnodlocalservice.operstate = 'activereported'
+                    objs.append(vnodlocalservice)
+
+
+            # Check for admin status 'DeactivationRequested'
+            deactivationreqs = VnodLocalPseudowireConnectorService.objects.filter(administrativeState='deactivationrequested')
+            for deactivationreq in deactivationreqs:
+                # Call the XOS Interface to de-actiavte the spoke
+                logger.debug("Attempting to de-activate VnodLocalService servicehandle: %s" % deactivationreq.servicehandle)
+                # De-activate the underlying service
+                vnodlocalservice = deactivationreq.vnodlocal
+                # Call our underlying provider to connect the pseudo wire
+                self.deactivate_pseudowire(deactivationreq)
+                deactivationreq.administrativeState = 'disabled'
+                deactivationreq.operstate = 'inactive'
+                objs.append(deactivationreq)
+                vnodlocalservice.administrativeState = 'deactivationrequested'
+                objs.append(vnodlocalservice)
+
+
+        elif deletion:
+            # Apply Deletion Semantics:
+            logger.debug("Applying Deletion Semanctics")
+            # TODO: Figure out the odd scenario of Service deletion
+            deletedobjs = VnodLocalPseudowireConnectorService.deleted_objects.all()
+
+            # Delete the underlying VnodLocalService objects
+            for deletedobj in deletedobjs:
+                # Set the VnodLocal to Deleted - its Synchronizer will take care of deletion
+                vnodlocalobj = deletedobj.vnodlocal
+                vnodlocalobj.deleted = True
+                vnodlocalobj.save()
+                # Delete the underlying pseudowire
+                self.delete_pseudowire(deletedobj)
+                # Finally - add the Service for deletion
+                objs.append(deletedobj)
+
+        # Finally just return the set of changed objects
+        return objs
+
+
+    def sync_record(self, o):
+
+        # Simply save the record to the DB - both updates and adds are handled the same way
+        o.save()
+
+
+    def delete_record(self, o):
+        # Overriden to customize our behaviour - the core sync step for will remove the record directly
+        # We just log and return
+        logger.debug("deleting Object %s" % str(o), extra=o.tologdict())
+
+    def get_vnodlocal_system(self):
+        # We only expect to have one of these objects in the system in the curent design
+        # So get the first element from the query
+        vnodlocalsystems = VnodLocalSystem.objects.all()
+        if not vnodlocalsystems:
+            return None
+
+        return vnodlocalsystems[0]
+
+    def activate_pseudowire(self, o, vnodlocalsystem):
+        # Call the underlying pseudowire provicers and call
+        logger.debug("activating pseudowire %s" % o.servicehandle)
+
+        pseudowireprovier = ProviderFactory.getprovider()
+
+        if pseudowireprovier is not None:
+            # Pass it the two ports - the internal port configured on the Pseudowire and the NNI port from
+            # the VnodLocal
+            if o.pseudowirehandle == '':
+                o.pseudowirehandle = pseudowireprovier.create(o.internalport, o.vnodlocal.portid, o.vnodlocal.vlanid, vnodlocalsystem)
+
+            # handle already exists - just connect it
+            pseudowireprovier.connect(o.pseudowirehandle)
+        else:
+            # No Provider configured - lets put a handle that reflects thsi
+            o.pseudowirehandle = 'No Pseudowire Provider configured'
+
+
+    def deactivate_pseudowire(self, o):
+        # Call the underlying pseudowire provicers and call
+        logger.debug("deactivating pseudowire %s" % o.servicehandle)
+
+        pseudowireprovier = ProviderFactory.getprovider()
+
+        if pseudowireprovier is not None:
+            # Pass it the handle
+            pseudowireprovier.disconnect(o.pseudowirehandle)
+
+
+    def delete_pseudowire(self, o):
+        # Call the underlying pseudowire provicers and call
+        logger.debug("deleting pseudowire %s" % o.servicehandle)
+
+        pseudowireprovier = ProviderFactory.getprovider()
+
+        if pseudowireprovier is not None:
+            # Pass it the handle
+            if o.pseudowirehandle != '':
+                pseudowireprovier.delete(o.pseudowirehandle)
+
+        # Either way blank out the handle name
+        o.pseudowirehandle = ''
diff --git a/xos/synchronizer/steps/sync_vnodlocalservice.py b/xos/synchronizer/steps/sync_vnodlocalservice.py
new file mode 100644
index 0000000..90e2ded
--- /dev/null
+++ b/xos/synchronizer/steps/sync_vnodlocalservice.py
@@ -0,0 +1,319 @@
+import os
+import sys
+
+from synchronizers.base.syncstep import SyncStep
+from services.vnodlocal.models import *
+import requests, json
+from requests.auth import HTTPBasicAuth
+
+from xos.logger import Logger, logging
+
+# vnod local will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+logger = Logger(level=logging.INFO)
+
+
+class SyncVnodLocalSystem(SyncStep):
+    provides = [VnodLocalService]
+    observes = VnodLocalService
+    requested_interval = 0
+    initialized = False
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+
+    def fetch_pending(self, deletion=False):
+        logger.info("VnodLocal fetch pending called")
+
+        # Some comments to replace as we write the code
+
+        #    The AdministrativeState state machine:
+        #
+        #               Diasabled (initial)
+        #                    |
+        #             ConfigurationRequested
+        #             /      /            \
+        #            /      /              \
+        #      ConfigurationFailed      Configured---------DeactivationRequested
+        #                                   \                      |
+        #                         ActivationRequested              |
+        #                         /      /        \                |
+        #                        /      /          \               |
+        #               ActivationFailed         Enabled -----------
+        #
+        #
+
+        #  The  OperationalState state machine
+        #
+        #           active-----------------|
+        #              |                   |
+        #       inactivereported           |
+        #              |                   |
+        #          inactive----------activereported
+
+        objs = []
+
+
+        # The whole thing needs to be conditional on the VnodLocalSystem existing and being 'enabled'
+        # This is the 'kill switch' in the system that is the first thing to check
+        vnodlocalsystem = self.get_vnodlocal_system()
+
+        if not vnodlocalsystem:
+            logger.debug("No VnodLocal System Configured, skipping sync")
+            return objs
+
+        # Check to make sure the Metro Network System is enabled
+        if vnodlocalsystem.administrativeState == 'disabled':
+            # Nothing to do
+            logger.debug("VnodLocal System configured - state is Disabled, skipping sync")
+            return objs
+
+
+
+        # Handle call when deletion is False
+        if deletion is False:
+
+            # First Part of Auto-attachement: What we need to do is ask the ECORD if there are any Spokes for our site
+            # that are set to 'auto-attached' but are not currently actually attached
+            # it will send back a list of servicehandles that meet that criteria. We will simply
+            # check if we have already created a VnodLocal for that service handle, if we have do
+            # nothing it should be still in progress. If we haven't create it, mark it as 'autoattached', set the
+            # servicehandle and mark it as 'ConfigurationRequested'
+            rest_url = vnodlocalsystem.restUrl
+            sitename = vnodlocalsystem.name
+            username = vnodlocalsystem.username
+            password = vnodlocalsystem.password
+
+            autoattachhandles = self.get_autoattachhandles(vnodlocalsystem)
+            for autoattachhandle in autoattachhandles:
+                # Check to see if it already exists - if not add it
+                if not VnodLocalService.objects.filter(servicehandle=autoattachhandle).exists():
+                    vnodlocal = VnodLocalService()
+                    vnodlocal.servicehandle = autoattachhandle
+                    vnodlocal.autoattached = True
+                    vnodlocal.administrativeState = 'configurationrequested'
+                    logger.debug("Adding Auto-attached VnodLocalService servicehandle: %s" % vnodlocal.servicehandle)
+                    objs.append(vnodlocal)
+
+            # Second Part of Auto-attachment
+            # Look for auto-attachmed Services that are Configured, move them automaticaly to activationrequested
+            autoattachconfigures = self.get_autoattachconfigured()
+            for autoattachconfigure in autoattachconfigures:
+                # Just bounce these forward to activationrequested to get them activated
+                autoattachconfigure.administrativeState = 'activationrequested'
+                objs.append(autoattachconfigure)
+
+
+            # Check for admin status 'ConfigurationRequested'
+            configreqs = VnodLocalService.objects.filter(administrativeState='configurationrequested')
+            for configreq in configreqs:
+                # Call the XOS Interface to configure the service
+                logger.debug("Attempting to configure VnodLocalService servicehandle: %s" % configreq.servicehandle)
+                # Add code to call REST api on the ECORD - For this state - we call VnodGlobal
+                # with the servciehandle and sitename it
+                # it gives us back the NNI port and Vlan Config
+                # we then set our state to 'Configured' or 'ConfigurationFailed'
+                servicehandle = configreq.servicehandle
+                query = {"sitename": sitename, "servicehandle" : servicehandle}
+
+                resp = requests.get("{}/vnodglobal_api_configuration/".format(rest_url), params=query,
+                                    auth=HTTPBasicAuth(username, password))
+
+                if resp.status_code == 200:
+                    resp = resp.json()
+                    # Success-path transitions to 'configured'
+                    configreq.vlanid = resp['vlanid']
+                    configreq.portid = resp['port']['name']
+                    configreq.administrativeState = 'configured'
+
+                    #update proxy adminstate in ecord
+                    data = {"sitename": sitename, "servicehandle": servicehandle, "adminstate": 'configured',
+                            "vlanid": configreq.vlanid, "portid": configreq.portid}
+                    resp = requests.post("{}/vnodglobal_api_status/".format(rest_url), data=json.dumps(data),
+                                     auth=HTTPBasicAuth(username, password))
+
+                else:
+                    configreq.administrativeState = 'configurationfailed'
+
+                objs.append(configreq)
+
+
+            # Check for admin status 'ActivationRequested'
+            activationreqs = VnodLocalService.objects.filter(administrativeState='activationrequested')
+            for acivationreq in activationreqs:
+                # Call the XOS Interface to activate the service
+                logger.debug("Attempting to activate VnodLocalService servicehandle: %s" % acivationreq.servicehandle)
+                # Add code to call REST api on the ECORD - For this state we send the VnodGlobal
+                # service our service handle, subscriber,
+                # VnodLocalId (this id)
+                # Once this is accepted we transition to the
+                # Final state of 'Enabled' or 'ActivationFailed'
+                servicehandle = acivationreq.servicehandle
+                vnodlocalid = acivationreq.id
+                vlanid = acivationreq.vlanid
+                portid = acivationreq.portid
+
+                data = {"sitename": sitename, "servicehandle": servicehandle, "vnodlocalid": vnodlocalid,
+                        "vlanid": vlanid, "portid": portid, "activate": "true"}
+
+                resp = requests.post("{}/vnodglobal_api_activation/".format(rest_url), data=json.dumps(data),
+                                    auth=HTTPBasicAuth(username, password))
+
+                if resp.status_code == 200:
+                    # Success-path transitions to 'enabled'
+                    acivationreq.administrativeState = 'enabled'
+
+                    # update proxy adminstate in ecord
+                    data = {"sitename": sitename, "servicehandle": servicehandle, "adminstate": 'enabled',
+                            "vlanid": vlanid, "portid": portid, "operstate": "active"}
+                    resp = requests.post("{}/vnodglobal_api_status/".format(rest_url), data=json.dumps(data),
+                                         auth=HTTPBasicAuth(username, password))
+                else:
+                    acivationreq.administrativeState = 'activationfailed'
+
+                    # update proxy adminstate in ecord
+                    data = {"sitename": sitename, "servicehandle": servicehandle, "adminstate": 'impaired',
+                            "operstate": "inactive", "vlanid": vlanid, "portid": portid}
+                    resp = requests.post("{}/vnodglobal_api_status/".format(rest_url), data=json.dumps(data),
+                                         auth=HTTPBasicAuth(username, password))
+
+                objs.append(acivationreq)
+
+
+            # Check for admin status 'DeactivationRequested'
+            deactivationreqs = VnodLocalService.objects.filter(administrativeState='deactivationrequested')
+            for deacivationreq in deactivationreqs:
+                # Call the XOS Interface to de-actiavte the spoke
+                logger.debug("Attempting to de-activate VnodLocalService servicehandle: %s" % deacivationreq.servicehandle)
+                # Add code to call REST api on the ECORD - Report change to VnodGlobal
+                servicehandle = deacivationreq.servicehandle
+                vnodlocalid = deacivationreq.id
+                vlanid = deacivationreq.vlanid
+                portid = deacivationreq.portid
+
+
+                data = {"sitename": sitename, "servicehandle": servicehandle, "vnodlocalid": vnodlocalid,
+                        "vlanid": vlanid, "portid": portid, "activate": "false"}
+
+                resp = requests.post("{}/vnodglobal_api_activation/".format(rest_url), data=json.dumps(data),
+                                     auth=HTTPBasicAuth(username, password))
+
+                if resp.status_code == 200:
+                    # Success-path transitions to 'enabled'
+                    deacivationreq.administrativeState = 'configured'
+                else:
+                    deacivationreq.administrativeState = 'deactivationfailed'
+
+                # update proxy adminstate in ecord
+                data = {"sitename": sitename, "servicehandle": servicehandle, "adminstate": 'impaired',
+                        "vlanid": vlanid, "portid": portid}
+                resp = requests.post("{}/vnodglobal_api_status/".format(rest_url), data=json.dumps(data),
+                                     auth=HTTPBasicAuth(username, password))
+
+                objs.append(deacivationreq)
+
+
+            # Check for oper status inactive reported
+            inactivereports = VnodLocalService.objects.filter(operstate='inactivereported')
+            for inactivereport in inactivereports:
+                # Call the XOS Interface to report operstate issue
+                logger.debug("Attempting to report inactive VnodLocalService servicehandle: %s" % inactivereport.servicehandle)
+                # Add code to call REST api on the ECORD - Report change to VnodGlobal
+
+                servicehandle = inactivereport.servicehandle
+                vlanid = inactivereport.vlanid
+                portid = inactivereport.portid
+
+                # update proxy operstate in ecord
+                data = {"sitename": sitename, "servicehandle": servicehandle, "operstate": "inactive",
+                        "adminstate":"impaired", "vlanid": vlanid, "portid": portid}
+                resp = requests.post("{}/vnodglobal_api_status/".format(rest_url), data=json.dumps(data),
+                                     auth=HTTPBasicAuth(username, password))
+
+                # transition to 'inactive' state regardless of whether call to ECORD was successful?!?
+                inactivereport.operstate = 'inactive'
+                objs.append(inactivereport)
+
+
+            # Check for oper status active reported
+            activereports = VnodLocalService.objects.filter(operstate='activereported')
+            for activereport in activereports:
+                # Call the XOS Interface to report operstate issue
+                logger.debug(
+                    "Attempting to report active VnodLocalService servicehandle: %s" % activereport.servicehandle)
+
+                servicehandle = activereport.servicehandle
+                vlanid = activereport.vlanid
+                portid = activereport.portid
+                # Add code to call REST api on the ECORD - Report change to VnodGlobal.
+                # update proxy operstate in ecord
+                data = {"sitename": sitename, "servicehandle": servicehandle, "operstate": "active",
+                        "vlanid": vlanid, "portid": portid}
+                resp = requests.post("{}/vnodglobal_api_status/".format(rest_url), data=json.dumps(data),
+                                 auth=HTTPBasicAuth(username, password))
+
+                activereport.operstate = 'active'
+                objs.append(activereport)
+        elif deletion:
+            # Apply Deletion Semantics:
+            logger.debug("Applying Deletion Semanctics")
+            # TODO: Figure out the odd scenario of Service deletion
+            deletedobjs = VnodLocalService.deleted_objects.all()
+            objs.extend(deletedobjs)
+
+        # Finally just return the set of changed objects
+        return objs
+
+    def get_vnodlocal_system(self):
+        # We only expect to have one of these objects in the system in the curent design
+        # So get the first element from the query
+        vnodlocalsystems = VnodLocalSystem.objects.all()
+        if not vnodlocalsystems:
+            return None
+
+        return vnodlocalsystems[0]
+
+    def get_autoattachhandles(self, vnodlocalsystem):
+        # Figure out API call to actually get this to work
+        rest_url = vnodlocalsystem.restUrl
+        sitename = vnodlocalsystem.name
+        username=vnodlocalsystem.username
+        password=vnodlocalsystem.password
+        query = {"sitename":sitename}
+
+
+        resp = requests.get("{}/vnodglobal_api_autoattach/".format(rest_url), params=query,
+                            auth=HTTPBasicAuth(username, password))
+
+        handles = []
+        if resp.status_code == 200:
+            resp = resp.json()
+            handles = resp['servicehandles']
+        else:
+            logger.debug("Request for autoattach servicehandles failed.")
+
+        return handles
+
+    def get_autoattachconfigured(self):
+        # Query for the set of auto-attached handles that are  in the 'Configured' state
+        autoattachedconfigured = VnodLocalService.objects.filter(autoattached=True, administrativeState='configured')
+
+        if not autoattachedconfigured:
+            return []
+
+        return autoattachedconfigured
+
+
+    def sync_record(self, o):
+
+        # Simply save the record to the DB - both updates and adds are handled the same way
+        o.save()
+
+
+    def delete_record(self, o):
+        # Overriden to customize our behaviour - the core sync step for will remove the record directly
+        # We just log and return
+        logger.debug("deleting Object %s" % str(o), extra=o.tologdict())
+
diff --git a/xos/synchronizer/vnodlocal-synchronizer-devel.py b/xos/synchronizer/vnodlocal-synchronizer-devel.py
new file mode 100755
index 0000000..df697ec
--- /dev/null
+++ b/xos/synchronizer/vnodlocal-synchronizer-devel.py
@@ -0,0 +1,13 @@
+#!/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__)), "../../synchronizers/base")
+sys.path.append("/opt/xos/synchronizers/base")
+print sys.path
+mod = importlib.import_module("xos-synchronizer")
+mod.main()
diff --git a/xos/synchronizer/vnodlocal-synchronizer.py b/xos/synchronizer/vnodlocal-synchronizer.py
new file mode 100755
index 0000000..64d0b08
--- /dev/null
+++ b/xos/synchronizer/vnodlocal-synchronizer.py
@@ -0,0 +1,12 @@
+#!/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__)), "../../synchronizers/base")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-synchronizer")
+mod.main()
diff --git a/xos/synchronizer/vnodlocal_synchronizer_config b/xos/synchronizer/vnodlocal_synchronizer_config
new file mode 100644
index 0000000..1b2bd33
--- /dev/null
+++ b/xos/synchronizer/vnodlocal_synchronizer_config
@@ -0,0 +1,37 @@
+[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=vnodlocal
+dependency_graph=/opt/xos/synchronizers/vnodlocal/model-deps
+steps_dir=/opt/xos/synchronizers/vnodlocal/steps
+sys_dir=/opt/xos/synchronizers/vnodlocal/sys
+deleters_dir=/opt/xos/synchronizers/vnodlocal/deleters
+log_file=console
+driver=None
+pretend=False
+backoff_disabled=True
+fofum_disabled=True
+
+[feefie]
+client_id='vicci_dev_central'
+user_id='pl'
diff --git a/xos/vnodlocalservice-onboard.yaml b/xos/vnodlocalservice-onboard.yaml
new file mode 100644
index 0000000..937cfab
--- /dev/null
+++ b/xos/vnodlocalservice-onboard.yaml
@@ -0,0 +1,20 @@
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+description: Onboard the vnodlocal service
+
+imports:
+   - custom_types/xos.yaml
+
+topology_template:
+  node_templates:
+    vnodlocal:
+      type: tosca.nodes.ServiceController
+      properties:
+          base_url: file:///opt/xos_services/metronet-local/xos/
+          # The following will concatenate with base_url automatically, if
+          # base_url is non-null.
+          models: models.py
+          admin: admin.py
+          rest_service: subdirectory:vnodlocalservice api/service/vnodlocalservice/vnodlocalservice.py
+          synchronizer: synchronizer/manifest
+          synchronizer_run: vnodlocal-synchronizer.py