Merge branch 'master' of github.com:jermowery/xos into AddVPNService
diff --git a/containers/synchronizer/Makefile b/containers/synchronizer/Makefile
index 352616a..733e7b8 100644
--- a/containers/synchronizer/Makefile
+++ b/containers/synchronizer/Makefile
@@ -1,6 +1,6 @@
 IMAGE_NAME:=xosproject/xos-synchronizer-openstack
 CONTAINER_NAME:=xos-synchronizer
-NO_DOCKER_CACHE?=false
+NO_DOCKER_CACHE?=true
 
 .PHONY: build
 build: ; sudo docker build --no-cache=${NO_DOCKER_CACHE} --rm -t ${IMAGE_NAME} .
diff --git a/containers/xos/Dockerfile.devel b/containers/xos/Dockerfile.devel
index a8a9710..2bc6094 100644
--- a/containers/xos/Dockerfile.devel
+++ b/containers/xos/Dockerfile.devel
@@ -34,7 +34,8 @@
     python-novaclient \
     python-neutronclient \
     python-glanceclient \
-    python-ceilometerclient
+    python-ceilometerclient \
+    openvpn
 
 RUN pip install \
     django==1.7 \
@@ -91,3 +92,16 @@
 
 # Define default command.
 CMD update-ca-certificates && python /opt/xos/manage.py runserver 0.0.0.0:8000 --insecure --makemigrations
+
+# for OpenVPN
+RUN mkdir -p /opt/openvpn
+RUN chmod 777 /opt/openvpn
+RUN git clone https://github.com/OpenVPN/easy-rsa.git /opt/openvpn
+RUN git -C /opt/openvpn pull origin master
+RUN echo "set_var EASYRSA	/opt/openvpn/easyrsa3" | tee /opt/openvpn/easyrsa3/vars
+RUN /opt/openvpn/easyrsa3/easyrsa --batch init-pki
+RUN /opt/openvpn/easyrsa3/easyrsa --batch --req-cn=XOS build-ca nopass
+RUN /opt/openvpn/easyrsa3/easyrsa --batch build-server-full server nopass
+RUN /opt/openvpn/easyrsa3/easyrsa --batch gen-dh
+RUN chmod 777 /opt/openvpn/easyrsa3/pki/private/server.key
+RUN chmod 777 /opt/openvpn/easyrsa3/pki/dh.pem
diff --git a/containers/xos/Makefile b/containers/xos/Makefile
index 0ba043d..d5cc57b 100644
--- a/containers/xos/Makefile
+++ b/containers/xos/Makefile
@@ -3,7 +3,7 @@
 TOSCA_CONFIG_PATH:=/opt/xos/configurations/opencloud/opencloud.yaml
 XOS_GIT_REPO?=git://github.com/open-cloud/xos.git
 XOS_GIT_BRANCH?=master
-NO_DOCKER_CACHE?=false
+NO_DOCKER_CACHE?=true
 
 .PHONY: build
 build: ; sudo docker build --no-cache=${NO_DOCKER_CACHE} --rm -t ${IMAGE_NAME} .
diff --git a/xos/configurations/common/Dockerfile.common b/xos/configurations/common/Dockerfile.common
index fd27593..a6a72c5 100644
--- a/xos/configurations/common/Dockerfile.common
+++ b/xos/configurations/common/Dockerfile.common
@@ -28,7 +28,8 @@
     python-dev \
     libyaml-dev \
     pkg-config \
-    python-pycurl
+    python-pycurl \
+    openvpn
 
 RUN pip install django==1.7
 RUN pip install djangorestframework==2.4.4
@@ -130,6 +131,19 @@
 RUN pip install python-dateutil
 RUN bash /opt/xos/tosca/install_tosca.sh
 
+# for OpenVPN
+RUN mkdir -p /opt/openvpn
+RUN git clone https://github.com/OpenVPN/easy-rsa.git /opt/openvpn
+RUN git -C /opt/openvpn pull origin master
+RUN cp /opt/xos/services/vpn/vars /opt/openvpn/vars
+RUN source /opt/openvpn/vars
+RUN /opt/openvpn/clean-all
+RUN /opt/openvpn/build-ca --batch
+RUN /opt/openvpn/build-key-server --batch server
+RUN /opt/openvpn/build-dh
+RUN chmod 777 /opt/openvpn/keys/server.key
+RUN chmod 777 /opt/openvpn/keys/dh2048.pem
+
 EXPOSE 8000
 
 # Set environment variables.
diff --git a/xos/configurations/devel/docker-compose.yml b/xos/configurations/devel/docker-compose.yml
index 803e57c..275329f 100644
--- a/xos/configurations/devel/docker-compose.yml
+++ b/xos/configurations/devel/docker-compose.yml
@@ -16,6 +16,20 @@
     volumes:
         - ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
 
+xos_synchronizer_vpn:
+    image: xosproject/xos-synchronizer-openstack
+    command: bash -c "sleep 120 ; python /opt/xos/synchronizers/vpn/vpn-synchronizer.py -C /opt/xos/synchronizers/vpn/vpn_config"
+    labels:
+        org.xosproject.kind: synchronizer
+        org.xosproject.target: vpn
+    links:
+        - xos_db
+    extra_hosts:
+        - ctl:${MYIP}
+    volumes:
+        - ../setup/id_rsa:/opt/xos/synchronizers/vpn/vpn_private_key:ro  # private key
+        - ../../core/static/vpn:/opt/xos/core/static/vpn:rw
+
 # FUTURE
 #xos_swarm_synchronizer:
 #    image: xosproject/xos-swarm-synchronizer
@@ -33,3 +47,4 @@
     volumes:
       - ../setup:/root/setup:ro
       - ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
+      - ../../core/static/vpn:/opt/xos/core/static/vpn:rw
diff --git a/xos/core/admin.py b/xos/core/admin.py
index 28d99fd..b66f5a6 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -264,6 +264,9 @@
     def backend_status_text(self, obj):
         return mark_safe(backend_text(obj))
 
+    def script_link(self, obj):
+        return mark_safe('<a href="/static/vpn/%s" target="_blank">Script link</a>' % obj.script)
+
     def backend_status_icon(self, obj):
         return mark_safe(backend_icon(obj))
     backend_status_icon.short_description = ""
@@ -488,7 +491,7 @@
 
 class SiteHostsUsersInline(SiteInline):
     def queryset(self, request):
-        return Site.select_by_user(request.user).filter(hosts_users=True)        
+        return Site.select_by_user(request.user).filter(hosts_users=True)
 
 class UserInline(XOSTabularInline):
     model = User
@@ -565,7 +568,7 @@
             kwargs['queryset'] = Service.select_by_user(request.user)
         if db_field.name == 'user':
             kwargs['queryset'] = User.select_by_user(request.user)
-        return super(ServicePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)         
+        return super(ServicePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
 
     def queryset(self, request):
         return ServicePrivilege.select_by_user(request.user)
@@ -817,12 +820,12 @@
     def save_model(self, request, obj, form, change):
         # update openstack connection to use this site/tenant
         obj.save_by_user(request.user)
-                    
+
     def delete_model(self, request, obj):
         obj.delete_by_user(request.user)
 
     def queryset(self, request):
-        return Controller.select_by_user(request.user)    
+        return Controller.select_by_user(request.user)
 
     @property
     def suit_form_tabs(self):
@@ -1015,11 +1018,11 @@
 
     def save_model(self, request, obj, form, change):
         # update openstack connection to use this site/tenant
-        obj.save_by_user(request.user) 
+        obj.save_by_user(request.user)
 
     def delete_model(self, request, obj):
         obj.delete_by_user(request.user)
-        
+
 
 class SitePrivilegeAdmin(XOSBaseAdmin):
     fieldList = ['backend_status_text', 'user', 'site', 'role']
@@ -1117,7 +1120,7 @@
           ('slicenetworks','Networks'),
           ('sliceprivileges','Privileges'),
           ('instances','Instances'),
-          #('reservations','Reservations'), 
+          #('reservations','Reservations'),
           ('tags','Tags'),
           ]
 
@@ -1126,7 +1129,7 @@
             tabs.append( ('admin-only', 'Admin-Only') )
 
         return tabs
-    
+
     def add_view(self, request, form_url='', extra_context=None):
         # Ugly hack for CORD
         self.inlines = self.normal_inlines
@@ -1223,7 +1226,7 @@
     def formfield_for_foreignkey(self, db_field, request, **kwargs):
         if db_field.name == 'slice':
             kwargs['queryset'] = Slice.select_by_user(request.user)
-        
+
         if db_field.name == 'user':
             kwargs['queryset'] = User.select_by_user(request.user)
 
@@ -1603,12 +1606,12 @@
                 login_details_fields.remove('profile')
             #if len(request.user.siteprivileges.filter(role__role = 'pi')) > 0:
                 # only admins and pis can change a user's site
-            #    self.readonly_fields = ('backend_status_text', 'site') 
+            #    self.readonly_fields = ('backend_status_text', 'site')
         self.fieldsets = (
             ('Login Details', {'fields': login_details_fields, 'classes':['suit-tab suit-tab-general']}),
             ('Contact Information', {'fields': self.fieldListContactInfo, 'classes':['suit-tab suit-tab-contact']}),
         )
-        return super(UserAdmin, self).get_form(request, obj, **kwargs)     
+        return super(UserAdmin, self).get_form(request, obj, **kwargs)
 
 class ControllerDashboardViewInline(XOSTabularInline):
     model = ControllerDashboardView
@@ -2081,7 +2084,7 @@
 # unregister the Group model from admin.
 #admin.site.unregister(Group)
 
-# When debugging it is often easier to see all the classes, but for regular use 
+# When debugging it is often easier to see all the classes, but for regular use
 # only the top-levels should be displayed
 showAll = False
 
@@ -2117,4 +2120,3 @@
     admin.site.register(TenantRootRole, TenantRootRoleAdmin)
     admin.site.register(TenantAttribute, TenantAttributeAdmin)
 #    admin.site.register(Container, ContainerAdmin)
-
diff --git a/xos/services/vpn/__init__.py b/xos/services/vpn/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/services/vpn/__init__.py
diff --git a/xos/services/vpn/admin.py b/xos/services/vpn/admin.py
new file mode 100644
index 0000000..608f97e
--- /dev/null
+++ b/xos/services/vpn/admin.py
@@ -0,0 +1,137 @@
+import time
+
+from core.admin import ReadOnlyAwareAdmin, SliceInline
+from core.middleware import get_request
+from core.models import User
+from django import forms
+from django.contrib import admin
+from services.vpn.models import VPN_KIND, VPNService, VPNTenant
+
+
+class VPNServiceAdmin(ReadOnlyAwareAdmin):
+    """Defines the admin for the VPNService."""
+    model = VPNService
+    verbose_name = "VPN Service"
+
+    list_display = ("backend_status_icon", "name", "enabled")
+
+    list_display_links = ('backend_status_icon', 'name', )
+
+    fieldsets = [(None, {'fields': ['backend_status_text', 'name', 'enabled',
+                                    'versionNumber', 'description', "view_url"],
+                         'classes':['suit-tab suit-tab-general']})]
+
+    readonly_fields = ('backend_status_text', )
+
+    inlines = [SliceInline]
+
+    extracontext_registered_admins = True
+
+    user_readonly_fields = ["name", "enabled", "versionNumber", "description"]
+
+    suit_form_tabs = (('general', 'VPN Service Details'),
+                      ('administration', 'Tenants'),
+                      ('slices', 'Slices'),)
+
+    suit_form_includes = (('vpnserviceadmin.html',
+                           'top',
+                           'administration'),)
+
+    def queryset(self, request):
+        return VPNService.get_service_objects_by_user(request.user)
+
+
+class VPNTenantForm(forms.ModelForm):
+    """The form used to create and edit a VPNTenant.
+
+    Attributes:
+        creator (forms.ModelChoiceField): The XOS user that created this tenant.
+        client_conf (forms.CharField): The readonly configuration used on the client to connect to this Tenant.
+        server_address (forms.GenericIPAddressField): The ip address on the VPN of this Tenant.
+        client_address (forms.GenericIPAddressField): The ip address on the VPN of the client.
+        is_persistent (forms.BooleanField): Determines if this Tenant keeps this connection alive through failures.
+        can_view_subnet (forms.BooleanField): Determins if this Tenant makes it's subnet available to the client.
+
+    """
+    creator = forms.ModelChoiceField(queryset=User.objects.all())
+    server_network = forms.GenericIPAddressField(
+        protocol="IPv4", required=True)
+    vpn_subnet = forms.GenericIPAddressField(protocol="IPv4", required=True)
+    is_persistent = forms.BooleanField(required=False)
+    clients_can_see_each_other = forms.BooleanField(required=False)
+
+    def __init__(self, *args, **kwargs):
+        super(VPNTenantForm, self).__init__(*args, **kwargs)
+        self.fields['kind'].widget.attrs['readonly'] = True
+        # self.fields['script_name'].widget.attrs['readonly'] = True
+        self.fields[
+            'provider_service'].queryset = VPNService.get_service_objects().all()
+
+        self.fields['kind'].initial = VPN_KIND
+
+        if self.instance:
+            self.fields['creator'].initial = self.instance.creator
+            self.fields['vpn_subnet'].initial = self.instance.vpn_subnet
+            self.fields[
+                'server_network'].initial = self.instance.server_network
+            self.fields[
+                'clients_can_see_each_other'].initial = self.instance.clients_can_see_each_other
+            self.fields['is_persistent'].initial = self.instance.is_persistent
+
+        if (not self.instance) or (not self.instance.pk):
+            self.fields['creator'].initial = get_request().user
+            self.fields['vpn_subnet'].initial = "255.255.255.0"
+            self.fields['server_network'].initial = "10.66.77.0"
+            self.fields['clients_can_see_each_other'].initial = True
+            self.fields['is_persistent'].initial = True
+            if VPNService.get_service_objects().exists():
+                self.fields["provider_service"].initial = VPNService.get_service_objects().all()[
+                    0]
+
+    def save(self, commit=True):
+        self.instance.creator = self.cleaned_data.get("creator")
+        self.instance.is_persistent = self.cleaned_data.get('is_persistent')
+        self.instance.vpn_subnet = self.cleaned_data.get("vpn_subnet")
+        self.instance.server_network = self.cleaned_data.get('server_network')
+        self.instance.clients_can_see_each_other = self.cleaned_data.get(
+            'clients_can_see_each_other')
+
+        if (not self.instance.script):
+            self.instance.script = str(time.time()) + ".vpn"
+
+        if (not self.instance.ca_crt):
+            self.instance.ca_crt = self.generate_ca_crt()
+
+        return super(VPNTenantForm, self).save(commit=commit)
+
+    def generate_ca_crt(self):
+        """str: Generates the ca cert by reading from the ca file"""
+        with open("/opt/openvpn/easyrsa3/pki/ca.crt") as crt:
+            return crt.readlines()
+
+    class Meta:
+        model = VPNTenant
+
+
+class VPNTenantAdmin(ReadOnlyAwareAdmin):
+    verbose_name = "VPN Tenant Admin"
+    list_display = ('id', 'backend_status_icon', 'instance',
+                    'server_network', 'vpn_subnet')
+    list_display_links = ('id', 'backend_status_icon',
+                          'instance', 'server_network', 'vpn_subnet')
+    fieldsets = [(None, {'fields': ['backend_status_text', 'kind',
+                                    'provider_service', 'instance', 'creator',
+                                    'server_network', 'vpn_subnet', 'is_persistent',
+                                    'clients_can_see_each_other', 'script_link'],
+                         'classes': ['suit-tab suit-tab-general']})]
+    readonly_fields = ('backend_status_text', 'instance', 'script_link')
+    form = VPNTenantForm
+
+    suit_form_tabs = (('general', 'Details'),)
+
+    def queryset(self, request):
+        return VPNTenant.get_tenant_objects_by_user(request.user)
+
+# Associate the admin forms with the models.
+admin.site.register(VPNService, VPNServiceAdmin)
+admin.site.register(VPNTenant, VPNTenantAdmin)
diff --git a/xos/services/vpn/models.py b/xos/services/vpn/models.py
new file mode 100644
index 0000000..65e04e7
--- /dev/null
+++ b/xos/services/vpn/models.py
@@ -0,0 +1,151 @@
+from core.models import Service, TenantWithContainer
+from django.db import transaction
+
+VPN_KIND = "vpn"
+
+
+class VPNService(Service):
+    """Defines the Service for creating VPN servers."""
+    KIND = VPN_KIND
+
+    class Meta:
+        proxy = True
+        # The name used to find this service, all directories are named this
+        app_label = "vpn"
+        verbose_name = "VPN Service"
+
+
+class VPNTenant(TenantWithContainer):
+    """Defines the Tenant for creating VPN servers."""
+
+    class Meta:
+        proxy = True
+        verbose_name = "VPN Tenant"
+
+    KIND = VPN_KIND
+
+    sync_attributes = ("nat_ip", "nat_mac",)
+
+    default_attributes = {'vpn_subnet': None,
+                          'server_network': None,
+                          'clients_can_see_each_other': True,
+                          'is_persistent': True,
+                          'script': None,
+                          'ca_crt': None}
+
+    def __init__(self, *args, **kwargs):
+        vpn_services = VPNService.get_service_objects().all()
+        if vpn_services:
+            self._meta.get_field(
+                "provider_service").default = vpn_services[0].id
+        super(VPNTenant, self).__init__(*args, **kwargs)
+
+    def save(self, *args, **kwargs):
+        super(VPNTenant, self).save(*args, **kwargs)
+        model_policy_vpn_tenant(self.pk)
+
+    def delete(self, *args, **kwargs):
+        self.cleanup_container()
+        super(VPNTenant, self).delete(*args, **kwargs)
+
+    @property
+    def addresses(self):
+        """Mapping[str, str]: The ip, mac address, and subnet of the NAT network of this Tenant."""
+        if (not self.id) or (not self.instance):
+            return {}
+
+        addresses = {}
+        for ns in self.instance.ports.all():
+            if "nat" in ns.network.name.lower():
+                addresses["ip"] = ns.ip
+                addresses["mac"] = ns.mac
+                break
+
+        return addresses
+
+    # This getter is necessary because nat_ip is a sync_attribute
+    @property
+    def nat_ip(self):
+        """str: The IP of this Tenant on the NAT network."""
+        return self.addresses.get("ip", None)
+
+    # This getter is necessary because nat_mac is a sync_attribute
+    @property
+    def nat_mac(self):
+        """str: The MAC address of this Tenant on the NAT network."""
+        return self.addresses.get("mac", None)
+
+    @property
+    def server_network(self):
+        """str: The IP address of the server on the VPN."""
+        return self.get_attribute(
+            'server_network',
+            self.default_attributes['server_network'])
+
+    @server_network.setter
+    def server_network(self, value):
+        self.set_attribute("server_network", value)
+
+    @property
+    def vpn_subnet(self):
+        """str: The IP address of the client on the VPN."""
+        return self.get_attribute(
+            'vpn_subnet',
+            self.default_attributes['vpn_subnet'])
+
+    @vpn_subnet.setter
+    def vpn_subnet(self, value):
+        self.set_attribute("vpn_subnet", value)
+
+    @property
+    def is_persistent(self):
+        """bool: True if the VPN connection is persistence, false otherwise."""
+        return self.get_attribute(
+            "is_persistent",
+            self.default_attributes['is_persistent'])
+
+    @is_persistent.setter
+    def is_persistent(self, value):
+        self.set_attribute("is_persistent", value)
+
+    @property
+    def clients_can_see_each_other(self):
+        """bool: True if the client can see the subnet of the server, false otherwise."""
+        return self.get_attribute(
+            "clients_can_see_each_other",
+            self.default_attributes['clients_can_see_each_other'])
+
+    @clients_can_see_each_other.setter
+    def clients_can_see_each_other(self, value):
+        self.set_attribute("clients_can_see_each_other", value)
+
+    @property
+    def script(self):
+        """string: The file name of the client script"""
+        return self.get_attribute("script", self.default_attributes['script'])
+
+    @script.setter
+    def script(self, value):
+        self.set_attribute("script", value)
+
+    @property
+    def ca_crt(self):
+        """str: the string for the ca certificate"""
+        return self.get_attribute("ca_crt", self.default_attributes['ca_crt'])
+
+    @ca_crt.setter
+    def ca_crt(self, value):
+        self.set_attribute("ca_crt", value)
+
+
+def model_policy_vpn_tenant(pk):
+    """Manages the contain for the VPN Tenant."""
+    # This section of code is atomic to prevent race conditions
+    with transaction.atomic():
+        # We find all of the tenants that are waiting to update
+        tenant = VPNTenant.objects.select_for_update().filter(pk=pk)
+        if not tenant:
+            return
+        # Since this code is atomic it is safe to always use the first tenant
+        tenant = tenant[0]
+        tenant.manage_container()
diff --git a/xos/services/vpn/templates/vpnserviceadmin.html b/xos/services/vpn/templates/vpnserviceadmin.html
new file mode 100644
index 0000000..d983771
--- /dev/null
+++ b/xos/services/vpn/templates/vpnserviceadmin.html
@@ -0,0 +1,10 @@
+<!-- Template used to for the button leading to the HelloWorldTenantComplete form. -->
+<div class = "left-nav">
+  <ul>
+    <li>
+      <a href="/admin/vpn/vpntenant/">
+        VPN Tenants
+      </a>
+    </li>
+  </ul>
+</div>
diff --git a/xos/synchronizers/vpn/__init__.py b/xos/synchronizers/vpn/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/synchronizers/vpn/__init__.py
diff --git a/xos/synchronizers/vpn/model-deps b/xos/synchronizers/vpn/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/synchronizers/vpn/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/synchronizers/vpn/run.sh b/xos/synchronizers/vpn/run.sh
new file mode 100755
index 0000000..9a2e69b
--- /dev/null
+++ b/xos/synchronizers/vpn/run.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+python vpn-synchronizer.py  -C $XOS_DIR/synchronizers/vpn/vpn_config
diff --git a/xos/synchronizers/vpn/steps/__init__.py b/xos/synchronizers/vpn/steps/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/synchronizers/vpn/steps/__init__.py
diff --git a/xos/synchronizers/vpn/steps/sync_vpntenant.py b/xos/synchronizers/vpn/steps/sync_vpntenant.py
new file mode 100644
index 0000000..1d0e1ea
--- /dev/null
+++ b/xos/synchronizers/vpn/steps/sync_vpntenant.py
@@ -0,0 +1,98 @@
+import os
+import sys
+import time
+
+from django.db.models import F, Q
+from services.vpn.models import VPNTenant
+from subprocess import Popen, PIPE
+from synchronizers.base.SyncInstanceUsingAnsible import \
+    SyncInstanceUsingAnsible
+
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+
+class SyncVPNTenant(SyncInstanceUsingAnsible):
+    """Class for syncing a VPNTenant using Ansible."""
+    provides = [VPNTenant]
+    observes = VPNTenant
+    requested_interval = 0
+    template_name = "sync_vpntenant.yaml"
+    service_key_name = "/opt/xos/synchronizers/vpn/vpn_private_key"
+
+    def __init__(self, *args, **kwargs):
+        super(SyncVPNTenant, self).__init__(*args, **kwargs)
+
+    def fetch_pending(self, deleted):
+        if (not deleted):
+            objs = VPNTenant.get_tenant_objects().filter(
+                Q(enacted__lt=F('updated')) | Q(enacted=None), Q(lazy_blocked=False))
+        else:
+            objs = VPNTenant.get_deleted_tenant_objects()
+
+        return objs
+
+    def get_extra_attributes(self, tenant):
+        return {"is_persistent": tenant.is_persistent,
+                "vpn_subnet": tenant.vpn_subnet,
+                "server_network": tenant.server_network,
+                "clients_can_see_each_other": tenant.clients_can_see_each_other,
+                "instnace_id": tenant.instance.instnace_id
+                }
+
+    def run_playbook(self, o, fields):
+        self.create_client_script(o)
+        # Generate the server files
+        (stdout, stderr) = Popen("/opt/openvpn/easyrsa3/easyrsa --batch build-server-full server" + o.instance.instance_id + " nopass",shell=True, stdout=PIPE).communicate()
+        print(str(stdout))
+        print(str(stderr))
+        super(SyncVPNTenant, self).run_playbook(o, fields)
+
+    def create_client_script(self, tenant):
+        script = open("/opt/xos/core/static/vpn/" + str(tenant.script), 'w')
+        # write the configuration portion
+        script.write("printf \"%b\" \"")
+        for line in self.generate_client_conf(tenant).splitlines():
+            script.write(line + r"\n")
+        script.write("\" > client.conf\n")
+        script.write("printf \"%b\" \"")
+        for line in self.generate_login().splitlines():
+            script.write(line + r"\n")
+        script.write("\" > login.up\n")
+        script.write("printf \"%b\" \"")
+        for line in tenant.ca_crt:
+            script.write(line.rstrip() + r"\n")
+        script.write("\" > ca.crt\n")
+        # make sure openvpn is installed
+        script.write("apt-get update\n")
+        script.write("apt-get install openvpn\n")
+        script.write("openvpn client.conf &\n")
+        # close the script
+        script.close()
+
+    def generate_login(self):
+        return str(time.time()) + "\npassword\n"
+
+    def generate_client_conf(self, tenant):
+        """str: Generates the client configuration to use to connect to this VPN server.
+
+        Args:
+            tenant (VPNTenant): The tenant to generate the client configuration for.
+
+        """
+        conf = ("client\n" +
+            "auth-user-pass login.up\n" +
+            "dev tun\n" +
+            "proto udp\n" +
+            "remote " + str(tenant.nat_ip) + " 1194\n" +
+            "resolv-retry infinite\n" +
+            "nobind\n" +
+            "ca ca.crt\n" +
+            "comp-lzo\n" +
+            "verb 3\n")
+
+        if tenant.is_persistent:
+            conf += "persist-tun\n"
+            conf += "persist-key\n"
+
+        return conf
diff --git a/xos/synchronizers/vpn/steps/sync_vpntenant.yaml b/xos/synchronizers/vpn/steps/sync_vpntenant.yaml
new file mode 100644
index 0000000..d7c7b8d
--- /dev/null
+++ b/xos/synchronizers/vpn/steps/sync_vpntenant.yaml
@@ -0,0 +1,82 @@
+---
+- hosts: {{ instance_name }}
+  gather_facts: False
+  connection: ssh
+  user: ubuntu
+  sudo: yes
+  vars:
+    server_network: {{ server_network }}
+    is_persistent: {{ is_persistent }}
+    vpn_subnet: {{ vpn_subnet }}
+    clients_can_see_each_other: {{ clients_can_see_each_other }}
+    instance_id: {{ instance_id }}
+
+  tasks:
+  - name: install openvpn
+    apt: name=openvpn state=present update_cache=yes
+
+  - name: stop openvpn
+    shell: killall openvpn || true
+
+  - name: make sure /opt/openvpn exists
+    file: path=/opt/openvpn state=directory
+
+  - name: get server key
+    copy: src=/opt/openvpn/easyrsa3/pki/private/server{{ instance_id }}.key dest=/opt/openvpn/server.key
+
+  - name: get server crt
+    copy: src=/opt/openvpn/easyrsa3/pki/issued/server{{ instance_id }}.crt dest=/opt/openvpn/server.crt
+
+  - name: get ca crt
+    copy: src=/opt/openvpn/easyrsa3/pki/ca.crt dest=/opt/openvpn/ca.crt
+
+  - name: get dh
+    copy: src=/opt/openvpn/easyrsa3/pki/dh.pem dest=/opt/openvpn/dh.pem
+
+  - name: erase config
+    shell: rm -f server.conf
+
+  - name: erase auth script
+    shell: rm -f auth.sh
+
+  - name: write auth script
+    shell: printf "%b" "#!/bin/bash\nexit 0" > auth.sh
+
+  - name: make auth script executable
+    shell: chmod 777 auth.sh
+
+  - name: write base config
+    shell:
+       |
+       printf "script-security 3 system
+       port 1194
+       proto udp
+       dev tun
+       ca /opt/openvpn/ca.crt
+       cert /opt/openvpn/server.crt
+       key /opt/openvpn/server.key
+       dh /opt/openvpn/dh.pem
+       server {{ server_network }} {{ vpn_subnet }}
+       ifconfig-pool-persist ipp.txt
+       comp-lzo
+       status openvpn-status.log
+       verb 3
+       auth-user-pass-verify auth.sh via-file
+       client-cert-not-required
+       username-as-common-name
+       " > server.conf
+
+  - name: write persistent config
+    shell:
+      |
+      printf "\nkeepalive 10 60
+      persist-tun
+      persist-key" >> server.conf
+    when: {{ is_persistent }}
+
+  - name: write client-to-client config
+    shell: printf "client-to-client" >> server.conf
+    when: {{ clients_can_see_each_other }}
+
+  - name: start openvpn
+    shell: openvpn server.conf &
diff --git a/xos/synchronizers/vpn/stop.sh b/xos/synchronizers/vpn/stop.sh
new file mode 100755
index 0000000..4178688
--- /dev/null
+++ b/xos/synchronizers/vpn/stop.sh
@@ -0,0 +1,2 @@
+# Kill the observer
+pkill -9 -f vpn-synchronizer.py
diff --git a/xos/synchronizers/vpn/vpn-synchronizer.py b/xos/synchronizers/vpn/vpn-synchronizer.py
new file mode 100755
index 0000000..3227ed9
--- /dev/null
+++ b/xos/synchronizers/vpn/vpn-synchronizer.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+
+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/synchronizers/vpn/vpn_config b/xos/synchronizers/vpn/vpn_config
new file mode 100644
index 0000000..2cdb192
--- /dev/null
+++ b/xos/synchronizers/vpn/vpn_config
@@ -0,0 +1,23 @@
+# 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 synchronizer
+[observer]
+name=vpn
+dependency_graph=/opt/xos/synchronizers/vpn/model-deps
+steps_dir=/opt/xos/synchronizers/vpn/steps
+sys_dir=/opt/xos/synchronizers/vpn/sys
+logfile=/var/log/xos_backend.log
+pretend=False
+backoff_disabled=True
+save_ansible_output=True
+proxy_ssh=False
diff --git a/xos/tools/xos-manage b/xos/tools/xos-manage
index 4783bf5..37827b5 100755
--- a/xos/tools/xos-manage
+++ b/xos/tools/xos-manage
@@ -60,13 +60,13 @@
         echo Waiting for postgres to start
         sleep 1
         sudo -u postgres psql -c '\q'
-    done 
+    done
 }
 
 function db_exists {
-   sudo -u postgres psql $DBNAME -c '\q' 2>/dev/null    
+   sudo -u postgres psql $DBNAME -c '\q' 2>/dev/null
    return $?
-} 
+}
 
 function createdb {
     wait_postgres
@@ -145,6 +145,7 @@
     python ./manage.py makemigrations cord
     python ./manage.py makemigrations ceilometer
     python ./manage.py makemigrations helloworldservice_complete
+    python ./manage.py makemigrations vpn
     python ./manage.py makemigrations onos
     #python ./manage.py makemigrations servcomp
 }
diff --git a/xos/xos/settings.py b/xos/xos/settings.py
index c8b0b07..1cec43f 100644
--- a/xos/xos/settings.py
+++ b/xos/xos/settings.py
@@ -30,7 +30,7 @@
 GEOIP_PATH = "/usr/share/GeoIP"
 XOS_DIR = "/opt/xos"
 
-DEBUG = False
+DEBUG = True
 TEMPLATE_DEBUG = DEBUG
 
 ADMINS = (
@@ -180,6 +180,7 @@
     'services.ceilometer',
     'services.requestrouter',
     'services.syndicate_storage',
+    'services.vpn',
     'geoposition',
     'rest_framework_swagger',
 )