Merge branch 'master' into feature/volt-autoconfig
diff --git a/containers/openvpn/Dockerfile b/containers/openvpn/Dockerfile
new file mode 100644
index 0000000..8ae8484
--- /dev/null
+++ b/containers/openvpn/Dockerfile
@@ -0,0 +1,12 @@
+FROM       xosproject/xos-synchronizer-openstack
+
+RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y \
+    openvpn
+
+# 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/vars
+RUN echo 'set_var EASYRSA_BATCH	"true"' | tee -a /opt/openvpn/vars
diff --git a/containers/openvpn/Makefile b/containers/openvpn/Makefile
new file mode 100644
index 0000000..bdfb126
--- /dev/null
+++ b/containers/openvpn/Makefile
@@ -0,0 +1,18 @@
+IMAGE_NAME:=xosproject/xos-openvpn
+CONTAINER_NAME:=xos-synchronizer
+NO_DOCKER_CACHE?=true
+
+.PHONY: build
+build: ; sudo docker build --no-cache=${NO_DOCKER_CACHE} --rm -t ${IMAGE_NAME} .
+
+.PHONY: run
+run: ; sudo docker run -d --name ${CONTAINER_NAME} -v /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro ${IMAGE_NAME}
+
+.PHONY: stop
+stop: ; sudo docker stop ${CONTAINER_NAME}
+
+.PHONY: rm
+rm: ; sudo docker rm ${CONTAINER_NAME}
+
+.PHONY: rmi
+rmi: ; docker rmi ${IMAGE_NAME}
diff --git a/containers/openvpn/conf/ansible-hosts b/containers/openvpn/conf/ansible-hosts
new file mode 100644
index 0000000..0dd74f1
--- /dev/null
+++ b/containers/openvpn/conf/ansible-hosts
@@ -0,0 +1,2 @@
+[localhost]
+127.0.0.1
diff --git a/containers/openvpn/conf/synchronizer.conf b/containers/openvpn/conf/synchronizer.conf
new file mode 100644
index 0000000..2131a25
--- /dev/null
+++ b/containers/openvpn/conf/synchronizer.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/synchronizers/openstack/xos-synchronizer.py
+stderr_logfile=/var/log/supervisor/synchronizer.err.log
+stdout_logfile=/var/log/supervisor/synchronizer.out.log
diff --git a/containers/xos/Dockerfile b/containers/xos/Dockerfile
index b5064ae..afc7c9d 100644
--- a/containers/xos/Dockerfile
+++ b/containers/xos/Dockerfile
@@ -50,6 +50,7 @@
     django-timezones \
     djangorestframework==3.3.3 \
     dnslib \
+    jinja2 \
     lxml \
     markdown \
     netaddr \
diff --git a/containers/xos/Dockerfile.devel b/containers/xos/Dockerfile.devel
index 7bed082..5dc62a6 100644
--- a/containers/xos/Dockerfile.devel
+++ b/containers/xos/Dockerfile.devel
@@ -50,6 +50,7 @@
     django-timezones \
     djangorestframework==3.3.3 \
     dnslib \
+    jinja2 \
     lxml \
     markdown \
     netaddr \
diff --git a/views/ngXosViews/openVPNDashboard/.bowerrc b/views/ngXosViews/openVPNDashboard/.bowerrc
new file mode 100644
index 0000000..e491038
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/.bowerrc
@@ -0,0 +1,3 @@
+{
+  "directory": "src/vendor/"
+}
\ No newline at end of file
diff --git a/views/ngXosViews/openVPNDashboard/.eslintrc b/views/ngXosViews/openVPNDashboard/.eslintrc
new file mode 100644
index 0000000..c852748
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/.eslintrc
@@ -0,0 +1,42 @@
+{
+    "ecmaFeatures": {
+        "blockBindings": true,
+        "forOf": true,
+        "destructuring": true,
+        "arrowFunctions": true,
+        "templateStrings": true
+    },
+    "env": { 
+        "browser": true,
+        "node": true,
+        "es6": true
+    },
+    "plugins": [
+        //"angular"
+    ],
+    "rules": {
+        "quotes": [2, "single"],
+        "camelcase": [1, {"properties": "always"}],
+        "no-underscore-dangle": 1,
+        "eqeqeq": [2, "smart"],
+        "no-alert": 1,
+        "key-spacing": [1, { "beforeColon": false, "afterColon": true }],
+        "indent": [2, 2],
+        "no-irregular-whitespace": 1,
+        "eol-last": 0,
+        "max-nested-callbacks": [2, 4],
+        "comma-spacing": [1, {"before": false, "after": true}],
+        "no-trailing-spaces": [1, { skipBlankLines: true }],
+        "no-unused-vars": [1, {"vars": "all", "args": "after-used"}],
+        "new-cap": 0,
+
+        //"angular/ng_module_name": [2, '/^xos\.*[a-z]*$/'],
+        //"angular/ng_controller_name": [2, '/^[a-z].*Ctrl$/'],
+        //"angular/ng_service_name": [2, '/^[A-Z].*Service$/'],
+        //"angular/ng_directive_name": [2, '/^[a-z]+[[A-Z].*]*$/'],
+        //"angular/ng_di": [0, "function or array"]
+    },
+    "globals" :{
+        "angular": true
+    } 
+}
\ No newline at end of file
diff --git a/views/ngXosViews/openVPNDashboard/.gitignore b/views/ngXosViews/openVPNDashboard/.gitignore
new file mode 100644
index 0000000..567aee4
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/.gitignore
@@ -0,0 +1,6 @@
+dist/
+src/vendor
+.tmp
+node_modules
+npm-debug.log
+dist/
\ No newline at end of file
diff --git a/views/ngXosViews/openVPNDashboard/bower.json b/views/ngXosViews/openVPNDashboard/bower.json
new file mode 100644
index 0000000..01b2715
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/bower.json
@@ -0,0 +1,29 @@
+{
+  "name": "xos-openVPNDashboard",
+  "version": "0.0.0",
+  "authors": [
+    "Jeremy Mowery <jermowery@email.arizona.edu>"
+  ],
+  "description": "The OpenVPN Dashboard",
+  "license": "MIT",
+  "ignore": [
+    "**/.*",
+    "node_modules",
+    "bower_components",
+    "static/js/vendor/",
+    "test",
+    "tests"
+  ],
+  "dependencies": {
+  },
+  "devDependencies": {
+    "jquery": "2.1.4",
+    "angular-mocks": "1.4.7",
+    "angular": "1.4.7",
+    "angular-ui-router": "0.2.15",
+    "angular-cookies": "1.4.7",
+    "angular-resource": "1.4.7",
+    "ng-lodash": "0.3.0",
+    "bootstrap-css": "2.3.2"
+  }
+}
diff --git a/views/ngXosViews/openVPNDashboard/env/default.js b/views/ngXosViews/openVPNDashboard/env/default.js
new file mode 100644
index 0000000..5b198ec
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/env/default.js
@@ -0,0 +1,13 @@
+// This is a default configuration for your development environment.
+// You can duplicate this configuration for any of your Backend Environments.
+// Different configurations are loaded setting a NODE_ENV variable that contain the config file name.
+// `NODE_ENV=local npm start`
+//
+// If xoscsrftoken or xossessionid are not specified the browser value are used
+// (works only for local environment as both application are served on the same domain)
+
+module.exports = {
+  host: '',
+  xoscsrftoken: '',
+  xossessionid: ''
+};
diff --git a/views/ngXosViews/openVPNDashboard/gulp/build.js b/views/ngXosViews/openVPNDashboard/gulp/build.js
new file mode 100644
index 0000000..625e3ee
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/gulp/build.js
@@ -0,0 +1,150 @@
+'use strict';
+
+// BUILD
+//
+// The only purpose of this gulpfile is to build a XOS view and copy the correct files into
+// .html => dashboards
+// .js (minified and concat) => static/js
+//
+// The template are parsed and added to js with angular $templateCache
+
+var gulp = require('gulp');
+var ngAnnotate = require('gulp-ng-annotate');
+var uglify = require('gulp-uglify');
+var templateCache = require('gulp-angular-templatecache');
+var runSequence = require('run-sequence');
+var concat = require('gulp-concat');
+var del = require('del');
+var wiredep = require('wiredep');
+var angularFilesort = require('gulp-angular-filesort');
+var _ = require('lodash');
+var eslint = require('gulp-eslint');
+var inject = require('gulp-inject');
+var rename = require('gulp-rename');
+var replace = require('gulp-replace');
+var postcss = require('gulp-postcss');
+var autoprefixer = require('autoprefixer');
+var mqpacker = require('css-mqpacker');
+var csswring = require('csswring');
+
+var TEMPLATE_FOOTER = `}]);
+angular.module('xos.openVPNDashboard').run(function($location){$location.path('/')});
+angular.bootstrap(angular.element('#xosOpenVPNDashboard'), ['xos.openVPNDashboard']);`;
+
+module.exports = function(options){
+
+  // delete previous builded file
+  gulp.task('clean', function(){
+    return del(
+      [options.dashboards + 'xosOpenVPNDashboard.html'],
+      {force: true}
+    );
+  });
+
+  // minify css
+  gulp.task('css', function () {
+    var processors = [
+      autoprefixer({browsers: ['last 1 version']}),
+      mqpacker,
+      csswring
+    ];
+
+    gulp.src([
+      `${options.css}**/*.css`,
+      `!${options.css}dev.css`
+    ])
+    .pipe(postcss(processors))
+    .pipe(gulp.dest(options.tmp + '/css/'));
+  });
+
+  gulp.task('copyCss', ['css'], function(){
+    return gulp.src([`${options.tmp}/css/*.css`])
+    .pipe(concat('xosOpenVPNDashboard.css'))
+    .pipe(gulp.dest(options.static + 'css/'))
+  });
+
+  // compile and minify scripts
+  gulp.task('scripts', function() {
+    return gulp.src([
+      options.tmp + '**/*.js'
+    ])
+    .pipe(ngAnnotate())
+    .pipe(angularFilesort())
+    .pipe(concat('xosOpenVPNDashboard.js'))
+    .pipe(uglify())
+    .pipe(gulp.dest(options.static + 'js/'));
+  });
+
+  // set templates in cache
+  gulp.task('templates', function(){
+    return gulp.src('./src/templates/*.html')
+      .pipe(templateCache({
+        module: 'xos.openVPNDashboard',
+        root: 'templates/',
+        templateFooter: TEMPLATE_FOOTER
+      }))
+      .pipe(gulp.dest(options.tmp));
+  });
+
+  // copy html index to Django Folder
+  gulp.task('copyHtml', ['clean'], function(){
+    return gulp.src(options.src + 'index.html')
+      // remove dev dependencies from html
+      .pipe(replace(/<!-- bower:css -->(\n.*)*\n<!-- endbower --><!-- endcss -->/, ''))
+      .pipe(replace(/<!-- bower:js -->(\n.*)*\n<!-- endbower --><!-- endjs -->/, ''))
+      .pipe(replace(/ng-app=".*"\s/, ''))
+      // rewriting css path
+      // .pipe(replace(/(<link.*">)/, ''))
+      // injecting minified files
+      .pipe(
+        inject(
+          gulp.src([
+            options.static + 'js/vendor/xosOpenVPNDashboardVendor.js',
+            options.static + 'js/xosOpenVPNDashboard.js',
+            options.static + 'css/xosOpenVPNDashboard.css'
+          ]),
+          {ignorePath: '/../../../xos/core/xoslib'}
+        )
+      )
+      .pipe(rename('xosOpenVPNDashboard.html'))
+      .pipe(gulp.dest(options.dashboards));
+  });
+
+  // minify vendor js files
+  gulp.task('wiredep', function(){
+    var bowerDeps = wiredep().js;
+    if(!bowerDeps){
+      return;
+    }
+
+    // remove angular (it's already loaded)
+    _.remove(bowerDeps, function(dep){
+      return dep.indexOf('angular/angular.js') !== -1;
+    });
+
+    return gulp.src(bowerDeps)
+      .pipe(concat('xosOpenVPNDashboardVendor.js'))
+      .pipe(uglify())
+      .pipe(gulp.dest(options.static + 'js/vendor/'));
+  });
+
+  gulp.task('lint', function () {
+    return gulp.src(['src/js/**/*.js'])
+      .pipe(eslint())
+      .pipe(eslint.format())
+      .pipe(eslint.failAfterError());
+  });
+
+  gulp.task('build', function() {
+    runSequence(
+      'lint',
+      'templates',
+      'babel',
+      'scripts',
+      'wiredep',
+      'copyHtml',
+      'copyCss',
+      'cleanTmp'
+    );
+  });
+};
diff --git a/views/ngXosViews/openVPNDashboard/gulp/server.js b/views/ngXosViews/openVPNDashboard/gulp/server.js
new file mode 100644
index 0000000..7605294
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/gulp/server.js
@@ -0,0 +1,146 @@
+'use strict';
+
+var gulp = require('gulp');
+var browserSync = require('browser-sync').create();
+var inject = require('gulp-inject');
+var runSequence = require('run-sequence');
+var angularFilesort = require('gulp-angular-filesort');
+var babel = require('gulp-babel');
+var wiredep = require('wiredep').stream;
+var httpProxy = require('http-proxy');
+var del = require('del');
+
+const environment = process.env.NODE_ENV;
+
+if (environment){
+  var conf = require(`../env/${environment}.js`);
+}
+else{
+  var conf = require('../env/default.js')
+}
+
+var proxy = httpProxy.createProxyServer({
+  target: conf.host || 'http://0.0.0.0:9999'
+});
+
+
+proxy.on('error', function(error, req, res) {
+  res.writeHead(500, {
+    'Content-Type': 'text/plain'
+  });
+
+  console.error('[Proxy]', error);
+});
+
+module.exports = function(options){
+
+  // open in browser with sync and proxy to 0.0.0.0
+  gulp.task('browser', function() {
+    browserSync.init({
+      // reloadDelay: 500,
+      // logLevel: 'debug',
+      // logConnections: true,
+      startPath: '#/',
+      snippetOptions: {
+        rule: {
+          match: /<!-- browserSync -->/i
+        }
+      },
+      server: {
+        baseDir: options.src,
+        routes: {
+          '/api': options.api,
+          '/xosHelpers/src': options.helpers
+        },
+        middleware: function(req, res, next){
+          if(
+            req.url.indexOf('/xos/') !== -1 ||
+            req.url.indexOf('/xoslib/') !== -1 ||
+            req.url.indexOf('/hpcapi/') !== -1
+          ){
+            if(conf.xoscsrftoken && conf.xossessionid){
+              req.headers.cookie = `xoscsrftoken=${conf.xoscsrftoken}; xossessionid=${conf.xossessionid}`;
+              req.headers['x-csrftoken'] = conf.xoscsrftoken;
+            }
+            proxy.web(req, res);
+          }
+          else{
+            next();
+          }
+        }
+      }
+    });
+
+    gulp.watch(options.src + 'js/**/*.js', ['js-watch']);
+    gulp.watch(options.src + 'vendor/**/*.js', ['bower'], function(){
+      browserSync.reload();
+    });
+    gulp.watch(options.src + '**/*.html', function(){
+      browserSync.reload();
+    });
+  });
+
+  // transpile js with sourceMaps
+  gulp.task('babel', function(){
+    return gulp.src(options.scripts + '**/*.js')
+      .pipe(babel({sourceMaps: true}))
+      .pipe(gulp.dest(options.tmp));
+  });
+
+  // inject scripts
+  gulp.task('injectScript', ['cleanTmp', 'babel'], function(){
+    return gulp.src(options.src + 'index.html')
+      .pipe(
+        inject(
+          gulp.src([
+            options.tmp + '**/*.js',
+            options.api + '*.js',
+            options.helpers + '**/*.js'
+          ])
+          .pipe(angularFilesort()),
+          {
+            ignorePath: [options.src, '/../../ngXosLib']
+          }
+        )
+      )
+      .pipe(gulp.dest(options.src));
+  });
+
+  // inject CSS
+  gulp.task('injectCss', function(){
+    return gulp.src(options.src + 'index.html')
+      .pipe(
+        inject(
+          gulp.src(options.src + 'css/*.css'),
+          {
+            ignorePath: [options.src]
+          }
+          )
+        )
+      .pipe(gulp.dest(options.src));
+  });
+
+  // inject bower dependencies with wiredep
+  gulp.task('bower', function () {
+    return gulp.src(options.src + 'index.html')
+    .pipe(wiredep({devDependencies: true}))
+    .pipe(gulp.dest(options.src));
+  });
+
+  gulp.task('js-watch', ['injectScript'], function(){
+    browserSync.reload();
+  });
+
+  gulp.task('cleanTmp', function(){
+    return del([options.tmp + '**/*']);
+  });
+
+  gulp.task('serve', function() {
+    runSequence(
+      'bower',
+      'injectScript',
+      'injectCss',
+      ['browser']
+    );
+  });
+};
diff --git a/views/ngXosViews/openVPNDashboard/gulpfile.js b/views/ngXosViews/openVPNDashboard/gulpfile.js
new file mode 100644
index 0000000..a3523ee
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/gulpfile.js
@@ -0,0 +1,25 @@
+'use strict';
+
+var gulp = require('gulp');
+var wrench = require('wrench');
+
+var options = {
+  src: 'src/',
+  css: 'src/css/',
+  scripts: 'src/js/',
+  tmp: 'src/.tmp',
+  dist: 'dist/',
+  api: '../../ngXosLib/api/',
+  helpers: '../../ngXosLib/xosHelpers/src/',
+  static: '../../../xos/core/xoslib/static/', // this is the django static folder
+  dashboards: '../../../xos/core/xoslib/dashboards/' // this is the django html folder
+};
+
+wrench.readdirSyncRecursive('./gulp')
+.map(function(file) {
+  require('./gulp/' + file)(options);
+});
+
+gulp.task('default', function () {
+  gulp.start('build');
+});
diff --git a/views/ngXosViews/openVPNDashboard/karma.conf.js b/views/ngXosViews/openVPNDashboard/karma.conf.js
new file mode 100644
index 0000000..dbd344a
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/karma.conf.js
@@ -0,0 +1,89 @@
+// Karma configuration
+// Generated on Tue Oct 06 2015 09:27:10 GMT+0000 (UTC)
+
+/* eslint indent: [2,2], quotes: [2, "single"]*/
+
+/*eslint-disable*/
+var wiredep = require('wiredep');
+var path = require('path');
+
+var bowerComponents = wiredep( {devDependencies: true} )[ 'js' ].map(function( file ){
+  return path.relative(process.cwd(), file);
+});
+
+module.exports = function(config) {
+/*eslint-enable*/
+  config.set({
+
+    // base path that will be used to resolve all patterns (eg. files, exclude)
+    basePath: '',
+
+
+    // frameworks to use
+    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+    frameworks: ['jasmine'],
+
+
+    // list of files / patterns to load in the browser
+    files: bowerComponents.concat([
+      'src/css/**/*.css',
+      '../../static/js/xosApi.js',
+      '../../static/js/vendor/ngXosHelpers.js',
+      'src/js/**/*.js',
+      'spec/**/*.mock.js',
+      'spec/**/*.test.js',
+      'src/**/*.html'
+    ]),
+
+
+    // list of files to exclude
+    exclude: [
+    ],
+
+
+    // preprocess matching files before serving them to the browser
+    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+    preprocessors: {
+      'src/js/**/*.js': ['babel'],
+      'spec/**/*.test.js': ['babel'],
+      'src/**/*.html': ['ng-html2js']
+    },
+
+    ngHtml2JsPreprocessor: {
+      stripPrefix: 'src/', //strip the src path from template url (http://stackoverflow.com/questions/22869668/karma-unexpected-request-when-testing-angular-directive-even-with-ng-html2js)
+      moduleName: 'templates' // define the template module name
+    },
+
+    // test results reporter to use
+    // possible values: 'dots', 'progress'
+    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+    reporters: ['mocha'],
+
+
+    // web server port
+    port: 9876,
+
+
+    // enable / disable colors in the output (reporters and logs)
+    colors: true,
+
+
+    // level of logging
+    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+    logLevel: config.LOG_INFO,
+
+
+    // enable / disable watching file and executing tests whenever any file changes
+    autoWatch: true,
+
+
+    // start these browsers
+    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+    browsers: ['PhantomJS'],
+
+
+    // Continuous Integration mode
+    // if true, Karma captures browsers, runs the tests and exits
+    singleRun: false
+  });
+};
diff --git a/views/ngXosViews/openVPNDashboard/package.json b/views/ngXosViews/openVPNDashboard/package.json
new file mode 100644
index 0000000..412afec
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/package.json
@@ -0,0 +1,45 @@
+{
+  "name": "xos-openVPNDashboard",
+  "version": "1.0.0",
+  "description": "Angular Application for XOS, created with generator-xos",
+  "scripts": {
+    "prestart": "npm install && bower install",
+    "start": "gulp serve",
+    "prebuild": "npm install && bower install",
+    "build": "gulp",
+    "test": "karma start",
+    "lint": "eslint src/js/"
+  },
+  "keywords": [
+    "XOS",
+    "Angular",
+    "XOSlib"
+  ],
+  "author": "Jeremy Mowery",
+  "license": "MIT",
+  "dependencies": {},
+  "devDependencies": {
+    "browser-sync": "^2.9.11",
+    "del": "^2.0.2",
+    "gulp": "^3.9.0",
+    "gulp-angular-filesort": "^1.1.1",
+    "gulp-angular-templatecache": "^1.8.0",
+    "gulp-babel": "^5.3.0",
+    "gulp-concat": "^2.6.0",
+    "gulp-inject": "^3.0.0",
+    "gulp-minify-html": "^1.0.4",
+    "gulp-rename": "^1.2.2",
+    "gulp-replace": "^0.5.4",
+    "gulp-uglify": "^1.4.2",
+    "http-proxy": "^1.12.0",
+    "proxy-middleware": "^0.15.0",
+    "run-sequence": "^1.1.4",
+    "wiredep": "^3.0.0-beta",
+    "wrench": "^1.5.8",
+    "gulp-ng-annotate": "^1.1.0",
+    "lodash": "^3.10.1",
+    "eslint": "^1.8.0",
+    "eslint-plugin-angular": "linkmesrl/eslint-plugin-angular",
+    "gulp-eslint": "^1.0.0"
+  }
+}
diff --git a/views/ngXosViews/openVPNDashboard/spec/sample.test.js b/views/ngXosViews/openVPNDashboard/spec/sample.test.js
new file mode 100644
index 0000000..822c114
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/spec/sample.test.js
@@ -0,0 +1,37 @@
+'use strict';
+
+describe('The User List', () => {
+  
+  var scope, element, isolatedScope, httpBackend;
+
+  beforeEach(module('xos.openVPNDashboard'));
+  beforeEach(module('templates'));
+
+  beforeEach(inject(function($httpBackend, $compile, $rootScope){
+    
+    httpBackend = $httpBackend;
+    // Setting up mock request
+    $httpBackend.expectGET('/xos/users/?no_hyperlinks=1').respond([
+      {
+        email: 'jermowery@email.arizona.edu',
+        firstname: 'Jeremy',
+        lastname: 'Mowery' 
+      }
+    ]);
+  
+    scope = $rootScope.$new();
+    element = angular.element('<users-list></users-list>');
+    $compile(element)(scope);
+    scope.$digest();
+    isolatedScope = element.isolateScope().vm;
+  }));
+
+  it('should load 1 users', () => {
+    httpBackend.flush();
+    expect(isolatedScope.users.length).toBe(1);
+    expect(isolatedScope.users[0].email).toEqual('jermowery@email.arizona.edu');
+    expect(isolatedScope.users[0].firstname).toEqual('Jeremy');
+    expect(isolatedScope.users[0].lastname).toEqual('Mowery');
+  });
+
+});
\ No newline at end of file
diff --git a/views/ngXosViews/openVPNDashboard/src/css/openVPNDashboard.css b/views/ngXosViews/openVPNDashboard/src/css/openVPNDashboard.css
new file mode 100644
index 0000000..085d5d4
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/src/css/openVPNDashboard.css
@@ -0,0 +1,14 @@
+#xosOpenVPNDashboard{
+  width: 70%;
+  margin: auto;
+}
+.vpn-row {
+    display: table-row;
+}
+.vpn-cell {
+    display: table-cell;
+    padding: 5px;
+}
+.vpn-header {
+    font-weight: bold;
+}
diff --git a/views/ngXosViews/openVPNDashboard/src/index.html b/views/ngXosViews/openVPNDashboard/src/index.html
new file mode 100644
index 0000000..83048df
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/src/index.html
@@ -0,0 +1,34 @@
+<!-- browserSync -->
+<!-- bower:css -->
+<link rel="stylesheet" href="vendor/bootstrap-css/css/bootstrap.css" />
+<!-- endbower --><!-- endcss -->
+<!-- inject:css -->
+<link rel="stylesheet" href="/css/openVPNDashboard.css">
+<!-- endinject -->
+
+<div ng-app="xos.openVPNDashboard" id="xosOpenVPNDashboard">
+    <div ui-view></div>
+</div>
+
+<!-- bower:js -->
+<script src="vendor/jquery/dist/jquery.js"></script>
+<script src="vendor/angular/angular.js"></script>
+<script src="vendor/angular-mocks/angular-mocks.js"></script>
+<script src="vendor/angular-ui-router/release/angular-ui-router.js"></script>
+<script src="vendor/angular-cookies/angular-cookies.js"></script>
+<script src="vendor/angular-resource/angular-resource.js"></script>
+<script src="vendor/ng-lodash/build/ng-lodash.js"></script>
+<script src="vendor/bootstrap-css/js/bootstrap.js"></script>
+<!-- endbower --><!-- endjs -->
+<!-- inject:js -->
+<script src="/xosHelpers/src/xosHelpers.module.js"></script>
+<script src="/xosHelpers/src/ui_components/table/table.component.js"></script>
+<script src="/xosHelpers/src/ui_components/ui-components.module.js"></script>
+<script src="/xosHelpers/src/services/noHyperlinks.interceptor.js"></script>
+<script src="/xosHelpers/src/services/csrfToken.interceptor.js"></script>
+<script src="/xosHelpers/src/services/api.services.js"></script>
+<script src="/api/ng-xoslib.js"></script>
+<script src="/api/ng-xos.js"></script>
+<script src="/api/ng-hpcapi.js"></script>
+<script src="/.tmp/main.js"></script>
+<!-- endinject -->
diff --git a/views/ngXosViews/openVPNDashboard/src/js/main.js b/views/ngXosViews/openVPNDashboard/src/js/main.js
new file mode 100644
index 0000000..04d5d76
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/src/js/main.js
@@ -0,0 +1,61 @@
+'use strict';
+
+angular.module('xos.openVPNDashboard', [
+  'ngResource',
+  'ngCookies',
+  'ngLodash',
+  'ui.router',
+  'xos.helpers'
+])
+.config(($stateProvider) => {
+  $stateProvider
+  .state('openVPNList', {
+    url: '/',
+    template: '<vpn-list></vpn-list>'
+  });
+})
+.config(($compileProvider) => {
+  $compileProvider.aHrefSanitizationWhitelist(
+    /^\s*(https?|ftp|mailto|tel|file|blob):/);
+})
+.service('Vpn', function($http, $q){
+
+  this.getOpenVpnTenants = () => {
+    let deferred = $q.defer();
+
+    $http.get('/xoslib/openvpntenant/')
+    .then((res) => {
+      deferred.resolve(res.data)
+    })
+    .catch((e) => {
+      deferred.reject(e);
+    });
+
+    return deferred.promise;
+  }
+})
+.config(function($httpProvider){
+  $httpProvider.interceptors.push('NoHyperlinks');
+})
+.directive('vpnList', function(){
+  return {
+    restrict: 'E',
+    scope: {},
+    bindToController: true,
+    controllerAs: 'vm',
+    templateUrl: 'templates/openvpn-list.tpl.html',
+    controller: function(Vpn){
+      Vpn.getOpenVpnTenants()
+      .then((vpns) => {
+        this.vpns = vpns;
+        for (var i = 0; i < this.vpns.length; i++) {
+          var blob = new Blob([this.vpns[i].script_text], {type: 'text/plain'});
+          this.vpns[i].script_text = (window.URL || window.webkitURL).createObjectURL( blob );
+        }
+      })
+      .catch((e) => {
+        throw new Error(e);
+      });
+    }
+  };
+});
diff --git a/views/ngXosViews/openVPNDashboard/src/templates/openvpn-list.tpl.html b/views/ngXosViews/openVPNDashboard/src/templates/openvpn-list.tpl.html
new file mode 100644
index 0000000..0c7635f
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/src/templates/openvpn-list.tpl.html
@@ -0,0 +1,19 @@
+<div style="display: table;">
+  <div class="vpn-row">
+    <h1 class="vpn-cell">VPN List</h1>
+  </div>
+  <div class="vpn-row">
+    <div class="vpn-cell vpn-header">ID</div>
+    <div class="vpn-cell vpn-header">VPN Network</div>
+    <div class="vpn-cell vpn-header">VPN Subnet</div>
+    <div class="vpn-cell vpn-header">Script Link</div>
+  </div>
+  <div class="vpn-row" ng-repeat="vpn in vm.vpns">
+    <div class="vpn-cell">{{ vpn.id }}</div>
+    <div class="vpn-cell">{{ vpn.server_network }}</div>
+    <div class="vpn-cell">{{ vpn.vpn_subnet }}</div>
+    <div class="vpn-cell">
+      <a download="connect-{{ vpn.id }}.vpn" ng-href="{{ vpn.script_text }}">Script</a>
+    </div>
+  </div>
+</div>
diff --git a/views/npm-debug.log b/views/npm-debug.log
new file mode 100644
index 0000000..38c9da9
--- /dev/null
+++ b/views/npm-debug.log
@@ -0,0 +1,20 @@
+0 info it worked if it ends with ok
+1 verbose cli [ '/usr/bin/nodejs', '/usr/bin/npm', 'start' ]
+2 info using npm@3.6.0
+3 info using node@v5.7.0
+4 verbose stack Error: ENOENT: no such file or directory, open '/home/jeremy/xos/views/package.json'
+4 verbose stack     at Error (native)
+5 verbose cwd /home/jeremy/xos/views
+6 error Linux 4.2.0-19-generic
+7 error argv "/usr/bin/nodejs" "/usr/bin/npm" "start"
+8 error node v5.7.0
+9 error npm  v3.6.0
+10 error path /home/jeremy/xos/views/package.json
+11 error code ENOENT
+12 error errno -2
+13 error syscall open
+14 error enoent ENOENT: no such file or directory, open '/home/jeremy/xos/views/package.json'
+15 error enoent ENOENT: no such file or directory, open '/home/jeremy/xos/views/package.json'
+15 error enoent This is most likely not a problem with npm itself
+15 error enoent and is related to npm not being able to find a file.
+16 verbose exit [ -2, true ]
diff --git a/xos/api/README.md b/xos/api/README.md
index c0244f0..eb2bd28 100644
--- a/xos/api/README.md
+++ b/xos/api/README.md
@@ -1,13 +1,24 @@
 ## XOS REST API
 
-The XOS API importer is automatic and will search this subdirectory and its hierarchy of children for valid API methods. API methods that are descendents of the django View class are discovered automatically. This should include django_rest_framework based Views and Viewsets. This processing is handled by import_methods.py.
+Source for the XOS REST API lives in directory `xos/api`. An importer
+tool, `import_methods.py`, auto-generates the REST API by searching
+this directory (and sub-directories) for valid API methods. These
+methods are descendents of the Django View class. This should include
+django_rest_framework based Views and Viewsets.
 
-A convention is established for locating API methods within the XOS hierarchy. The root of the api will automatically be /api/. Under that are the following paths:
+We establish a convention for locating API methods within the XOS
+hierarchy. The root of the api is automatically `/api/`. Under that
+are the following paths:
 
 * `/api/service` ... API endpoints that are service-wide
 * `/api/tenant` ... API endpoints that are relative to a tenant within a service
 
-For example, `/api/tenant/cord/subscriber/` contains the Subscriber API for the CORD service. 
+For example, `/api/tenant/cord/subscriber/` contains the Subscriber
+API for the CORD service.
 
-The API importer will automatically construct REST paths based on where files are placed within the directory hierarchy. For example, the files in `xos/api/tenant/cord/` will automatically appear at the API endpoint `http://server_name/api/tenant/cord/`. 
-The directory `examples` contains examples that demonstrate using the API from the Linux command line.
+The API importer automatically constructs REST paths based on
+where files are placed within the directory hierarchy. For example,
+the files in `xos/api/tenant/cord/` will automatically appear at the
+API endpoint `http://server_name/api/tenant/cord/`.  The directory
+`examples` contains examples that demonstrate using the API from the
+Linux command line.
diff --git a/xos/configurations/acord/Makefile b/xos/configurations/acord/Makefile
index b5b93fa..cfa04f8 100644
--- a/xos/configurations/acord/Makefile
+++ b/xos/configurations/acord/Makefile
@@ -8,8 +8,9 @@
 cord: 
 	sudo MYIP=$(MYIP) docker-compose up -d
 	bash ../common/wait_for_xos.sh
-	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
-	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
+	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
 
 acord: cord
diff --git a/xos/configurations/acord/ceilometer.yaml b/xos/configurations/acord/ceilometer.yaml
index ff56579..66d5d32 100644
--- a/xos/configurations/acord/ceilometer.yaml
+++ b/xos/configurations/acord/ceilometer.yaml
@@ -164,6 +164,9 @@
     ceilometer-trusty-server-multi-nic:
       type: tosca.nodes.Image
 
+    m1.small:
+      type: tosca.nodes.Flavor
+
     mysite_ceilometer:
       description: Ceilometer Proxy Slice
       type: tosca.nodes.Slice
@@ -177,8 +180,10 @@
           - default_image:
                 node: ceilometer-trusty-server-multi-nic
                 relationship: tosca.relationships.DefaultImage
+          - m1.small:
+                node: m1.small
+                relationship: tosca.relationships.DefaultFlavor
       properties:
-          default_flavor: m1.small
           max_instances: 2
 
 #    mysite_sflow:
diff --git a/xos/configurations/bash/copyin-vtn.sh b/xos/configurations/bash/copyin-vtn.sh
deleted file mode 100644
index ef18704..0000000
--- a/xos/configurations/bash/copyin-vtn.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#! /bin/bash
-
-export SETUPDIR=/root/setup
-
-# copy in file necessary to setup VTN
-
-cd ../cord
-CONTAINER=$( docker ps|grep "xos"|awk '{print $NF}' )
-make vtn_network_cfg_json
-docker cp $SETUPDIR/vtn-network-cfg.json $CONTAINER:/root/setup/
-docker cp ../common/id_rsa.pub $CONTAINER:/opt/xos/observers/onos/onos_key.pub
-docker cp ../common/id_rsa $CONTAINER:/opt/xos/observers/onos/onos_key
diff --git a/xos/configurations/common/cloudlab-openstack.yaml b/xos/configurations/common/cloudlab-openstack.yaml
new file mode 100644
index 0000000..969f84c
--- /dev/null
+++ b/xos/configurations/common/cloudlab-openstack.yaml
@@ -0,0 +1,89 @@
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+# Note:
+#   assumes mydeployment.yaml has already been run, and the following exist:
+#       MyDeployment
+#       mysite
+#       padmin@vicci.org
+#       Public Shared IPv4
+#   assumes the following have been created and filled with appropriate data:
+#       /root/setup/admin_openrc
+#       /root/setup/flat_net_name
+#       /root/setup/padmin_public_key
+
+description: >
+    * Adds OpenCloud Sites, Deployments, and Controllers.
+
+imports:
+   - custom_types/xos.yaml
+
+topology_template:
+  node_templates:
+    trusty-server-multi-nic:
+      type: tosca.nodes.Image
+      properties:
+         disk_format: QCOW2
+         container_format: BARE
+
+    MyDeployment:
+      type: tosca.nodes.Deployment
+      properties:
+          no-create: True
+          no-delete: True
+      requirements:
+          - image:
+              node: trusty-server-multi-nic
+              relationship: tosca.relationships.SupportsImage
+
+    CloudLab:
+      type: tosca.nodes.Controller
+      requirements:
+          - deployment:
+              node: MyDeployment
+              relationship: tosca.relationships.ControllerDeployment
+      properties:
+          backend_type: OpenStack
+          version: Kilo
+          auth_url: { get_script_env: [ SELF, adminrc, OS_AUTH_URL, LOCAL_FILE] }
+          admin_user: { get_script_env: [ SELF, adminrc, OS_USERNAME, LOCAL_FILE] }
+          admin_password: { get_script_env: [ SELF, adminrc, OS_PASSWORD, LOCAL_FILE] }
+          admin_tenant: { get_script_env: [ SELF, adminrc, OS_TENANT_NAME, LOCAL_FILE] }
+          rabbit_user: { get_script_env: [ SELF, controller_settings, RABBIT_USER, LOCAL_FILE] }
+          rabbit_password: { get_script_env: [ SELF, controller_settings, RABBIT_PASS, LOCAL_FILE] }
+          rabbit_host: { get_script_env: [ SELF, controller_settings, CONTROLLER_FLAT_LAN_IP, LOCAL_FILE] }
+          domain: Default
+      artifacts:
+          adminrc: /root/setup/admin-openrc.sh
+          controller_settings: /root/setup/controller_settings
+
+    mysite:
+      type: tosca.nodes.Site
+      properties:
+          no-create: True
+          no-delete: True
+      requirements:
+          - deployment:
+               node: MyDeployment
+               relationship: tosca.relationships.SiteDeployment
+               requirements:
+                   - controller:
+                       node: CloudLab
+                       relationship: tosca.relationships.UsesController
+
+    Public shared IPv4:
+      type: tosca.nodes.NetworkTemplate
+      properties:
+          no-create: True
+          no-delete: True
+          shared_network_name: { get_artifact: [ SELF, flat_net_name, LOCAL_FILE] }
+      artifacts:
+          flat_net_name: /root/setup/flat_net_name
+
+    padmin@vicci.org:
+      type: tosca.nodes.User
+      properties:
+          no-create: True
+          no-delete: True
+          public_key: { get_artifact: [ SELF, pubkey, LOCAL_FILE ] }
+      artifacts:
+          pubkey: /root/setup/padmin_public_key
diff --git a/xos/configurations/common/fixtures.yaml b/xos/configurations/common/fixtures.yaml
index 6419211..6d9c0e8 100644
--- a/xos/configurations/common/fixtures.yaml
+++ b/xos/configurations/common/fixtures.yaml
@@ -7,6 +7,11 @@
 
 topology_template:
   node_templates:
+
+# -----------------------------------------------------------------------------
+# Network Parameter Types
+# -----------------------------------------------------------------------------
+
     s_tag:
       type: tosca.nodes.NetworkParameterType
 
@@ -24,3 +29,97 @@
 
     neutron_port_name:
       type: tosca.nodes.NetworkParameterType
+
+# ----------------------------------------------------------------------------
+# Roles
+# ----------------------------------------------------------------------------
+
+    siterole#admin:
+      type: tosca.nodes.SiteRole
+
+    siterole#pi:
+      type: tosca.nodes.SiteRole
+
+    siterole#tech:
+      type: tosca.nodes.SiteRole
+
+    tenantrole#admin:
+      type: tosca.nodes.TenantRole
+
+    tenantrole#access:
+      type: tosca.nodes.TenantRole
+
+    deploymentrole#admin:
+      type: tosca.nodes.DeploymentRole
+
+    slicerole#admin:
+      type: tosca.nodes.SliceRole
+
+    slicerole#access:
+      type: tosca.nodes.SliceRole
+
+# -----------------------------------------------------------------------------
+# Flavors
+# -----------------------------------------------------------------------------
+
+    m1.small:
+      type: tosca.nodes.Flavor
+
+    m1.medium:
+      type: tosca.nodes.Flavor
+
+    m1.large:
+      type: tosca.nodes.Flavor
+
+# -----------------------------------------------------------------------------
+# Dashboard Views
+# -----------------------------------------------------------------------------
+
+    xsh:
+      type: tosca.nodes.DashboardView
+      properties:
+          url: template:xsh
+
+    Customize:
+      type: tosca.nodes.DashboardView
+      properties:
+          url: template:customize
+
+    Tenant:
+      type: tosca.nodes.DashboardView
+      properties:
+          url: template:xosTenant
+
+    Developer:
+      type: tosca.nodes.DashboardView
+      properties:
+          url: template:xosDeveloper_datatables
+
+# -----------------------------------------------------------------------------
+# Network Templates
+# -----------------------------------------------------------------------------
+
+    Private:
+      type: tosca.nodes.NetworkTemplate
+      properties:
+          visibility: private
+          translation: none
+
+    Public shared IPv4:
+      type: tosca.nodes.NetworkTemplate
+      properties:
+          visibility: private
+          translation: NAT
+          shared_network_name: nat-net
+
+    Public dedicated IPv4:
+      type: tosca.nodes.NetworkTemplate
+      properties:
+          visibility: public
+          translation: none
+          shared_network_name: ext-net
+
+
+
+
+
diff --git a/xos/configurations/common/mydeployment.yaml b/xos/configurations/common/mydeployment.yaml
new file mode 100644
index 0000000..66bb75d
--- /dev/null
+++ b/xos/configurations/common/mydeployment.yaml
@@ -0,0 +1,69 @@
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+description: Some basic fixtures
+
+imports:
+   - custom_types/xos.yaml
+
+topology_template:
+  node_templates:
+    m1.large:
+      type: tosca.nodes.Flavor
+
+    m1.medium:
+      type: tosca.nodes.Flavor
+
+    m1.small:
+      type: tosca.nodes.Flavor
+
+    MyDeployment:
+      type: tosca.nodes.Deployment
+      requirements:
+          - m1.large:
+             node: m1.large
+             relationship: tosca.relationships.SupportsFlavor
+          - m1.medium:
+             node: m1.medium
+             relationship: tosca.relationships.SupportsFlavor
+          - m1.small:
+             node: m1.small
+             relationship: tosca.relationships.SupportsFlavor
+
+    mysite:
+      type: tosca.nodes.Site
+      properties:
+          display_name: MySite
+      requirements:
+          - deployment:
+               node: MyDeployment
+               relationship: tosca.relationships.SiteDeployment
+
+    # Attach the Tenant view to the MyDeployment deployment
+    Tenant:
+      type: tosca.nodes.DashboardView
+      properties:
+          no-create: true
+          no-delete: true
+      requirements:
+          - deployment:
+              node: MyDeployment
+              relationship: tosca.relationships.SupportsDeployment
+
+    padmin@vicci.org:
+      type: tosca.nodes.User
+      properties:
+          password: letmein
+#          encrypted_password: pbkdf2_sha256$12000$Qufx9iqtaYma$xs0YurPOcj9qYQna/Qrb3K+im9Yr2XEVr0J4Kqek7AE=
+          firstname: XOS
+          lastname: admin
+          is_admin: true
+      requirements:
+          - site:
+              node: mysite
+              relationship: tosca.relationships.MemberOfSite
+          - tenant_dashboard:
+              node: Tenant
+              relationship: tosca.relationships.UsesDashboard
+
+
+
diff --git a/xos/configurations/cord-deprecated/Makefile b/xos/configurations/cord-deprecated/Makefile
index 1e53e79..184f2d5 100644
--- a/xos/configurations/cord-deprecated/Makefile
+++ b/xos/configurations/cord-deprecated/Makefile
@@ -8,8 +8,9 @@
 cord: virtualbng_json vtn_network_cfg_json
 	sudo MYIP=$(MYIP) docker-compose up -d
 	bash ../common/wait_for_xos.sh
-	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
-	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
+	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/cord/cord.yaml
 
diff --git a/xos/configurations/cord-deprecated/ceilometer.yaml b/xos/configurations/cord-deprecated/ceilometer.yaml
index 3724265..464b07b 100644
--- a/xos/configurations/cord-deprecated/ceilometer.yaml
+++ b/xos/configurations/cord-deprecated/ceilometer.yaml
@@ -164,6 +164,9 @@
     ceilometer-trusty-server-multi-nic:
       type: tosca.nodes.Image
 
+    m1.small:
+      type: tosca.nodes.Flavor
+
     mysite_ceilometer:
       description: Ceilometer Proxy Slice
       type: tosca.nodes.Slice
@@ -177,8 +180,9 @@
           - default_image:
                 node: ceilometer-trusty-server-multi-nic
                 relationship: tosca.relationships.DefaultImage
-      properties:
-          default_flavor: m1.small
+          - default_flavor:
+                node: m1.small
+                relationship: tosca.relationships.DefaultFlavor
 
 #    mysite_sflow:
 #      description: Slice for sFlow service
diff --git a/xos/configurations/cord-pod/Makefile b/xos/configurations/cord-pod/Makefile
index 9296f10..ce1baf8 100644
--- a/xos/configurations/cord-pod/Makefile
+++ b/xos/configurations/cord-pod/Makefile
@@ -2,16 +2,17 @@
 xos: nodes.yaml images.yaml
 	sudo docker-compose up -d
 	../common/wait_for_xos_port.sh 80
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
 	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/setup.yaml
 	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
 	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/images.yaml
 
-vtn:
+vtn: vtn-external.yaml
 	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/vtn-external.yaml
 
-cord: virtualbng_json
+cord: 
 	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/mgmt-net.yaml
-	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
 	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/cord-vtn-vsg.yaml
 
 exampleservice:
@@ -26,6 +27,9 @@
 images.yaml:
 	export SETUPDIR=.; bash ../common/make-images-yaml.sh
 
+vtn-external.yaml:
+	export SETUPDIR=.; bash ./make-vtn-external-yaml.sh
+
 virtualbng_json:
 	export SETUPDIR=.; bash ./make-virtualbng-json.sh
 
diff --git a/xos/configurations/cord-pod/README.md b/xos/configurations/cord-pod/README.md
index b1304b7..d5051c9 100644
--- a/xos/configurations/cord-pod/README.md
+++ b/xos/configurations/cord-pod/README.md
@@ -31,7 +31,7 @@
 To set up OpenStack, follow the instructions in the
 [README.md](https://github.com/open-cloud/openstack-cluster-setup/blob/master/README.md)
 file of the [open-cloud/openstack-cluster-setup](https://github.com/open-cloud/openstack-cluster-setup/)
-repository.  If you're just getting started with CORD, it's probably best to begin with the 
+repository.  If you're just getting started with CORD, it's probably best to begin with the
 single-node CORD test environment to familiarize yourself with the overall setup.
 
 **NOTE: In order to use the cord-pod configuration, you must set up OpenStack using the above recipe.**
@@ -73,51 +73,82 @@
 ubuntu@xos:~$ cd xos/xos/configurations/cord-pod
 ```
 
-Next, check that the following files exist in this directory:
+Next, check that the following files exist in this directory
+(they will have been put there for you by the cluster installation scripts):
 
  * *admin-openrc.sh*: Admin credentials for your OpenStack cloud
  * *id_rsa[.pub]*: A keypair that will be used by the various services
  * *node_key*: A private key that allows root login to the compute nodes
 
-They will have been put there for you by the cluster installation scripts.
+XOS can then be brought up for CORD by running a few `make` commands.
+First, run:
 
-**If your setup uses the CORD fabric**, you need to modify the autogenerated VTN
-configuration and node tags, and edit `cord-vtn-vsg.yml` as follows.
-
- 1. The VTN app configuration is autogenerated by XOS.  For more information
-about the configuration, see [this page on the ONOS Wiki](https://wiki.onosproject.org/display/ONOS/CORD+VTN),
-under the **ONOS Settings** heading.  To see the generated
-configuration, go to http://xos/admin/onos/onosapp/, click on
-*VTN_ONOS_app*, then the *Attributes* tab, and look for the
-`rest_onos/v1/network/configuration/` attribute.  You can edit this
-configuration after deleting the `autogenerate` attribute (otherwise XOS will
-overwrite your changes), or you can change the other
-attributes and delete  `rest_onos/v1/network/configuration/` in order
-to get XOS to regenerate it.
-
- 2. The process of autoconfiguring VTN also assigns some default values to per-node parameters.  Go to
- http://xos/admin/core/node/, select a node, then select the *Tags* tab.  Configure the following:
-  * `bridgeId` (the ID to set on the node's br-int)
-  * `dataPlaneIntf` (the data plane interface for the fabric on the node)
-  * `dataPlaneIp` (the IP address for the node on the fabric)
-
- 3. Modify `cord-vtn-vsg.yml` and set these parameters to the
-appropriate values for the fabric:
-  * `addresses_vsg:properties:addresses` (IP address block of fabric)
-  * `addresses_vsg:properties:gateway_ip` 
-  * `addresses_vsg:properties:gateway_mac` 
-
-If you're not using the fabric then the default values should be OK.  
-
-XOS can then be brought up for CORD by running a few `make` commands:
 ```
 ubuntu@xos:~/xos/xos/configurations/cord-pod$ make
+```
+
+After this you will be able to login to the XOS GUI at
+*http://xos/* using username/password `padmin@vicci.org/letmein`.
+Before proceeding, you should verify that objects in XOS are
+being sync'ed with OpenStack.  Log into the GUI and select *Users*
+at left.  Make sure there is a green check next to `padmin@vicci.org`.
+
+> If you are **not** building the single-node development POD, the next
+> step is to create and edit the VTN configuration.  Run `make vtn-external.yaml`
+> then edit the `vtn-external.yml` TOSCA file.  The `rest_hostname:`
+> field points to the host where ONOS should run the VTN app.  The
+> fields in the `service_vtn` and the objects of type `tosca.nodes.Tag`
+> correspond to the VTN fields listed
+> on [the CORD VTN page on the ONOS Wiki](https://wiki.onosproject.org/display/ONOS/CORD+VTN),
+> under the **ONOS Settings** heading; refer there for the fields'
+> meanings.  
+
+Then run:
+
+```
 ubuntu@xos:~/xos/xos/configurations/cord-pod$ make vtn
+```
+The above step configures the ONOS VTN app by generating a configuration
+and pushing it to ONOS.  You are able to see and modify the configuration
+via the GUI as follows:
+
+* To see the generated configuration, go to *http://xos/admin/onos/onosapp/*, select
+*VTN_ONOS_app*, then the *Attributes* tab, and look for the
+`rest_onos/v1/network/configuration/` attribute.  
+
+* To change the VTN configuration, modify the fields of the VTN Service object
+and the Tag objects associated with Nodes.  Don't forget to select *Save*.
+
+* After modifying the above fields, delete the `rest_onos/v1/network/configuration/` attribute
+in the *ONOS_VTN_app* and select *Save*.  The attribute will be regenerated using the new information.
+
+* Alternatively, if you want to load your own VTN configuration manually, you can delete the
+`autogenerate` attribute from the *ONOS_VTN_app*, edit the configuration in the
+`rest_onos/v1/network/configuration/` attribute, and select *Save*.
+
+Before proceeding, check that the VTN app is controlling Open vSwitch on the compute nodes.  Log
+into ONOS and run the `cordvtn-nodes` command:
+
+```
+$ ssh -p 8101 karaf@onos-cord   # password is karaf
+onos> cordvtn-nodes
+hostname=nova-compute, hostMgmtIp=192.168.122.177/24, dpIp=192.168.199.1/24, br-int=of:0000000000000001, dpIntf=veth1, init=COMPLETE
+Total 1 nodes
+```
+The important part is the `init=COMPLETE` at the end.  If you do not see this, refer to
+[the CORD VTN page on the ONOS Wiki](https://wiki.onosproject.org/display/ONOS/CORD+VTN) for
+help fixing the problem.  This must be working to bring up VMs on the POD.
+
+> If you are **not** building the single-node development POD, modify `cord-vtn-vsg.yml` 
+> and change `addresses_vsg` so that it contains the IP address block,
+> gateway IP, and gateway MAC of the CORD fabric.  
+
+Then run:
+
+```
 ubuntu@xos:~/xos/xos/configurations/cord-pod$ make cord
 ```
 
-After the first 'make' command above, you will be able to login to XOS at
-*http://xos/* using username/password `padmin@vicci.org/letmein`.
 
 ### Inspecting the vSG
 
diff --git a/xos/configurations/cord-pod/ceilometer.yaml b/xos/configurations/cord-pod/ceilometer.yaml
index 3b32345..d07f2e9 100644
--- a/xos/configurations/cord-pod/ceilometer.yaml
+++ b/xos/configurations/cord-pod/ceilometer.yaml
@@ -171,6 +171,9 @@
     ceilometer-trusty-server-multi-nic:
       type: tosca.nodes.Image
 
+    m1.small:
+      type: tosca.nodes.Flavor
+
     mysite_ceilometer:
       description: Ceilometer Proxy Slice
       type: tosca.nodes.Slice
@@ -187,8 +190,9 @@
           - management:
               node: management
               relationship: tosca.relationships.ConnectsToNetwork
-      properties:
-          default_flavor: m1.small
+          - m1.small:
+              node: m1.small
+              relationship: tosca.relationships.DefaultFlavor
 
 #    mysite_sflow:
 #      description: Slice for sFlow service
diff --git a/xos/configurations/cord-pod/docker-compose.yml b/xos/configurations/cord-pod/docker-compose.yml
index 3666d47..5087485 100644
--- a/xos/configurations/cord-pod/docker-compose.yml
+++ b/xos/configurations/cord-pod/docker-compose.yml
@@ -13,7 +13,7 @@
 #        - xos_db
 #    volumes:
 #        - ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
-#        - xos_cord_config:/opt/xos/xos_configuration/xos_cord_config:ro
+#        - ./xos_cord_config:/opt/xos/xos_configuration/xos_cord_config:ro
 #        - .:/root/setup:ro
 #        - ../vtn/files/xos_vtn_config:/opt/xos/xos_configuration/xos_vtn_config:ro
 #        - ./images:/opt/xos/images:ro
@@ -96,7 +96,7 @@
     volumes:
         - .:/root/setup:ro
         - ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
-        - xos_cord_config:/opt/xos/xos_configuration/xos_cord_config:ro
+        - ./xos_cord_config:/opt/xos/xos_configuration/xos_cord_config:ro
         - ../vtn/files/xos_vtn_config:/opt/xos/xos_configuration/xos_vtn_config:ro
         - ./id_rsa.pub:/opt/xos/synchronizers/onos/onos_key.pub:ro
         - ./id_rsa.pub:/opt/xos/synchronizers/vcpe/vcpe_public_key:ro
diff --git a/xos/configurations/cord-pod/make-vtn-external-yaml.sh b/xos/configurations/cord-pod/make-vtn-external-yaml.sh
new file mode 100644
index 0000000..6a69783
--- /dev/null
+++ b/xos/configurations/cord-pod/make-vtn-external-yaml.sh
@@ -0,0 +1,122 @@
+FN=$SETUPDIR/vtn-external.yaml
+
+rm -f $FN
+
+cat >> $FN <<EOF
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+imports:
+   - custom_types/xos.yaml
+
+description: autogenerated node tags file for VTN configuration
+
+topology_template:
+  node_templates:
+
+    service_ONOS_CORD:
+      type: tosca.nodes.ONOSService
+      requirements:
+      properties:
+          kind: onos
+          view_url: /admin/onos/onosservice/$id$/
+          no_container: true
+          rest_hostname: onos-cord
+          rest_onos/v1/network/configuration/: >
+            {
+              "devices" : {
+                "of:1000000000000001" : {
+                  "accessDevice" : {
+                    "uplink" : "2",
+                    "vlan"   : "222",
+                    "defaultVlan" : "0"
+                  },
+                  "basic" : {
+                    "driver" : "pmc-olt"
+                  }
+                }
+              }
+            }
+
+    service_vtn:
+      type: tosca.nodes.VTNService
+      properties:
+          view_url: /admin/vtn/vtnservice/$id$/
+          privateGatewayMac: 00:00:00:00:00:01
+          localManagementIp: 172.27.0.1/24
+          ovsdbPort: 6641
+          sshUser: root
+          sshKeyFile: /root/node_key
+          sshPort: 22
+          xosEndpoint: http://xos/
+          xosUser: padmin@vicci.org
+          xosPassword: letmein
+
+EOF
+
+NODES=$( bash -c "source $SETUPDIR/admin-openrc.sh ; nova host-list" |grep compute|awk '{print $2}' )
+I=0
+for NODE in $NODES; do
+    echo $NODE
+    cat >> $FN <<EOF
+    $NODE:
+      type: tosca.nodes.Node
+
+    # VTN bridgeId field for node $NODE
+    ${NODE}_bridgeId_tag:
+      type: tosca.nodes.Tag
+      properties:
+          name: bridgeId
+          value: of:0000000000000001
+      requirements:
+          - target:
+              node: $NODE
+              relationship: tosca.relationships.TagsObject
+          - service:
+              node: service_ONOS_CORD
+              relationship: tosca.relationships.MemberOfService
+
+    # VTN dataPlaneIntf field for node $NODE
+    ${NODE}_dataPlaneIntf_tag:
+      type: tosca.nodes.Tag
+      properties:
+          name: dataPlaneIntf
+          value: veth1
+      requirements:
+          - target:
+              node: $NODE
+              relationship: tosca.relationships.TagsObject
+          - service:
+              node: service_ONOS_CORD
+              relationship: tosca.relationships.MemberOfService
+
+    # VTN dataPlaneIp field for node $NODE
+    ${NODE}_dataPlaneIp_tag:
+      type: tosca.nodes.Tag
+      properties:
+          name: dataPlaneIp
+          value: 10.168.0.253/24
+      requirements:
+          - target:
+              node: $NODE
+              relationship: tosca.relationships.TagsObject
+          - service:
+              node: service_ONOS_CORD
+              relationship: tosca.relationships.MemberOfService
+
+EOF
+done
+
+cat >> $FN <<EOF
+    VTN_ONOS_app:
+      type: tosca.nodes.ONOSVTNApp
+      requirements:
+          - onos_tenant:
+              node: service_ONOS_CORD
+              relationship: tosca.relationships.TenantOfService
+          - vtn_service:
+              node: service_vtn
+              relationship: tosca.relationships.UsedByService
+      properties:
+          dependencies: org.onosproject.drivers, org.onosproject.drivers.ovsdb, org.onosproject.openflow-base, org.onosproject.ovsdb-base, org.onosproject.dhcp, org.onosproject.cordvtn, org.onosproject.olt, org.onosproject.igmp, org.onosproject.cordmcast
+          autogenerate: vtn-network-cfg
+EOF
\ No newline at end of file
diff --git a/xos/configurations/cord-pod/vtn-external.yaml b/xos/configurations/cord-pod/vtn-external.yaml
deleted file mode 100644
index bc0f65c..0000000
--- a/xos/configurations/cord-pod/vtn-external.yaml
+++ /dev/null
@@ -1,51 +0,0 @@
-tosca_definitions_version: tosca_simple_yaml_1_0
-
-description: Set up ONOS VTN app
-
-imports:
-   - custom_types/xos.yaml
-
-topology_template:
-  node_templates:
-
-    service_ONOS_CORD:
-      type: tosca.nodes.ONOSService
-      requirements:
-      properties:
-          kind: onos
-          view_url: /admin/onos/onosservice/$id$/
-          no_container: true
-          rest_hostname: onos-cord
-          rest_onos/v1/network/configuration/: >
-            {
-              "devices" : {
-                "of:0000000000000001" : {
-                  "accessDevice" : {
-                    "uplink" : "2",
-                    "vlan"   : "222",
-                    "defaultVlan" : "0"
-                  },
-                  "basic" : {
-                    "driver" : "pmc-olt"
-                  }
-                }
-              }
-            }
-
-    VTN_ONOS_app:
-      type: tosca.nodes.ONOSVTNApp
-      requirements:
-          - onos_tenant:
-              node: service_ONOS_CORD
-              relationship: tosca.relationships.TenantOfService
-          - vtn_service:
-              node: service_vtn
-              relationship: tosca.relationships.UsedByService
-      properties:
-          dependencies: org.onosproject.drivers, org.onosproject.drivers.ovsdb, org.onosproject.openflow-base, org.onosproject.ovsdb-base, org.onosproject.dhcp, org.onosproject.cordvtn, org.onosproject.olt, org.onosproject.igmp, org.onosproject.cordmcast
-          autogenerate: vtn-network-cfg
-
-    service_vtn:
-      type: tosca.nodes.VTNService
-      properties:
-          view_url: /admin/vtn/vtnservice/$id$/
diff --git a/xos/configurations/devel/Makefile b/xos/configurations/devel/Makefile
index 1e650f3..e55f38b 100644
--- a/xos/configurations/devel/Makefile
+++ b/xos/configurations/devel/Makefile
@@ -7,7 +7,9 @@
 xos:
 	sudo MYIP=$(MYIP) docker-compose up -d
 	bash ../common/wait_for_xos.sh
-	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
 
 containers:
diff --git a/xos/configurations/frontend/Makefile b/xos/configurations/frontend/Makefile
index ee2739c..09c714b 100644
--- a/xos/configurations/frontend/Makefile
+++ b/xos/configurations/frontend/Makefile
@@ -4,6 +4,8 @@
 	sudo make -f ../common/Makefile.prereqs
 	sudo docker-compose up -d
 	bash ../common/wait_for_xos.sh
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
 	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/frontend/sample.yaml
 
 containers:
@@ -28,7 +30,6 @@
 	sudo docker exec frontend_xos_1 touch /opt/xos/xos/settings.py
 
 mock-cord:
-	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
 	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/frontend/mocks/cord.yaml
 	sudo docker exec frontend_xos_1 cp /opt/xos/configurations/cord/xos_cord_config /opt/xos/xos_configuration/
 	sudo docker exec frontend_xos_1 touch /opt/xos/xos/settings.py
diff --git a/xos/configurations/frontend/xos.sql b/xos/configurations/frontend/xos.sql
index 7b13f4d..bbfd15a 100644
--- a/xos/configurations/frontend/xos.sql
+++ b/xos/configurations/frontend/xos.sql
@@ -5163,12 +5163,6 @@
 265	Can add vbng tenant	12	add_vbngtenant
 266	Can change vbng tenant	12	change_vbngtenant
 267	Can delete vbng tenant	12	delete_vbngtenant
-268	Can add Hello World Service	7	add_helloworldservicecomplete
-269	Can change Hello World Service	7	change_helloworldservicecomplete
-270	Can delete Hello World Service	7	delete_helloworldservicecomplete
-271	Can add Hello World Tenant	12	add_helloworldtenantcomplete
-272	Can change Hello World Tenant	12	change_helloworldtenantcomplete
-273	Can delete Hello World Tenant	12	delete_helloworldtenantcomplete
 274	Can add ONOS Service	7	add_onosservice
 275	Can change ONOS Service	7	change_onosservice
 276	Can delete ONOS Service	7	delete_onosservice
@@ -6509,8 +6503,6 @@
 87	cord subscriber root	cord	cordsubscriberroot
 88	vOLT Service	cord	voltservice
 89	vSG Service	cord	vsgservice
-90	Hello World Tenant	helloworldservice_complete	helloworldtenantcomplete
-91	Hello World Service	helloworldservice_complete	helloworldservicecomplete
 92	ONOS Service	onos	onosservice
 93	onos app	onos	onosapp
 94	s flow tenant	ceilometer	sflowtenant
@@ -6548,7 +6540,6 @@
 4	auth	0001_initial	2016-04-05 17:41:46.384468+00
 5	ceilometer	0001_initial	2016-04-05 17:41:46.659809+00
 6	cord	0001_initial	2016-04-05 17:41:46.862406+00
-7	helloworldservice_complete	0001_initial	2016-04-05 17:41:47.056651+00
 8	hpc	0001_initial	2016-04-05 17:41:50.450946+00
 9	onos	0001_initial	2016-04-05 17:41:50.637887+00
 10	requestrouter	0001_initial	2016-04-05 17:41:51.319325+00
diff --git a/xos/configurations/opencloud/Makefile b/xos/configurations/opencloud/Makefile
index aef4946..03168ed 100644
--- a/xos/configurations/opencloud/Makefile
+++ b/xos/configurations/opencloud/Makefile
@@ -1,6 +1,7 @@
 xos:
 	sudo docker-compose up -d
 	bash ./wait_for_xos.sh
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
 	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/opencloud/opencloud.yaml
 
 containers:
diff --git a/xos/configurations/openvpn/Makefile b/xos/configurations/openvpn/Makefile
new file mode 100644
index 0000000..aa6f2d5
--- /dev/null
+++ b/xos/configurations/openvpn/Makefile
@@ -0,0 +1,59 @@
+MYIP:=$(shell hostname -i)
+
+cloudlab: common_cloudlab xos
+
+xos:
+	sudo MYIP=$(MYIP) docker-compose up -d
+	bash ../common/wait_for_xos.sh
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
+	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
+
+frontend:
+	sudo make -f ../common/Makefile.prereqs
+	sudo docker-compose up -d
+	bash ../common/wait_for_xos.sh
+	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/frontend/sample.yaml
+
+containers:
+	cd ../../../containers/xos; make devel
+	cd ../../../containers/synchronizer; make
+	cd ../../../containers/openvpn; make
+
+common_cloudlab:
+	make -C ../common -f Makefile.cloudlab
+
+stop:
+	sudo MYIP=$(MYIP) docker-compose stop
+
+showlogs:
+	sudo MYIP=$(MYIP) docker-compose logs
+
+rm: stop
+	sudo MYIP=$(MYIP) docker-compose rm
+
+ps:
+	sudo MYIP=$(MYIP) docker-compose ps
+
+enter-xos:
+	sudo docker exec -it openvpn_xos_1 bash
+
+enter-synchronizer:
+	sudo docker exec -it openvpn_xos_synchronizer_openvpn_1 bash
+
+upgrade_pkgs:
+	sudo pip install httpie --upgrade
+
+rebuild_xos:
+	make -C ../../../containers/xos devel
+
+rebuild_synchronizer:
+	make -C ../../../containers/synchronizer
+
+cleanup_docker: rm
+	sudo docker rm -v $(docker ps -a -q -f status=exited) || true
+	docker rm -v $(docker ps -a -q -f status=exited) || true
+	sudo docker rmi $(docker images -qf "dangling=true") || true
+	socker rmi $(docker images -qf "dangling=true") || true
+	sudo docker run -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker:/var/lib/docker --rm martin/docker-cleanup-volumes || true
diff --git a/xos/configurations/openvpn/docker-compose.yml b/xos/configurations/openvpn/docker-compose.yml
new file mode 100644
index 0000000..e609838
--- /dev/null
+++ b/xos/configurations/openvpn/docker-compose.yml
@@ -0,0 +1,62 @@
+xos_db:
+    image: xosproject/xos-postgres
+    expose:
+        - "5432"
+
+xos_synchronizer_openstack:
+    image: xosproject/xos-synchronizer-openstack
+    command: bash -c "sleep 120; python /opt/xos/synchronizers/openstack/xos-synchronizer.py"
+    labels:
+        org.xosproject.kind: synchronizer
+        org.xosproject.target: openstack
+    links:
+        - xos_db
+    extra_hosts:
+        - ctl:${MYIP}
+    volumes:
+        - ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
+        - ./images:/opt/xos/images:ro
+
+xos_synchronizer_openvpn:
+    image: xosproject/xos-openvpn
+    command: bash -c "sleep 120 ; python /opt/xos/synchronizers/openvpn/openvpn-synchronizer.py -C /opt/xos/synchronizers/openvpn/openvpn_config"
+    labels:
+        org.xosproject.kind: synchronizer
+        org.xosproject.target: openvpn
+    links:
+        - xos_db
+    extra_hosts:
+        - ctl:${MYIP}
+    volumes:
+        - ../setup/id_rsa:/opt/xos/synchronizers/openvpn/openvpn_private_key:ro  # private key
+    volumes_from:
+        - xos_openvpn_data:rw
+
+xos_openvpn_data:
+    image: xosproject/xos-openvpn
+    links:
+        - xos_db
+    extra_hosts:
+        - ctl:${MYIP}
+    volumes:
+        - /opt/openvpn
+
+# FUTURE
+#xos_swarm_synchronizer:
+#    image: xosproject/xos-swarm-synchronizer
+#    labels:
+#        org.xosproject.kind: synchronizer
+#        org.xosproject.target: swarm
+
+xos:
+    image: xosproject/xos-openvpn
+    command: python /opt/xos/manage.py runserver 0.0.0.0:8000 --insecure --makemigrations
+    ports:
+        - "9999:8000"
+    links:
+        - xos_db
+    volumes:
+      - ../setup:/root/setup:ro
+      - ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
+    volumes_from:
+      - xos_openvpn_data:rw
diff --git a/xos/configurations/syndicate/Makefile b/xos/configurations/syndicate/Makefile
index eb8050e..3b29ee5 100644
--- a/xos/configurations/syndicate/Makefile
+++ b/xos/configurations/syndicate/Makefile
@@ -7,7 +7,9 @@
 xos: syndicate_config
 	sudo MYIP=$(MYIP) docker-compose up -d
 	bash ../common/wait_for_xos.sh
-	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
 
 containers:
diff --git a/xos/configurations/test-standalone/Makefile b/xos/configurations/test-standalone/Makefile
index 50f2cc5..3ef5848 100644
--- a/xos/configurations/test-standalone/Makefile
+++ b/xos/configurations/test-standalone/Makefile
@@ -16,8 +16,6 @@
 export TRUNCATE_FN
 
 prepare: xos
-	# INSTALL DEPS
-	# RUN ONCE BEFORE RUNNING TESTS
 	sudo docker exec -i teststandalone_xos_1 bash -c "cd /opt/xos/tests/api; npm install --production"
 	sudo docker exec teststandalone_xos_1 pip install dredd_hooks
 
@@ -25,17 +23,15 @@
 	sudo make -f ../common/Makefile.prereqs
 	sudo docker-compose up -d
 	bash ../common/wait_for_xos.sh
-	# sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/frontend/sample.yaml
-	# sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
 
 restore-initial-db-status:
 	sudo docker exec teststandalone_xos_db_1 psql -U postgres -d xos -c "$$TRUNCATE_FN"
 	sudo docker exec teststandalone_xos_db_1 psql -U postgres -d xos -c "SELECT truncate_tables('postgres');"
 	sudo docker exec teststandalone_xos_db_1 psql -U postgres -d xos -c "SELECT setval('core_tenant_id_seq', 1)"
 	sudo docker-compose run xos python /opt/xos/manage.py --noobserver --nomodelpolicy loaddata /opt/xos/core/fixtures/core_initial_data.json
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
 	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/frontend/sample.yaml
-	sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
-
 
 
 test: restore-initial-db-status
@@ -45,10 +41,6 @@
 test-tosca:
 	sudo docker-compose run xos bash -c "cd /opt/xos/tosca/tests; python ./alltests.py"
 
-test-gui:
-	cd ../../../views/ngXosViews/ceilometerDashboard; npm install
-	cd ../../../views/ngXosViews/ceilometerDashboard; npm run test:ci
-
 base-container: 
 	cd ../../../containers/xos; make devel
 
diff --git a/xos/configurations/test/Makefile b/xos/configurations/test/Makefile
index adf349a..d4e54fd 100644
--- a/xos/configurations/test/Makefile
+++ b/xos/configurations/test/Makefile
@@ -7,7 +7,9 @@
 xos:
 	sudo MYIP=$(MYIP) docker-compose up -d
 	bash ../common/wait_for_xos.sh
-	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+	sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
 	sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
 	sudo MYIP=$(MYIP) docker-compose run xos bash -c "cd /opt/xos/tosca/tests; python ./alltests.py"
 	sudo MYIP=$(MYIP) docker-compose run xos bash -c "cd /opt/xos/tosca/tests; python ./allObserverTests.py"
diff --git a/xos/core/admin.py b/xos/core/admin.py
index f14710b..5cc0ddd 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -977,6 +977,24 @@
                       )
 
 
+class TenantRoleAdmin(XOSBaseAdmin):
+    """Admin for TenantRoles."""
+    model = TenantRole
+    fields = ('role',)
+
+
+class TenantPrivilegeInline(XOSTabularInline):
+    """Inline for adding a TenantPrivilege to a Tenant."""
+    model = TenantPrivilege
+    extra = 0
+    suit_classes = 'suit-tab suit-tab-tenantprivileges'
+    fields = ['backend_status_icon', 'user', 'role', 'tenant']
+    readonly_fields = ('backend_status_icon', )
+
+    def queryset(self, request):
+        return TenantPrivilege.select_by_user(request.user)
+
+
 class ProviderTenantInline(XOSTabularInline):
     model = CoarseTenant
     fields = ['provider_service', 'subscriber_service', 'connect_method']
@@ -2404,5 +2422,6 @@
     admin.site.register(Flavor, FlavorAdmin)
     admin.site.register(TenantRoot, TenantRootAdmin)
     admin.site.register(TenantRootRole, TenantRootRoleAdmin)
+    admin.site.register(TenantRole, TenantRoleAdmin)
     admin.site.register(TenantAttribute, TenantAttributeAdmin)
     admin.site.register(AddressPool, AddressPoolAdmin)
diff --git a/xos/core/fixtures/core_initial_data.json b/xos/core/fixtures/core_initial_data.json
index 86658bb..01ff999 100644
--- a/xos/core/fixtures/core_initial_data.json
+++ b/xos/core/fixtures/core_initial_data.json
@@ -1,214 +1,6 @@
 [
 {
     "fields": {
-        "updated": "2015-02-17T22:06:37.837Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:37.837Z",
-        "deleted": false,
-        "site_url": null,
-        "enabled": true,
-        "longitude": null,
-        "name": "MySite",
-        "backend_register": "{}",
-        "login_base": "mysite",
-        "location": "0,0",
-        "latitude": null,
-        "is_public": true,
-        "backend_status": "0 - Provisioning in progress",
-        "abbreviated_name": "mysite",
-        "enacted": null
-    },
-    "model": "core.site",
-    "pk": 1
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:38.620Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:38.620Z",
-        "deleted": false,
-        "backend_register": "{}",
-        "role": "admin",
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.siterole",
-    "pk": 1
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:38.670Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:38.669Z",
-        "deleted": false,
-        "backend_register": "{}",
-        "role": "pi",
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.siterole",
-    "pk": 2
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:38.731Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:38.730Z",
-        "deleted": false,
-        "backend_register": "{}",
-        "role": "tech",
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.siterole",
-    "pk": 3
-},
-{
-    "fields": {
-        "accessControl": "allow all",
-        "updated": "2015-02-17T22:06:37.789Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:37.789Z",
-        "deleted": false,
-        "name": "MyDeployment",
-        "backend_register": "{}",
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.deployment",
-    "pk": 1
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:38.894Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:38.894Z",
-        "deleted": false,
-        "backend_register": "{}",
-        "role": "admin",
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.deploymentrole",
-    "pk": 1
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:37.893Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:37.893Z",
-        "deleted": false,
-        "availability_zone": null,
-        "site": 1,
-        "backend_register": "{}",
-        "controller": null,
-        "deployment": 1,
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.sitedeployment",
-    "pk": 1
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:38.953Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:38.953Z",
-        "deleted": false,
-        "deployments": [],
-        "enabled": true,
-        "name": "xsh",
-        "backend_register": "{}",
-        "url": "template:xsh",
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.dashboardview",
-    "pk": 1
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:39.011Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:39.011Z",
-        "deleted": false,
-        "deployments": [],
-        "enabled": true,
-        "name": "Customize",
-        "backend_register": "{}",
-        "url": "template:customize",
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.dashboardview",
-    "pk": 2
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:39.244Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:39.069Z",
-        "deleted": false,
-        "deployments": [
-            1
-        ],
-        "enabled": true,
-        "name": "Tenant",
-        "backend_register": "{}",
-        "url": "template:xosTenant",
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.dashboardview",
-    "pk": 3
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:39.302Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:39.302Z",
-        "deleted": false,
-        "deployments": [],
-        "enabled": true,
-        "name": "Developer",
-        "backend_register": "{}",
-        "url": "template:xosDeveloper_datatables",
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.dashboardview",
-    "pk": 4
-},
-{
-    "fields": {
-        "policed": null,
-        "site": 1,
-        "is_staff": true,
-        "timezone": "America/New_York",
-        "backend_status": "Provisioning in progress",
-        "is_registering": false,
-        "last_login": "2015-02-17T22:35:17.822Z",
-        "email": "padmin@vicci.org",
-        "username": "padmin@vicci.org",
-        "updated": "2015-02-17T22:06:38.059Z",
-        "firstname": "XOS",
-        "user_url": null,
-        "deleted": false,
-        "lastname": "admin",
-        "is_active": true,
-        "phone": null,
-        "is_admin": true,
-        "password": "pbkdf2_sha256$12000$Qufx9iqtaYma$xs0YurPOcj9qYQna/Qrb3K+im9Yr2XEVr0J4Kqek7AE=",
-        "enacted": null,
-        "public_key": null,
-        "is_readonly": false,
-        "created": "2015-02-17T22:06:38.059Z"
-    },
-    "model": "core.user",
-    "pk": 1
-},
-{
-    "fields": {
         "updated": "2015-02-17T22:06:39.361Z",
         "membershipFee": 0,
         "policed": null,
@@ -226,162 +18,5 @@
     },
     "model": "core.serviceclass",
     "pk": 1
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:38.236Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:38.095Z",
-        "deleted": false,
-        "deployments": [
-            1
-        ],
-        "description": null,
-        "name": "m1.small",
-        "backend_register": "{}",
-        "default": false,
-        "flavor": "m1.small",
-        "backend_status": "0 - Provisioning in progress",
-        "order": 0,
-        "enacted": null
-    },
-    "model": "core.flavor",
-    "pk": 1
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:38.394Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:38.287Z",
-        "deleted": false,
-        "deployments": [
-            1
-        ],
-        "description": null,
-        "name": "m1.medium",
-        "backend_register": "{}",
-        "default": false,
-        "flavor": "m1.medium",
-        "backend_status": "0 - Provisioning in progress",
-        "order": 0,
-        "enacted": null
-    },
-    "model": "core.flavor",
-    "pk": 2
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:38.561Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:38.445Z",
-        "deleted": false,
-        "deployments": [
-            1
-        ],
-        "description": null,
-        "name": "m1.large",
-        "backend_register": "{}",
-        "default": false,
-        "flavor": "m1.large",
-        "backend_status": "0 - Provisioning in progress",
-        "order": 0,
-        "enacted": null
-    },
-    "model": "core.flavor",
-    "pk": 3
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:38.778Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:38.778Z",
-        "deleted": false,
-        "backend_register": "{}",
-        "role": "admin",
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.slicerole",
-    "pk": 1
-},
-{
-    "fields": {
-        "updated": "2015-02-17T22:06:38.836Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:38.836Z",
-        "deleted": false,
-        "backend_register": "{}",
-        "role": "access",
-        "backend_status": "0 - Provisioning in progress",
-        "enacted": null
-    },
-    "model": "core.slicerole",
-    "pk": 2
-},
-{
-    "fields": {
-        "shared_network_id": null,
-        "updated": "2015-02-17T22:06:39.419Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:39.419Z",
-        "deleted": false,
-        "description": "A private virtual network",
-        "visibility": "private",
-        "name": "Private",
-        "backend_register": "{}",
-        "topology_kind": "bigswitch",
-        "guaranteed_bandwidth": 0,
-        "translation": "none",
-        "backend_status": "0 - Provisioning in progress",
-        "shared_network_name": null,
-        "controller_kind": null,
-        "enacted": null
-    },
-    "model": "core.networktemplate",
-    "pk": 1
-},
-{
-    "fields": {
-        "shared_network_id": null,
-        "updated": "2015-02-17T22:06:39.477Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:39.477Z",
-        "deleted": false,
-        "description": "Connect a instance to the public network",
-        "visibility": "private",
-        "name": "Public shared IPv4",
-        "backend_register": "{}",
-        "topology_kind": "bigswitch",
-        "guaranteed_bandwidth": 0,
-        "translation": "NAT",
-        "backend_status": "0 - Provisioning in progress",
-        "shared_network_name": "nat-net",
-        "controller_kind": null,
-        "enacted": null
-    },
-    "model": "core.networktemplate",
-    "pk": 2
-},
-{
-    "fields": {
-        "shared_network_id": null,
-        "updated": "2015-02-17T22:06:39.536Z",
-        "policed": null,
-        "created": "2015-02-17T22:06:39.536Z",
-        "deleted": false,
-        "description": "Connect a instance to the public network",
-        "visibility": "public",
-        "name": "Public dedicated IPv4",
-        "backend_register": "{}",
-        "topology_kind": "bigswitch",
-        "guaranteed_bandwidth": 0,
-        "translation": "none",
-        "backend_status": "0 - Provisioning in progress",
-        "shared_network_name": "ext-net",
-        "controller_kind": null,
-        "enacted": null
-    },
-    "model": "core.networktemplate",
-    "pk": 3
 }
 ]
diff --git a/xos/core/migrations/0001_initial.py b/xos/core/migrations/0001_initial.py
index b2e5d00..c55a8bf 100644
--- a/xos/core/migrations/0001_initial.py
+++ b/xos/core/migrations/0001_initial.py
@@ -1628,6 +1628,52 @@
             },
             bases=(models.Model,),
         ),
+        migrations.CreateModel(
+            name='TenantPrivilege',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID',
+                                        serialize=False, auto_created=True, primary_key=True)),
+                ('created', models.DateTimeField(
+                    default=django.utils.timezone.now, auto_now_add=True)),
+                ('updated', models.DateTimeField(
+                    default=django.utils.timezone.now, auto_now=True)),
+                ('enacted', models.DateTimeField(
+                    default=None, null=True, blank=True)),
+                ('policed', models.DateTimeField(
+                    default=None, null=True, blank=True)),
+                ('backend_status', models.CharField(
+                    default=b'Provisioning in progress', max_length=140)),
+                ('deleted', models.BooleanField(default=False)),
+            ],
+            options={
+                'abstract': False,
+            },
+            bases=(models.Model,),
+        ),
+        migrations.CreateModel(
+            name='TenantRole',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID',
+                                        serialize=False, auto_created=True, primary_key=True)),
+                ('created', models.DateTimeField(
+                    default=django.utils.timezone.now, auto_now_add=True)),
+                ('updated', models.DateTimeField(
+                    default=django.utils.timezone.now, auto_now=True)),
+                ('enacted', models.DateTimeField(
+                    default=None, null=True, blank=True)),
+                ('policed', models.DateTimeField(
+                    default=None, null=True, blank=True)),
+                ('backend_status', models.CharField(
+                    default=b'Provisioning in progress', max_length=140)),
+                ('deleted', models.BooleanField(default=False)),
+                ('role', models.CharField(unique=True, max_length=30,
+                                          choices=[(b'admin', b'Admin'), (b'access', b'Access')])),
+            ],
+            options={
+                'abstract': False,
+            },
+            bases=(models.Model,),
+        ),
         migrations.AddField(
             model_name='sliceprivilege',
             name='role',
diff --git a/xos/core/models/__init__.py b/xos/core/models/__init__.py
index 6fad0f1..5b0ad4b 100644
--- a/xos/core/models/__init__.py
+++ b/xos/core/models/__init__.py
@@ -1,7 +1,7 @@
 from .plcorebase import PlCoreBase,PlCoreBaseManager,PlCoreBaseDeletionManager,PlModelMixIn
 from .project import Project
 from .singletonmodel import SingletonModel
-from .service import Service, Tenant, TenantWithContainer, CoarseTenant, ServicePrivilege, TenantRoot, TenantRootPrivilege, TenantRootRole, Subscriber, Provider
+from .service import Service, Tenant, TenantWithContainer, CoarseTenant, ServicePrivilege, TenantRoot, TenantRootPrivilege, TenantRootRole, TenantPrivilege, TenantRole, Subscriber, Provider
 from .service import ServiceAttribute, TenantAttribute, ServiceRole
 from .tag import Tag
 from .role import Role
diff --git a/xos/core/models/plcorebase.py b/xos/core/models/plcorebase.py
index c8c25a7..060570d 100644
--- a/xos/core/models/plcorebase.py
+++ b/xos/core/models/plcorebase.py
@@ -2,6 +2,7 @@
 import json
 import os
 import sys
+import threading
 from django import db
 from django.db import models
 from django.forms.models import model_to_dict
@@ -198,7 +199,7 @@
     # default values for created and updated are only there to keep evolution
     # from failing.
     created = models.DateTimeField(auto_now_add=True, default=timezone.now)
-    updated = models.DateTimeField(auto_now=True, default=timezone.now)
+    updated = models.DateTimeField(default=timezone.now)
     enacted = models.DateTimeField(null=True, blank=True, default=None)
     policed = models.DateTimeField(null=True, blank=True, default=None)
 
@@ -270,6 +271,9 @@
                 if not (field in ["backend_register", "backend_status", "deleted", "enacted", "updated"]):
                     ignore_composite_key_check=False
 
+        if 'synchronizer' not in threading.current_thread().name:
+            self.updated = datetime.datetime.now()
+
         super(PlCoreBase, self).save(*args, **kwargs)
 
         # This is a no-op if observer_disabled is set
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 641c5ae..1c19e56 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -892,3 +892,52 @@
                     [trp.id for trp in cls.objects.filter(tenant_root=priv.tenant_root)])
 
             return cls.objects.filter(id__in=trp_ids)
+
+
+class TenantRole(PlCoreBase):
+    """A TenantRole option."""
+    ROLE_CHOICES = (('admin', 'Admin'), ('access', 'Access'))
+    role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
+
+    def __unicode__(self): return u'%s' % (self.role)
+
+
+class TenantPrivilege(PlCoreBase):
+    """"A TenantPrivilege which defines how users can access a particular Tenant.
+
+    Attributes:
+        id (models.AutoField): The ID of the privilege.
+        user (models.ForeignKey): A Foreign Key to the a User.
+        tenant (models.ForeignKey): A ForeignKey to the Tenant.
+        role (models.ForeignKey): A ForeignKey to the TenantRole.
+    """
+    id = models.AutoField(primary_key=True)
+    user = models.ForeignKey('User', related_name="tenantprivileges")
+    tenant = models.ForeignKey('Tenant', related_name="tenantprivileges")
+    role = models.ForeignKey('TenantRole', related_name="tenantprivileges")
+
+    def __unicode__(self): return u'%s %s %s' % (
+        self.tenant, self.user, self.role)
+
+    def save(self, *args, **kwds):
+        if not self.user.is_active:
+            raise PermissionDenied, "Cannot modify role(s) of a disabled user"
+        super(TenantPrivilege, self).save(*args, **kwds)
+
+    def can_update(self, user):
+        return user.can_update_tenant_privilege(self)
+
+    @classmethod
+    def select_by_user(cls, user):
+        if user.is_admin:
+            return cls.objects.all()
+        else:
+            # User can see his own privilege
+            trp_ids = [trp.id for trp in cls.objects.filter(user=user)]
+
+            # A tenant admin can see the TenantPrivileges for their Tenants
+            for priv in cls.objects.filter(user=user, role__role="admin"):
+                trp_ids.extend(
+                    [trp.id for trp in cls.objects.filter(tenant=priv.tenant)])
+
+            return cls.objects.filter(id__in=trp_ids)
diff --git a/xos/core/models/user.py b/xos/core/models/user.py
index f1d73d2..715c670 100644
--- a/xos/core/models/user.py
+++ b/xos/core/models/user.py
@@ -356,9 +356,21 @@
             return True
         return False
 
+    def can_update_tenant(self, tenant, allow=[]):
+        from core.models.service import Tenant, TenantPrivilege
+        if self.can_update_root():
+            return True
+        if TenantPrivilege.objects.filter(
+                tenant=tenant, user=self, role__role__in=['admin', 'Admin'] + allow):
+            return True
+        return False
+
     def can_update_tenant_root_privilege(self, tenant_root_privilege, allow=[]):
         return self.can_update_tenant_root(tenant_root_privilege.tenant_root, allow)
 
+    def can_update_tenant_privilege(self, tenant_privilege, allow=[]):
+        return self.can_update_tenant(tenant_privilege.tenant, allow)
+
     def get_readable_objects(self, filter_by=None):
         """ Returns a list of objects that the user is allowed to read. """
         from core.models import Deployment, Flavor, Image, Network, NetworkTemplate, Node, PlModelMixIn, Site, Slice, SliceTag, Instance, Tag, User, DeploymentPrivilege, SitePrivilege, SlicePrivilege
diff --git a/xos/core/xoslib/dashboards/helloworld.html b/xos/core/xoslib/dashboards/helloworld.html
deleted file mode 100644
index 91dde39..0000000
--- a/xos/core/xoslib/dashboards/helloworld.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<!-- /opt/xos/templates/admin/dashboard/helloworld.html -->
-<div>Hello, {{ user.firstname }} {{ user.lastname }}.</div>
-<div>This is the hello world view. The value of foobar is {{ foobar }}.</div>
-<div id="dynamicTableOfInterestingThings"></div>
-<p>Type a new description for the the first slice in the collection:</p>
-<input type="text" name="newDescription" id="newDescription">
-<input type="button" name="submitNewDescription" value="submit new description"  id="submitNewDescription">
-
-<script src="{{ STATIC_URL }}/js/vendor/underscore-min.js"></script>
-<script src="{{ STATIC_URL }}/js/vendor/backbone.js"></script>
-<script src="{{ STATIC_URL }}/js/xoslib/xos-defaults.js"></script>
-<script src="{{ STATIC_URL }}/js/xoslib/xos-backbone.js"></script>
-
-<script src="{{ STATIC_URL }}/js/helloworld.js"></script>
diff --git a/xos/core/xoslib/dashboards/xosOpenVPNDashboard.html b/xos/core/xoslib/dashboards/xosOpenVPNDashboard.html
new file mode 100644
index 0000000..eb1c9c6
--- /dev/null
+++ b/xos/core/xoslib/dashboards/xosOpenVPNDashboard.html
@@ -0,0 +1,14 @@
+<!-- browserSync -->
+
+<!-- inject:css -->
+<link rel="stylesheet" href="/static/css/xosOpenVPNDashboard.css">
+<!-- endinject -->
+
+<div id="xosOpenVPNDashboard">
+    <div ui-view></div>
+</div>
+
+
+<!-- inject:js -->
+<script src="/static/js/xosOpenVPNDashboard.js"></script>
+<!-- endinject -->
diff --git a/xos/core/xoslib/methods/openvpnview.py b/xos/core/xoslib/methods/openvpnview.py
new file mode 100644
index 0000000..d8cf39e
--- /dev/null
+++ b/xos/core/xoslib/methods/openvpnview.py
@@ -0,0 +1,84 @@
+import jinja2
+from core.models import TenantPrivilege
+from plus import PlusSerializerMixin
+from rest_framework import serializers
+from services.openvpn.models import OpenVPNService, OpenVPNTenant
+from xos.apibase import XOSListCreateAPIView
+
+if hasattr(serializers, "ReadOnlyField"):
+    # rest_framework 3.x
+    ReadOnlyField = serializers.ReadOnlyField
+else:
+    # rest_framework 2.x
+    ReadOnlyField = serializers.Field
+
+
+def get_default_openvpn_service():
+    openvpn_services = OpenVPNService.get_service_objects().all()
+    if openvpn_services:
+        return openvpn_services[0].id
+    return None
+
+
+class OpenVPNTenantSerializer(serializers.ModelSerializer, PlusSerializerMixin):
+    """A Serializer for the OpenVPNTenant that has the minimum information required for clients.
+
+    Attributes:
+        id (ReadOnlyField): The ID of OpenVPNTenant.
+        server_network (ReadOnlyField): The network of the VPN.
+        vpn_subnet (ReadOnlyField): The subnet of the VPN.
+        script_text (SerializerMethodField): The text of the script for the client to use to
+            connect.
+    """
+    id = ReadOnlyField()
+    server_network = ReadOnlyField()
+    vpn_subnet = ReadOnlyField()
+    script_text = serializers.SerializerMethodField()
+
+    class Meta:
+        model = OpenVPNTenant
+        fields = ('id', 'service_specific_attribute', 'vpn_subnet',
+                  'server_network', 'script_text')
+
+    def get_script_text(self, obj):
+        """Gets the text of the client script for the requesting user.
+
+        Parameters:
+            obj (services.openvpn.models.OpenVPNTenant): The OpenVPNTenant to connect to.
+
+        Returns:
+            str: The client script as a str.
+        """
+        env = jinja2.Environment(
+            loader=jinja2.FileSystemLoader("/opt/xos/services/openvpn/templates"))
+        template = env.get_template("connect.vpn.j2")
+        client_name = self.context['request'].user.email + "-" + str(obj.id)
+        remote_ids = list(obj.failover_server_ids)
+        remote_ids.insert(0, obj.id)
+        remotes = OpenVPNTenant.get_tenant_objects().filter(pk__in=remote_ids)
+        pki_dir = OpenVPNService.get_pki_dir(obj)
+        fields = {"client_name": client_name,
+                  "remotes": remotes,
+                  "is_persistent": obj.is_persistent,
+                  "ca_crt": obj.get_ca_crt(pki_dir),
+                  "client_crt": obj.get_client_cert(client_name, pki_dir),
+                  "client_key": obj.get_client_key(client_name, pki_dir)
+                 }
+        return template.render(fields)
+
+
+class OpenVPNTenantList(XOSListCreateAPIView):
+    """Class that provides a list of OpenVPNTenants that the user has permission to access."""
+    serializer_class = OpenVPNTenantSerializer
+    method_kind = "list"
+    method_name = "openvpntenant"
+
+    def get_queryset(self):
+        # Get every privilege for this user
+        tenants_privs = TenantPrivilege.objects.all().filter(
+            user=self.request.user)
+        vpn_tenants = []
+        for priv in tenants_privs:
+            vpn_tenants.append(
+                OpenVPNTenant.get_tenant_objects().filter(pk=priv.tenant.pk)[0])
+        return vpn_tenants
diff --git a/xos/core/xoslib/methods/vtn.py b/xos/core/xoslib/methods/vtn.py
index 1a2af53..c9dbf3a 100644
--- a/xos/core/xoslib/methods/vtn.py
+++ b/xos/core/xoslib/methods/vtn.py
@@ -57,7 +57,8 @@
         for xos_service in Service.objects.all():
             if service in xos_service.get_vtn_src_ids():
                 return Response(xos_service.get_vtn_dependencies_ids())
-        raise DoesNotExist()
+        return Response([])
+        # raise DoesNotExist()
 
     def list(self, request):
         raise Exception("Not Implemented")
diff --git a/xos/core/xoslib/static/css/xosOpenVPNDashboard.css b/xos/core/xoslib/static/css/xosOpenVPNDashboard.css
new file mode 100644
index 0000000..d9d966e
--- /dev/null
+++ b/xos/core/xoslib/static/css/xosOpenVPNDashboard.css
@@ -0,0 +1 @@
+#xosOpenVPNDashboard{width:70%;margin:auto}.vpn-row{display:table-row}.vpn-cell{display:table-cell;padding:5px}.vpn-header{font-weight:700}
\ No newline at end of file
diff --git a/xos/core/xoslib/static/js/helloworld.js b/xos/core/xoslib/static/js/helloworld.js
deleted file mode 100644
index 166d183..0000000
--- a/xos/core/xoslib/static/js/helloworld.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* eslint-disable guard-for-in, space-before-blocks */
-
-// helloworld.js
-function updateHelloWorldData() {
-  var html = '<table class="table table-bordered table-striped">';
-
-  for (var slicekey in xos.slices.models) {
-    var slice = xos.slices.models[slicekey];
-
-    html = html + '<tr><td>' + slice.get('name') + '</td><td>' + slice.get('description') + '</td></tr>';
-  }
-  html = html + '</table>';
-  $('#dynamicTableOfInterestingThings').html(html);
-}
-
-$(document).ready(function() {
-  xos.slices.on('change', function() {
-    updateHelloWorldData();
-  });
-  xos.slices.on('remove', function() {
-    updateHelloWorldData();
-  });
-  xos.slices.on('sort', function() {
-    updateHelloWorldData();
-  });
-
-  xos.slices.startPolling();
-});
-
-// helloworld.js
-$(document).ready(function() {
-  $('#submitNewDescription').bind('click', function() {
-    var newDescription = $('#newDescription').val();
-
-    xos.slices.models[0].set('description', newDescription);
-    xos.slices.models[0].save();
-  });
-});
diff --git a/xos/core/xoslib/static/js/xosOpenVPNDashboard.js b/xos/core/xoslib/static/js/xosOpenVPNDashboard.js
new file mode 100644
index 0000000..8723888
--- /dev/null
+++ b/xos/core/xoslib/static/js/xosOpenVPNDashboard.js
@@ -0,0 +1 @@
+"use strict";angular.module("xos.openVPNDashboard",["ngResource","ngCookies","ngLodash","ui.router","xos.helpers"]).config(["$stateProvider",function(n){n.state("openVPNList",{url:"/",template:"<vpn-list></vpn-list>"})}]).config(["$compileProvider",function(n){n.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|file|blob):/)}]).service("Vpn",["$http","$q",function(n,e){this.getOpenVpnTenants=function(){var t=e.defer();return n.get("/xoslib/openvpntenant/").then(function(n){t.resolve(n.data)})["catch"](function(n){t.reject(n)}),t.promise}}]).config(["$httpProvider",function(n){n.interceptors.push("NoHyperlinks")}]).directive("vpnList",function(){return{restrict:"E",scope:{},bindToController:!0,controllerAs:"vm",templateUrl:"templates/openvpn-list.tpl.html",controller:["Vpn",function(n){var e=this;n.getOpenVpnTenants().then(function(n){e.vpns=n;for(var t=0;t<e.vpns.length;t++){var i=new Blob([e.vpns[t].script_text],{type:"text/plain"});e.vpns[t].script_text=(window.URL||window.webkitURL).createObjectURL(i)}})["catch"](function(n){throw new Error(n)})}]}}),angular.module("xos.openVPNDashboard").run(["$templateCache",function(n){n.put("templates/openvpn-list.tpl.html",'<div style="display: table;">\n  <div class="vpn-row">\n    <h1 class="vpn-cell">VPN List</h1>\n  </div>\n  <div class="vpn-row">\n    <div class="vpn-cell vpn-header">ID</div>\n    <div class="vpn-cell vpn-header">VPN Network</div>\n    <div class="vpn-cell vpn-header">VPN Subnet</div>\n    <div class="vpn-cell vpn-header">Script Link</div>\n  </div>\n  <div class="vpn-row" ng-repeat="vpn in vm.vpns">\n    <div class="vpn-cell">{{ vpn.id }}</div>\n    <div class="vpn-cell">{{ vpn.server_network }}</div>\n    <div class="vpn-cell">{{ vpn.vpn_subnet }}</div>\n    <div class="vpn-cell">\n      <a download="connect-{{ vpn.id }}.vpn" ng-href="{{ vpn.script_text }}">Script</a>\n    </div>\n  </div>\n</div>\n')}]),angular.module("xos.openVPNDashboard").run(["$location",function(n){n.path("/")}]),angular.bootstrap(angular.element("#xosOpenVPNDashboard"),["xos.openVPNDashboard"]);
\ No newline at end of file
diff --git a/xos/services/helloworld/models.py b/xos/services/helloworld/models.py
deleted file mode 100644
index 9bb343e..0000000
--- a/xos/services/helloworld/models.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from django.db import models
-from core.models import User, Service, SingletonModel, PlCoreBase, Instance
-from core.models.plcorebase import StrippedCharField
-import os
-from django.db import models
-from django.forms.models import model_to_dict
-from django.db.models import Q
-
-
-# Create your models here.
-
-class Hello(PlCoreBase):
-    name = models.CharField(max_length=254,help_text="Salutation e.g. Hello or Bonjour")
-    instance_backref = models.ForeignKey(Instance,related_name="hellos")
-    
-class World(PlCoreBase):
-    name = models.CharField(max_length=254,help_text="Name of planet")
-    hello = models.ForeignKey(Hello) 
diff --git a/xos/services/helloworld/view.py b/xos/services/helloworld/view.py
deleted file mode 100644
index 6ad9ae1..0000000
--- a/xos/services/helloworld/view.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from django.http import HttpResponse
-from django.views.generic import TemplateView, View
-from django import template
-from core.models import *
-from services.helloworld.models import *
-import json
-import os
-import time
-import tempfile
-
-class HelloWorldView(TemplateView):
-    head_template = r"""{% extends "admin/dashboard/dashboard_base.html" %}
-       {% load admin_static %}
-       {% block content %}
-    """
-
-    tail_template = r"{% endblock %}"
-
-    def get(self, request, name="root", *args, **kwargs):
-        head_template = self.head_template
-        tail_template = self.tail_template
-
-        try:
-            hello_name = request.GET['hello_name']
-            world_name = request.GET['world_name']
-            instance_id_str = request.GET['instance_id']
-            instance_id = int(instance_id_str)
-
-            i = Instance.objects.get(pk=instance_id)
-            i.pk=None
-            i.userData=None
-            i.instance_id=None
-            i.instance_name=None
-            i.enacted=None
-            i.save()
-            h = Hello(name=hello_name,instance_backref=i)
-            h.save()
-            w = World(hello=h,name=world_name)
-            w.save()
-
-            t = template.Template(head_template + 'Done. New instance id: %r'%i.pk + self.tail_template)
-        except KeyError:
-            html = """<form>
-                Hello string: <input type="text" name="hello_name" placeholder="Planet"><br>
-                World string: <input type="text" name="world_name" placeholder="Earth"><br>
-                Id of instance to copy: <input type="text" name="instance_id" placeholder="3"><br>
-                <input type="submit" value="Submit">
-                  </form>"""
-
-            t = template.Template(head_template + html + self.tail_template)
-
-        response_kwargs = {}
-        response_kwargs.setdefault('content_type', self.content_type)
-        return self.response_class(
-            request = request,
-            template = t,
-            **response_kwargs)
diff --git a/xos/services/helloworldservice_complete/__init__.py b/xos/services/helloworldservice_complete/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/xos/services/helloworldservice_complete/__init__.py
+++ /dev/null
diff --git a/xos/services/helloworldservice_complete/admin.py b/xos/services/helloworldservice_complete/admin.py
deleted file mode 100644
index 3c8e793..0000000
--- a/xos/services/helloworldservice_complete/admin.py
+++ /dev/null
@@ -1,141 +0,0 @@
-
-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.helloworldservice_complete.models import HelloWorldServiceComplete, HelloWorldTenantComplete, HELLO_WORLD_KIND
-
-# The class to provide an admin interface on the web for the service.
-# We do only configuration here and don't change any logic because the logic
-# is taken care of for us by ReadOnlyAwareAdmin
-class HelloWorldServiceCompleteAdmin(ReadOnlyAwareAdmin):
-    # We must set the model so that the admin knows what fields to use
-    model = HelloWorldServiceComplete
-    verbose_name = "Hello World Service"
-    verbose_name_plural = "Hello World Services"
-
-    # Setting list_display creates columns on the admin page, each value here
-    # is a column, the column is populated for every instance of the model.
-    list_display = ("backend_status_icon", "name", "enabled")
-
-    # Used to indicate which values in the columns of the admin form are links.
-    list_display_links = ('backend_status_icon', 'name', )
-
-    # Denotes the sections of the form, the fields in the section, and the
-    # CSS classes used to style them. We represent this as a set of tuples, each
-    # tuple as a name (or None) and a set of fields and classes.
-    # Here the first section does not have a name so we use none. That first
-    # section has several fields indicated in the 'fields' attribute, and styled
-    # by the classes indicated in the 'classes' attribute. The classes given
-    # here are important for rendering the tabs on the form. To give the tabs
-    # we must assign the classes suit-tab and suit-tab-<name> where
-    # where <name> will be used later.
-    fieldsets = [(None, {'fields': ['backend_status_text', 'name', 'enabled',
-                                    'versionNumber', 'description', "view_url"],
-                         'classes':['suit-tab suit-tab-general']})]
-
-    # Denotes the fields that are readonly and cannot be changed.
-    readonly_fields = ('backend_status_text', )
-
-    # Inlines are used to denote other models that can be edited on the same
-    # form as this one. In this case the service form also allows changes
-    # to slices.
-    inlines = [SliceInline]
-
-    extracontext_registered_admins = True
-
-    # Denotes the fields that can be changed by an admin but not be all users
-    user_readonly_fields = ["name", "enabled", "versionNumber", "description"]
-
-    # Associates fieldsets from this form and from the inlines.
-    # The format here are tuples, of (<name>, tab title). <name> comes from the
-    # <name> in the fieldsets.
-    suit_form_tabs = (('general', 'Hello World Service Details'),
-                      ('administration', 'Tenants'),
-                      ('slices', 'Slices'),)
-
-    # Used to include a template for a tab. Here we include the
-    # helloworldserviceadmin template in the top position for the administration
-    # tab.
-    suit_form_includes = (('helloworldserviceadmin.html',
-                           'top',
-                           'administration'),)
-
-    # Used to get the objects for this model that are associated with the
-    # requesting user.
-    def queryset(self, request):
-        return HelloWorldServiceComplete.get_service_objects_by_user(request.user)
-
-# Class to represent the form to add and edit tenants.
-# We need to define this instead of just using an admin like we did for the
-# service because tenants vary more than services and there isn't a common form.
-# This allows us to change the python behavior for the admin form to save extra
-# fields and control defaults.
-class HelloWorldTenantCompleteForm(forms.ModelForm):
-    # Defines a field for the creator of this service. It is a dropdown which
-    # is populated with all of the users.
-    creator = forms.ModelChoiceField(queryset=User.objects.all())
-    # Defines a text field for the display message, it is not required.
-    display_message = forms.CharField(required=False)
-
-    def __init__(self, *args, **kwargs):
-        super(HelloWorldTenantCompleteForm, self).__init__(*args, **kwargs)
-        # Set the kind field to readonly
-        self.fields['kind'].widget.attrs['readonly'] = True
-        # Define the logic for obtaining the objects for the provider_service
-        # dropdown of the tenant form.
-        self.fields[
-            'provider_service'].queryset = HelloWorldServiceComplete.get_service_objects().all()
-        # Set the initial kind to HELLO_WORLD_KIND for this tenant.
-        self.fields['kind'].initial = HELLO_WORLD_KIND
-        # If there is an instance of this model then we can set the initial
-        # form values to the existing values.
-        if self.instance:
-            self.fields['creator'].initial = self.instance.creator
-            self.fields[
-                'display_message'].initial = self.instance.display_message
-
-        # If there is not an instance then we need to set initial values.
-        if (not self.instance) or (not self.instance.pk):
-            self.fields['creator'].initial = get_request().user
-            if HelloWorldServiceComplete.get_service_objects().exists():
-                self.fields["provider_service"].initial = HelloWorldServiceComplete.get_service_objects().all()[0]
-
-    # This function describes what happens when the save button is pressed on
-    # the tenant form. In this case we set the values for the instance that were
-    # entered.
-    def save(self, commit=True):
-        self.instance.creator = self.cleaned_data.get("creator")
-        self.instance.display_message = self.cleaned_data.get(
-            "display_message")
-        return super(HelloWorldTenantCompleteForm, self).save(commit=commit)
-
-    class Meta:
-        model = HelloWorldTenantComplete
-
-# Define the admin form for the tenant. This uses a similar structure as the
-# service but uses HelloWorldTenantCompleteForm to change the python behavior.
-
-
-class HelloWorldTenantCompleteAdmin(ReadOnlyAwareAdmin):
-    verbose_name = "Hello World Tenant"
-    verbose_name_plural = "Hello World Tenants"
-    list_display = ('id', 'backend_status_icon', 'instance', 'display_message')
-    list_display_links = ('backend_status_icon', 'instance', 'display_message',
-                          'id')
-    fieldsets = [(None, {'fields': ['backend_status_text', 'kind',
-                                    'provider_service', 'instance', 'creator',
-                                    'display_message'],
-                         'classes': ['suit-tab suit-tab-general']})]
-    readonly_fields = ('backend_status_text', 'instance',)
-    form = HelloWorldTenantCompleteForm
-
-    suit_form_tabs = (('general', 'Details'),)
-
-    def queryset(self, request):
-        return HelloWorldTenantComplete.get_tenant_objects_by_user(request.user)
-
-# Associate the admin forms with the models.
-admin.site.register(HelloWorldServiceComplete, HelloWorldServiceCompleteAdmin)
-admin.site.register(HelloWorldTenantComplete, HelloWorldTenantCompleteAdmin)
diff --git a/xos/services/helloworldservice_complete/models.py b/xos/services/helloworldservice_complete/models.py
deleted file mode 100644
index 8a4ce59..0000000
--- a/xos/services/helloworldservice_complete/models.py
+++ /dev/null
@@ -1,113 +0,0 @@
-from core.models import Service, TenantWithContainer
-from django.db import transaction
-
-HELLO_WORLD_KIND = "helloworldservice_complete"
-
-# The class to represent the service. Most of the service logic is given for us
-# in the Service class but, we have some configuration that is specific for
-# this example.
-class HelloWorldServiceComplete(Service):
-    KIND = HELLO_WORLD_KIND
-
-    class Meta:
-        # When the proxy field is set to True the model is represented as
-        # it's superclass in the database, but we can still change the python
-        # behavior. In this case HelloWorldServiceComplete is a Service in the
-        # database.
-        proxy = True
-        # The name used to find this service, all directories are named this
-        app_label = "helloworldservice_complete"
-        verbose_name = "Hello World Service"
-
-# This is the class to represent the tenant. Most of the logic is given to use
-# in TenantWithContainer, however there is some configuration and logic that
-# we need to define for this example.
-class HelloWorldTenantComplete(TenantWithContainer):
-
-    class Meta:
-        # Same as a above, HelloWorldTenantComplete is represented as a
-        # TenantWithContainer, but we change the python behavior.
-        proxy = True
-        verbose_name = "Hello World Tenant"
-
-    # The kind of the service is used on forms to differentiate this service
-    # from the other services.
-    KIND = HELLO_WORLD_KIND
-
-    # Ansible requires that the sync_attributes field contain nat_ip and nat_mac
-    # these will be used to determine where to SSH to for ansible.
-    # Getters must be defined for every attribute specified here.
-    sync_attributes = ("nat_ip", "nat_mac",)
-
-    # default_attributes is used cleanly indicate what the default values for
-    # the fields are.
-    default_attributes = {'display_message': 'Hello World!'}
-
-    def __init__(self, *args, **kwargs):
-        helloworld_services = HelloWorldServiceComplete.get_service_objects().all()
-        # When the tenant is created the default service in the form is set
-        # to be the first created HelloWorldServiceComplete
-        if helloworld_services:
-            self._meta.get_field(
-                "provider_service").default = helloworld_services[0].id
-        super(HelloWorldTenantComplete, self).__init__(*args, **kwargs)
-
-    def save(self, *args, **kwargs):
-        super(HelloWorldTenantComplete, self).save(*args, **kwargs)
-        # This call needs to happen so that an instance is created for this
-        # tenant is created in the slice. One instance is created per tenant.
-        model_policy_helloworld_tenant(self.pk)
-
-    def delete(self, *args, **kwargs):
-        # Delete the instance that was created for this tenant
-        self.cleanup_container()
-        super(HelloWorldTenantComplete, self).delete(*args, **kwargs)
-
-    # Getter for the message that will appear on the webpage
-    # By default it is "Hello World!"
-    @property
-    def display_message(self):
-        return self.get_attribute(
-            "display_message",
-            self.default_attributes['display_message'])
-
-    # Setter for the message that will appear on the webpage
-    @display_message.setter
-    def display_message(self, value):
-        self.set_attribute("display_message", value)
-
-    @property
-    def addresses(self):
-        if (not self.id) or (not self.instance):
-            return {}
-
-        addresses = {}
-        # The ports field refers to networks for the instance.
-        # This loop stores the details for the NAT network that will be
-        # necessary for ansible.
-        for ns in self.instance.ports.all():
-            if "nat" in ns.network.name.lower():
-                addresses["nat"] = (ns.ip, ns.mac)
-        return addresses
-
-    # This getter is necessary because nat_ip is a sync_attribute
-    @property
-    def nat_ip(self):
-        return self.addresses.get("nat", (None, None))[0]
-
-    # This getter is necessary because nat_mac is a sync_attribute
-    @property
-    def nat_mac(self):
-        return self.addresses.get("nat", (None, None))[1]
-
-
-def model_policy_helloworld_tenant(pk):
-    # This section of code is atomic to prevent race conditions
-    with transaction.atomic():
-        # We find all of the tenants that are waiting to update
-        tenant = HelloWorldTenantComplete.objects.select_for_update().filter(pk=pk)
-        if not tenant:
-            return
-        # Since this code is atomic it is safe to always use the first tenant
-        tenant = tenant[0]
-        tenant.manage_container()
diff --git a/xos/services/helloworldservice_complete/templates/helloworldserviceadmin.html b/xos/services/helloworldservice_complete/templates/helloworldserviceadmin.html
deleted file mode 100644
index ba418ee..0000000
--- a/xos/services/helloworldservice_complete/templates/helloworldserviceadmin.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!-- Template used to for the button leading to the HelloWorldTenantComplete form. -->
-<div class = "left-nav">
-  <ul>
-    <li>
-      <a href="/admin/helloworldservice_complete/helloworldtenantcomplete/">
-        Hello World Tenants
-      </a>
-    </li>
-  </ul>
-</div>
diff --git a/xos/services/helloworld/__init__.py b/xos/services/openvpn/__init__.py
similarity index 100%
rename from xos/services/helloworld/__init__.py
rename to xos/services/openvpn/__init__.py
diff --git a/xos/services/openvpn/admin.py b/xos/services/openvpn/admin.py
new file mode 100644
index 0000000..28e778d
--- /dev/null
+++ b/xos/services/openvpn/admin.py
@@ -0,0 +1,229 @@
+from django import forms
+from django.contrib import admin
+
+from core.admin import ReadOnlyAwareAdmin, SliceInline, TenantPrivilegeInline
+from core.middleware import get_request
+from core.models import User
+from services.openvpn.models import OPENVPN_KIND, OpenVPNService, OpenVPNTenant
+from xos.exceptions import XOSValidationError
+
+
+class OpenVPNServiceForm(forms.ModelForm):
+
+    exposed_ports = forms.CharField(required=True)
+
+    def __init__(self, *args, **kwargs):
+        super(OpenVPNServiceForm, self).__init__(*args, **kwargs)
+
+        if self.instance:
+            self.fields['exposed_ports'].initial = (
+                self.instance.exposed_ports_str)
+
+    def save(self, commit=True):
+        self.instance.exposed_ports = self.cleaned_data['exposed_ports']
+        return super(OpenVPNServiceForm, self).save(commit=commit)
+
+    def clean_exposed_ports(self):
+        exposed_ports = self.cleaned_data['exposed_ports']
+        self.instance.exposed_ports_str = exposed_ports
+        port_mapping = {"udp": [], "tcp": []}
+        parts = exposed_ports.split(",")
+        for part in parts:
+            part = part.strip()
+            if "/" in part:
+                (protocol, ports) = part.split("/", 1)
+            elif " " in part:
+                (protocol, ports) = part.split(None, 1)
+            else:
+                raise XOSValidationError(
+                    'malformed port specifier %s, format example: ' +
+                    '"tcp 123, tcp 201:206, udp 333"' % part)
+
+            protocol = protocol.strip()
+            ports = ports.strip()
+
+            if not (protocol in ["udp", "tcp"]):
+                raise XOSValidationError('unknown protocol %s' % protocol)
+
+            if "-" in ports:
+                port_mapping[protocol].extend(
+                    self.parse_port_range(ports, "-"))
+            elif ":" in ports:
+                port_mapping[protocol].extend(
+                    self.parse_port_range(ports, ":"))
+            else:
+                port_mapping[protocol].append(int(ports))
+
+        return port_mapping
+
+    def parse_port_range(self, port_str, split_str):
+        (first, last) = port_str.split(split_str)
+        first = int(first.strip())
+        last = int(last.strip())
+        return list(range(first, last))
+
+    class Meta:
+        model = OpenVPNService
+
+
+class OpenVPNServiceAdmin(ReadOnlyAwareAdmin):
+    """Defines the admin for the OpenVPNService."""
+    model = OpenVPNService
+    form = OpenVPNServiceForm
+    verbose_name = "OpenVPN 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",
+                                    'exposed_ports'],
+                         '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'),
+                      ('slices', 'Slices'),)
+
+    def queryset(self, request):
+        return OpenVPNService.get_service_objects_by_user(request.user)
+
+
+class OpenVPNTenantForm(forms.ModelForm):
+    """The form used to create and edit a OpenVPNTenant.
+
+    Attributes:
+        creator (forms.ModelChoiceField): The XOS user that created this
+            tenant.
+        server_network (forms.GenericIPAddressField): The IP address of the VPN network.
+        vpn_subnet (forms.GenericIPAddressField): The subnet used by the VPN network.
+        is_persistent (forms.BooleanField): Determines if this Tenant keeps
+            this connection alive through failures.
+        clients_can_see_each_other (forms.BooleanField): Determines if the clients on the VPN can
+            communicate with each other.
+        failover_servers (forms.ModelMultipleChoiceField): The other OpenVPNTenants to use as failover
+            servers.
+        protocol (forms.ChoiceField): The protocol to use.
+        use_ca_from (forms.ModelChoiceField): Another OpenVPNTenant to use the CA of, this is a very
+            hacky way to let VPNs have the same clients.
+    """
+    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)
+    failover_servers = forms.ModelMultipleChoiceField(
+        required=False, queryset=OpenVPNTenant.get_tenant_objects())
+    protocol = forms.ChoiceField(required=True, choices=[
+        ("tcp", "tcp"), ("udp", "udp")])
+    use_ca_from = forms.ModelChoiceField(
+        queryset=OpenVPNTenant.get_tenant_objects(), required=False)
+
+    def __init__(self, *args, **kwargs):
+        super(OpenVPNTenantForm, self).__init__(*args, **kwargs)
+        self.fields['kind'].widget.attrs['readonly'] = True
+        self.fields['failover_servers'].widget.attrs['rows'] = 300
+        self.fields[
+            'provider_service'].queryset = (
+                OpenVPNService.get_service_objects().all())
+
+        self.fields['kind'].initial = OPENVPN_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
+            self.initial['protocol'] = self.instance.protocol
+            self.fields['failover_servers'].queryset = (
+                OpenVPNTenant.get_tenant_objects().exclude(pk=self.instance.pk))
+            self.initial['failover_servers'] = OpenVPNTenant.get_tenant_objects().filter(
+                pk__in=self.instance.failover_server_ids)
+            self.fields['use_ca_from'].queryset = (
+                OpenVPNTenant.get_tenant_objects().exclude(pk=self.instance.pk))
+            if (self.instance.use_ca_from_id):
+                self.initial['use_ca_from'] = (
+                    OpenVPNTenant.get_tenant_objects().filter(pk=self.instance.use_ca_from_id)[0])
+
+        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
+            self.fields['failover_servers'].queryset = (
+                OpenVPNTenant.get_tenant_objects())
+            if OpenVPNService.get_service_objects().exists():
+                self.fields["provider_service"].initial = (
+                    OpenVPNService.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')
+
+        self.instance.failover_server_ids = [
+            tenant.id for tenant in self.cleaned_data.get('failover_servers')]
+
+        # Do not aquire a new port number if the protocol hasn't changed
+        if ((not self.instance.protocol) or
+                (self.instance.protocol != self.cleaned_data.get("protocol"))):
+            self.instance.protocol = self.cleaned_data.get("protocol")
+            self.instance.port_number = (
+                self.instance.provider_service.get_next_available_port(
+                    self.instance.protocol))
+
+        if (self.cleaned_data.get('use_ca_from')):
+            self.instance.use_ca_from_id = self.cleaned_data.get(
+                'use_ca_from').id
+        else:
+            self.instance.use_ca_from_id = None
+
+        return super(OpenVPNTenantForm, self).save(commit=commit)
+
+    class Meta:
+        model = OpenVPNTenant
+
+
+class OpenVPNTenantAdmin(ReadOnlyAwareAdmin):
+    verbose_name = "OpenVPN 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', 'use_ca_from',
+                                    'clients_can_see_each_other',
+                                    'failover_servers', "protocol"],
+                         'classes': ['suit-tab suit-tab-general']})]
+    readonly_fields = ('backend_status_text', 'instance')
+    form = OpenVPNTenantForm
+    inlines = [TenantPrivilegeInline]
+
+    suit_form_tabs = (('general', 'Details'),
+                      ('tenantprivileges', 'Privileges'))
+
+    def queryset(self, request):
+        return OpenVPNTenant.get_tenant_objects_by_user(request.user)
+
+
+# Associate the admin forms with the models.
+admin.site.register(OpenVPNService, OpenVPNServiceAdmin)
+admin.site.register(OpenVPNTenant, OpenVPNTenantAdmin)
diff --git a/xos/services/openvpn/models.py b/xos/services/openvpn/models.py
new file mode 100644
index 0000000..8aaa825
--- /dev/null
+++ b/xos/services/openvpn/models.py
@@ -0,0 +1,316 @@
+from subprocess import PIPE, Popen
+
+from django.db import transaction
+
+from core.models import Service, TenantWithContainer
+from xos.exceptions import XOSConfigurationError, XOSValidationError
+
+OPENVPN_KIND = "openvpn"
+
+
+class OpenVPNService(Service):
+    """Defines the Service for creating VPN servers."""
+    KIND = OPENVPN_KIND
+    OPENVPN_PREFIX = "/opt/openvpn/"
+    """The location of the openvpn EASY RSA files and PKIs."""
+    SERVER_PREFIX = OPENVPN_PREFIX + "server-"
+    """The prefix for server PKIs."""
+    VARS = OPENVPN_PREFIX + "vars"
+    """The location of the vars file with information for using EASY RSA."""
+    EASYRSA_LOC = OPENVPN_PREFIX + "easyrsa3/easyrsa"
+    """The location of the EASY RSA binary."""
+    EASYRSA_COMMAND_PREFIX = EASYRSA_LOC + " --vars=" + VARS
+    """Prefix for EASY RSA commands."""
+
+    @classmethod
+    def execute_easyrsa_command(cls, pki_dir, command):
+        """Executes the given EASY RSA command using the given PKI.
+
+        Parameters:
+            pki_dir (str): The directory for the pki to execute the command on.
+            command (str): The command to execute using ESAY RSA.
+        """
+        full_command = (
+            OpenVPNService.EASYRSA_COMMAND_PREFIX + " --pki-dir=" +
+            pki_dir + " " + command)
+        proc = Popen(
+            full_command, shell=True, stdout=PIPE, stderr=PIPE
+        )
+        (stdout, stderr) = proc.communicate()
+        if (proc.returncode != 0):
+            raise XOSConfigurationError(
+                full_command + " failed with standard out:" + str(stdout) +
+                " and stderr: " + str(stderr))
+
+    @classmethod
+    def get_pki_dir(cls, tenant):
+        """Gets the directory of the PKI for the given tenant.
+
+        Parameters:
+            tenant (services.openvpn.models.OpenVPNTenant): The tenant to get the PKI directory for.
+
+        Returns:
+            str: The pki directory for the tenant.
+        """
+        return OpenVPNService.SERVER_PREFIX + str(tenant.id)
+
+    class Meta:
+        proxy = True
+        # The name used to find this service, all directories are named this
+        app_label = "openvpn"
+        verbose_name = "OpenVPN Service"
+
+    default_attributes = {'exposed_ports': None,
+                          'exposed_ports_str': None}
+
+    @property
+    def exposed_ports(self):
+        """Mapping[str, list(str)]: maps protocols to a list of ports for that protocol."""
+        return self.get_attribute("exposed_ports",
+                                  self.default_attributes["exposed_ports"])
+
+    @exposed_ports.setter
+    def exposed_ports(self, value):
+        self.set_attribute("exposed_ports", value)
+
+    @property
+    def exposed_ports_str(self):
+        """str: a raw str representing the exposed ports."""
+        return self.get_attribute("exposed_ports_str",
+                                  self.default_attributes["exposed_ports_str"])
+
+    @exposed_ports_str.setter
+    def exposed_ports_str(self, value):
+        self.set_attribute("exposed_ports_str", value)
+
+    def get_next_available_port(self, protocol):
+        """Gets the next free port for the given protocol.
+
+        Parameters:
+            protocol (str): The protocol to get a port for, must be tcp or udp.
+
+        Returns:
+            int: a port number.
+
+        Raises:
+            xos.exceptions.XOSValidationError: If there the protocol is not udp or tcp.
+            xos.exceptions.XOSValidationError: If there are no available ports for the protocol.
+        """
+        if protocol != "udp" and protocol != "tcp":
+            raise XOSValidationError("Port protocol must be udp or tcp")
+        if not self.exposed_ports[protocol]:
+            raise XOSValidationError(
+                "No availble ports for protocol: " + protocol)
+        tenants = [
+            tenant for tenant in OpenVPNTenant.get_tenant_objects().all()
+            if tenant.protocol == protocol]
+        port_numbers = self.exposed_ports[protocol]
+        for port_number in port_numbers:
+            if (
+                len([
+                    tenant for tenant in tenants
+                    if tenant.port_number == port_number]) == 0):
+                return port_number
+
+
+class OpenVPNTenant(TenantWithContainer):
+    """Defines the Tenant for creating VPN servers."""
+
+    class Meta:
+        proxy = True
+        verbose_name = "OpenVPN Tenant"
+
+    KIND = OPENVPN_KIND
+
+    sync_attributes = ("nat_ip", "nat_mac",)
+
+    default_attributes = {'vpn_subnet': None,
+                          'server_network': None,
+                          'clients_can_see_each_other': True,
+                          'is_persistent': True,
+                          'port': None,
+                          'use_ca_from_id': None,
+                          'failover_server_ids': list(),
+                          'protocol': None}
+
+    def __init__(self, *args, **kwargs):
+        vpn_services = OpenVPNService.get_service_objects().all()
+        if vpn_services:
+            self._meta.get_field(
+                "provider_service").default = vpn_services[0].id
+        super(OpenVPNTenant, self).__init__(*args, **kwargs)
+
+    def save(self, *args, **kwargs):
+        super(OpenVPNTenant, self).save(*args, **kwargs)
+        model_policy_vpn_tenant(self.pk)
+
+    def delete(self, *args, **kwargs):
+        self.cleanup_container()
+        super(OpenVPNTenant, self).delete(*args, **kwargs)
+
+    @property
+    def protocol(self):
+        """str: The protocol that this tenant is listening on."""
+        return self.get_attribute(
+            "protocol", self.default_attributes["protocol"])
+
+    @protocol.setter
+    def protocol(self, value):
+        self.set_attribute("protocol", value)
+
+    @property
+    def use_ca_from_id(self):
+        """int: The ID of OpenVPNTenant to use to obtain a CA."""
+        return self.get_attribute(
+            "use_ca_from_id", self.default_attributes["use_ca_from_id"])
+
+    @use_ca_from_id.setter
+    def use_ca_from_id(self, value):
+        self.set_attribute("use_ca_from_id", value)
+
+    @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 failover_server_ids(self):
+        """list(int): The IDs of the OpenVPNTenants to use as failover servers."""
+        return self.get_attribute(
+            "failover_server_ids", self.default_attributes["failover_server_ids"])
+
+    @failover_server_ids.setter
+    def failover_server_ids(self, value):
+        self.set_attribute("failover_server_ids", 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 port_number(self):
+        """int: the integer representing the port number for this server"""
+        return self.get_attribute("port", self.default_attributes['port'])
+
+    @port_number.setter
+    def port_number(self, value):
+        self.set_attribute("port", value)
+
+    def get_ca_crt(self, pki_dir):
+        """Gets the lines fo the ca.crt file for this OpenVPNTenant.
+
+        Parameters:
+            pki_dir (str): The PKI directory to look in.
+
+        Returns:
+            list(str): The lines of the ca.crt file for this OpenVPNTenant.
+        """
+        with open(pki_dir + "/ca.crt", 'r') as f:
+            return f.readlines()
+
+    def get_client_cert(self, client_name, pki_dir):
+        """Gets the lines fo the crt file for a client.
+
+        Parameters:
+            pki_dir (str): The PKI directory to look in.
+            client_name (str): The client name to use.
+
+        Returns:
+            list(str): The lines of the crt file for the client.
+        """
+        with open(pki_dir + "/issued/" + client_name + ".crt", 'r') as f:
+            return f.readlines()
+
+    def get_client_key(self, client_name, pki_dir):
+        """Gets the lines fo the key file for a client.
+
+        Parameters:
+            pki_dir (str): The PKI directory to look in.
+            client_name (str): The client name to use.
+
+        Returns:
+            list(str): The lines of the key file for the client.
+        """
+        with open(pki_dir + "/private/" + client_name + ".key", 'r') as f:
+            return f.readlines()
+
+
+def model_policy_vpn_tenant(pk):
+    """Manages the container for the VPN Tenant.
+
+    Parameters
+        pk (int): The ID of this OpenVPNTenant.
+    """
+    # 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 = OpenVPNTenant.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/openvpn/templates/connect.vpn.j2 b/xos/services/openvpn/templates/connect.vpn.j2
new file mode 100644
index 0000000..2028cd9
--- /dev/null
+++ b/xos/services/openvpn/templates/connect.vpn.j2
@@ -0,0 +1,24 @@
+#! /bin/bash
+# This file autogenerated by OpenVPNTenant.
+# It contains a script used to generate the OPENVPN client files.
+printf "%b" "client
+dev tun
+remote-cert-tls server
+resolv-retry 60
+nobind
+ca ca.crt
+cert {{ client_name }}.crt
+key {{ client_name }}.key
+verb 3
+{% for tenant in remotes %}remote {{ tenant.nat_ip }} {{ tenant.port_number }} {{ tenant.protocol }}{% endfor %}
+{% if is_persistent %}
+persist-tun
+persist-key
+{% endif %}
+" > client.conf
+printf "%b" "{% for line in ca_crt %}{{ line }}{% endfor %}" > ca.crt
+printf "%b" "{% for line in client_crt %}{{ line }}{% endfor %}" > {{ client_name }}.crt
+printf "%b" "{% for line in client_key %}{{ line }}{% endfor %}" > {{ client_name }}.key
+apt-get update
+apt-get install openvpn -y
+openvpn client.conf
diff --git a/xos/services/vtn/admin.py b/xos/services/vtn/admin.py
index c64e054..03ff5cd 100644
--- a/xos/services/vtn/admin.py
+++ b/xos/services/vtn/admin.py
@@ -29,6 +29,9 @@
     sshUser = forms.CharField(required=False)
     sshKeyFile = forms.CharField(required=False)
     mgmtSubnetBits = forms.CharField(required=False)
+    xosEndpoint = forms.CharField(required=False)
+    xosUser = forms.CharField(required=False)
+    xosPassword = forms.CharField(required=False)
 
     def __init__(self,*args,**kwargs):
         super (VTNServiceForm,self ).__init__(*args,**kwargs)
@@ -40,6 +43,9 @@
             self.fields['sshUser'].initial = self.instance.sshUser
             self.fields['sshKeyFile'].initial = self.instance.sshKeyFile
             self.fields['mgmtSubnetBits'].initial = self.instance.mgmtSubnetBits
+            self.fields['xosEndpoint'].initial = self.instance.xosEndpoint
+            self.fields['xosUser'].initial = self.instance.xosUser
+            self.fields['xosPassword'].initial = self.instance.xosPassword
 
     def save(self, commit=True):
         self.instance.privateGatewayMac = self.cleaned_data.get("privateGatewayMac")
@@ -49,6 +55,9 @@
         self.instance.sshUser = self.cleaned_data.get("sshUser")
         self.instance.sshKeyFile = self.cleaned_data.get("sshKeyFile")
         self.instance.mgmtSubnetBits = self.cleaned_data.get("mgmtSubnetBits")
+        self.instance.xosEndpoint = self.cleaned_data.get("xosEndpoint")
+        self.instance.xosUser = self.cleaned_data.get("xosUser")
+        self.instance.xosPassword = self.cleaned_data.get("xosPassword")
         return super(VTNServiceForm, self).save(commit=commit)
 
     class Meta:
@@ -62,7 +71,7 @@
     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","icon_url",
-                                    'privateGatewayMac', 'localManagementIp', 'ovsdbPort', 'sshPort', 'sshUser', 'sshKeyFile', 'mgmtSubnetBits' ], 'classes':['suit-tab suit-tab-general']})]
+                                    'privateGatewayMac', 'localManagementIp', 'ovsdbPort', 'sshPort', 'sshUser', 'sshKeyFile', 'mgmtSubnetBits', 'xosEndpoint', 'xosUser', 'xosPassword' ], 'classes':['suit-tab suit-tab-general']})]
     readonly_fields = ('backend_status_text', )
     inlines = [SliceInline,ServiceAttrAsTabInline,ServicePrivilegeInline]
 
@@ -84,4 +93,3 @@
         return VTNService.get_service_objects_by_user(request.user)
 
 admin.site.register(VTNService, VTNServiceAdmin)
-
diff --git a/xos/services/vtn/models.py b/xos/services/vtn/models.py
index cb254d7..52b1633 100644
--- a/xos/services/vtn/models.py
+++ b/xos/services/vtn/models.py
@@ -37,8 +37,10 @@
                           ("sshUser", "root"),
                           ("sshKeyFile", "/root/node_key") ,
                           ("mgmtSubnetBits", "24"),
+                          ("xosEndpoint", "http://xos/"),
+                          ("xosUser", "padmin@vicci.org"),
+                          ("xosPassword", "letmein"),
 
                          )
 
 VTNService.setup_simple_attributes()
-
diff --git a/xos/synchronizers/base/backend.py b/xos/synchronizers/base/backend.py
index 5f11d46..d7307fa 100644
--- a/xos/synchronizers/base/backend.py
+++ b/xos/synchronizers/base/backend.py
@@ -15,7 +15,7 @@
     def run(self):
         # start the openstack observer
         observer = XOSObserver()
-        observer_thread = threading.Thread(target=observer.run)
+        observer_thread = threading.Thread(target=observer.run,name='synchronizer')
         observer_thread.start()
 
         # start model policies thread
diff --git a/xos/synchronizers/base/event_loop.py b/xos/synchronizers/base/event_loop.py
index f224380..4f7d436 100644
--- a/xos/synchronizers/base/event_loop.py
+++ b/xos/synchronizers/base/event_loop.py
@@ -521,7 +521,12 @@
 
                         loop_end = time.time()
 
-                        diag = Diag.objects.filter(name=Config().observer_name).first()
+                        try:
+                            observer_name = Config().observer_name
+                        except:
+                            observer_name = ''
+
+                        diag = Diag.objects.filter(name=observer_name).first()
                         if (diag):
                             br_str = diag.backend_register
                             br = json.loads(br_str)
diff --git a/xos/synchronizers/helloworld/helloworld-synchronizer.py b/xos/synchronizers/helloworld/helloworld-synchronizer.py
deleted file mode 100755
index 84bec4f..0000000
--- a/xos/synchronizers/helloworld/helloworld-synchronizer.py
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/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/synchronizers/helloworld/helloworld_config b/xos/synchronizers/helloworld/helloworld_config
deleted file mode 100644
index 1f67242..0000000
--- a/xos/synchronizers/helloworld/helloworld_config
+++ /dev/null
@@ -1,47 +0,0 @@
-[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/synchronizers/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/synchronizers/helloworld/model-deps b/xos/synchronizers/helloworld/model-deps
deleted file mode 100644
index 63188f0..0000000
--- a/xos/synchronizers/helloworld/model-deps
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-    "OriginServer": [
-        "ContentProvider"
-    ], 
-    "ContentProvider": [
-        "ServiceProvider"
-    ], 
-    "CDNPrefix": [
-        "ContentProvider"
-    ], 
-    "AccessMap": [
-        "ContentProvider"
-    ], 
-    "SiteMap": [
-        "ContentProvider", 
-        "ServiceProvider", 
-        "CDNPrefix"
-    ]
-}
diff --git a/xos/synchronizers/helloworld/nohup.out b/xos/synchronizers/helloworld/nohup.out
deleted file mode 100644
index 74072c6..0000000
--- a/xos/synchronizers/helloworld/nohup.out
+++ /dev/null
@@ -1 +0,0 @@
-python: can't open file 'helloworld-observer.py': [Errno 2] No such file or directory
diff --git a/xos/synchronizers/helloworld/run.sh b/xos/synchronizers/helloworld/run.sh
deleted file mode 100755
index 1b9d834..0000000
--- a/xos/synchronizers/helloworld/run.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#if [[ ! -e ./hpc-backend.py ]]; then
-#    ln -s ../xos-observer.py hpc-backend.py
-#fi
-
-export XOS_DIR=/opt/xos
-python helloworld-synchronizer.py  -C $XOS_DIR/synchronizers/helloworld/helloworld_config
diff --git a/xos/synchronizers/helloworld/start.sh b/xos/synchronizers/helloworld/start.sh
deleted file mode 100755
index 7945db3..0000000
--- a/xos/synchronizers/helloworld/start.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-export XOS_DIR=/opt/xos
-
-echo $XOS_DIR/synchronizers/helloworld/helloworld_config
-python helloworld-synchronizer.py -C $XOS_DIR/synchronizers/helloworld/helloworld_config
diff --git a/xos/synchronizers/helloworld/steps/sync_hello.py b/xos/synchronizers/helloworld/steps/sync_hello.py
deleted file mode 100644
index 55d318a..0000000
--- a/xos/synchronizers/helloworld/steps/sync_hello.py
+++ /dev/null
@@ -1,26 +0,0 @@
-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 xos.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/synchronizers/helloworld/steps/sync_world.py b/xos/synchronizers/helloworld/steps/sync_world.py
deleted file mode 100644
index a4e7e3c..0000000
--- a/xos/synchronizers/helloworld/steps/sync_world.py
+++ /dev/null
@@ -1,25 +0,0 @@
-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 xos.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/synchronizers/helloworld/stop.sh b/xos/synchronizers/helloworld/stop.sh
deleted file mode 100755
index a0b4a8e..0000000
--- a/xos/synchronizers/helloworld/stop.sh
+++ /dev/null
@@ -1 +0,0 @@
-pkill -9 -f hpc-observer.py
diff --git a/xos/synchronizers/helloworldservice_complete/helloworldservice_config b/xos/synchronizers/helloworldservice_complete/helloworldservice_config
deleted file mode 100644
index 69894fc..0000000
--- a/xos/synchronizers/helloworldservice_complete/helloworldservice_config
+++ /dev/null
@@ -1,36 +0,0 @@
-# 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/synchronizers/helloworldservice_complete/model-deps
-# The location of your SyncSteps
-steps_dir=/opt/xos/synchronizers/helloworldservice_complete/steps
-# A temporary directory that will be used by ansible
-sys_dir=/opt/xos/synchronizers/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/synchronizers/helloworldservice_complete/run.sh b/xos/synchronizers/helloworldservice_complete/run.sh
deleted file mode 100755
index 331f8ae..0000000
--- a/xos/synchronizers/helloworldservice_complete/run.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-# Runs the XOS observer using helloworldservice_config
-export XOS_DIR=/opt/xos
-python helloworldservice-synchronizer.py  -C $XOS_DIR/synchronizers/helloworldservice_complete/helloworldservice_config
diff --git a/xos/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.py b/xos/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.py
deleted file mode 100644
index 69a08f5..0000000
--- a/xos/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import os
-import sys
-from django.db.models import Q, F
-from services.helloworldservice_complete.models import HelloWorldServiceComplete, HelloWorldTenantComplete
-from synchronizers.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/synchronizers/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/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.yaml b/xos/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.yaml
deleted file mode 100644
index 719c75f..0000000
--- a/xos/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
----
-- 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/synchronizers/helloworldservice_complete/stop.sh b/xos/synchronizers/helloworldservice_complete/stop.sh
deleted file mode 100755
index 76e68d9..0000000
--- a/xos/synchronizers/helloworldservice_complete/stop.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-# Kill the observer
-pkill -9 -f helloworldservice-observer.py
diff --git a/xos/synchronizers/onos/steps/sync_onosapp.py b/xos/synchronizers/onos/steps/sync_onosapp.py
index 666da21..944e7fe 100644
--- a/xos/synchronizers/onos/steps/sync_onosapp.py
+++ b/xos/synchronizers/onos/steps/sync_onosapp.py
@@ -17,6 +17,7 @@
 from services.onos.models import ONOSService, ONOSApp
 from xos.logger import Logger, logging
 from services.vrouter.models import VRouterService
+from services.vtn.models import VTNService
 
 # hpclibrary will be in steps/..
 parentdir = os.path.join(os.path.dirname(__file__),"..")
@@ -117,18 +118,9 @@
                 raise Exception("Controller user object for %s does not exist" % instance.creator)
             return cuser.kuser_id
 
-
-    def node_tag_default(self, o, node, tagname, default):
+    def get_node_tag(self, o, node, tagname):
         tags = Tag.select_by_content_object(node).filter(name=tagname)
-        if tags:
-            value = tags[0].value
-        else:
-            value = default
-            logger.info("node %s: saving default value %s for tag %s" % (node.name, value, tagname))
-            service = self.get_onos_service(o)
-            tag = Tag(service=service, content_object=node, name=tagname, value=value)
-            tag.save()
-        return value
+        return tags[0].value
 
     # Scan attrs for attribute name
     # If it's not present, save it as a TenantAttribute
@@ -145,16 +137,31 @@
     # This function currently assumes a single Deployment and Site
     def get_vtn_config(self, o, attrs):
 
-        # The "attrs" argument contains a list of all service and tenant attributes
-        # If an attribute is present, use it in the configuration
-        # Otherwise save the attriute with a reasonable (for a CORD devel pod) default value
-        # The admin will see all possible configuration values and the assigned defaults
-        privateGatewayMac = self.attribute_default(o, attrs, "privateGatewayMac", "00:00:00:00:00:01")
-        localManagementIp = self.attribute_default(o, attrs, "localManagementIp", "172.27.0.1/24")
-        ovsdbPort = self.attribute_default(o, attrs, "ovsdbPort", "6641")
-        sshPort = self.attribute_default(o, attrs, "sshPort", "22")
-        sshUser = self.attribute_default(o, attrs, "sshUser", "root")
-        sshKeyFile = self.attribute_default(o, attrs, "sshKeyFile", "/root/node_key")
+        privateGatewayMac = None
+        localManagementIp = None
+        ovsdbPort = None
+        sshPort = None
+        sshUser = None
+        sshKeyFile = None
+        mgmtSubnetBits = None
+        xosEndpoint = None
+        xosUser = None
+        xosPassword = None
+
+        # VTN-specific configuration from the VTN Service
+        vtns = VTNService.get_service_objects().all()
+        if vtns:
+            vtn = vtns[0]
+            privateGatewayMac = vtn.privateGatewayMac
+            localManagementIp = vtn.localManagementIp
+            ovsdbPort = vtn.ovsdbPort
+            sshPort = vtn.sshPort
+            sshUser = vtn.sshUser
+            sshKeyFile = vtn.sshKeyFile
+            mgmtSubnetBits = vtn.mgmtSubnetBits
+            xosEndpoint = vtn.xosEndpoint
+            xosUser = vtn.xosUser
+            xosPassword = vtn.xosPassword
 
         # OpenStack endpoints and credentials
         keystone_server = "http://keystone:5000/v2.0/"
@@ -186,6 +193,11 @@
                             "user": user_name,
                             "password": password
                         },
+                        "xos": {
+                            "endpoint": xosEndpoint,
+                            "user": xosUser,
+                            "password": xosPassword
+                        },
                         "publicGateways": [],
                         "nodes" : []
                     }
@@ -194,20 +206,14 @@
         }
 
         # Generate apps->org.onosproject.cordvtn->cordvtn->nodes
-
-        # We need to generate a CIDR address for the physical node's
-        # address on the management network
-        mgmtSubnetBits = self.attribute_default(o, attrs, "mgmtSubnetBits", "24")
-
         nodes = Node.objects.all()
         for node in nodes:
             nodeip = socket.gethostbyname(node.name)
 
             try:
-                bridgeId = self.node_tag_default(o, node, "bridgeId", "of:0000000000000001")
-                dataPlaneIntf = self.node_tag_default(o, node, "dataPlaneIntf", "veth1")
-                # This should be generated from the AddressPool if not present
-                dataPlaneIp = self.node_tag_default(o, node, "dataPlaneIp", "192.168.199.1/24")
+                bridgeId = self.get_node_tag(o, node, "bridgeId")
+                dataPlaneIntf = self.get_node_tag(o, node, "dataPlaneIntf")
+                dataPlaneIp = self.get_node_tag(o, node, "dataPlaneIp")
             except:
                 logger.error("not adding node %s to the VTN configuration" % node.name)
                 continue
diff --git a/xos/services/helloworld/__init__.py b/xos/synchronizers/openvpn/__init__.py
similarity index 100%
copy from xos/services/helloworld/__init__.py
copy to xos/synchronizers/openvpn/__init__.py
diff --git a/xos/synchronizers/helloworldservice_complete/model-deps b/xos/synchronizers/openvpn/model-deps
similarity index 100%
rename from xos/synchronizers/helloworldservice_complete/model-deps
rename to xos/synchronizers/openvpn/model-deps
diff --git a/xos/synchronizers/helloworldservice_complete/helloworldservice-synchronizer.py b/xos/synchronizers/openvpn/openvpn-synchronizer.py
similarity index 76%
rename from xos/synchronizers/helloworldservice_complete/helloworldservice-synchronizer.py
rename to xos/synchronizers/openvpn/openvpn-synchronizer.py
index 95f4081..3227ed9 100755
--- a/xos/synchronizers/helloworldservice_complete/helloworldservice-synchronizer.py
+++ b/xos/synchronizers/openvpn/openvpn-synchronizer.py
@@ -1,8 +1,5 @@
 #!/usr/bin/env python
 
-# This imports and runs ../../xos-observer.py
-# Runs the standard XOS observer
-
 import importlib
 import os
 import sys
diff --git a/xos/synchronizers/openvpn/openvpn_config b/xos/synchronizers/openvpn/openvpn_config
new file mode 100644
index 0000000..8a58b52
--- /dev/null
+++ b/xos/synchronizers/openvpn/openvpn_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=openvpn
+dependency_graph=/opt/xos/synchronizers/openvpn/model-deps
+steps_dir=/opt/xos/synchronizers/openvpn/steps
+sys_dir=/opt/xos/synchronizers/openvpn/sys
+logfile=/var/log/xos_backend.log
+pretend=False
+backoff_disabled=True
+save_ansible_output=True
+proxy_ssh=False
diff --git a/xos/synchronizers/openvpn/run.sh b/xos/synchronizers/openvpn/run.sh
new file mode 100755
index 0000000..a5d90c9
--- /dev/null
+++ b/xos/synchronizers/openvpn/run.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+python openvpn-synchronizer.py  -C $XOS_DIR/synchronizers/openvpn/openvpn_config
diff --git a/xos/services/helloworld/__init__.py b/xos/synchronizers/openvpn/steps/__init__.py
similarity index 100%
copy from xos/services/helloworld/__init__.py
copy to xos/synchronizers/openvpn/steps/__init__.py
diff --git a/xos/synchronizers/openvpn/steps/roles/openvpn/handlers/main.yml b/xos/synchronizers/openvpn/steps/roles/openvpn/handlers/main.yml
new file mode 100644
index 0000000..8725e29
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/roles/openvpn/handlers/main.yml
@@ -0,0 +1,4 @@
+---
+
+- name: restart openvpn
+  shell: (kill -9 $(cat {{ pki_dir }}/pid) || true) && (openvpn {{ pki_dir }}/server.conf &)
diff --git a/xos/synchronizers/openvpn/steps/roles/openvpn/tasks/main.yml b/xos/synchronizers/openvpn/steps/roles/openvpn/tasks/main.yml
new file mode 100644
index 0000000..47093b2
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/roles/openvpn/tasks/main.yml
@@ -0,0 +1,38 @@
+---
+
+- name: install openvpn
+  apt: name=openvpn state=present update_cache=yes
+
+- name: make sure /opt/openvpn exists
+  file: path=/opt/openvpn state=directory
+
+- name: make sure directory for this server exists
+  file: path={{ pki_dir }} state=directory
+
+- name: get server key
+  copy: src={{ pki_dir }}/private/server.key dest={{ pki_dir }}/server.key
+  notify:
+  - restart openvpn
+
+- name: get server crt
+  copy: src={{ pki_dir }}/issued/server.crt dest={{ pki_dir }}/server.crt
+  notify:
+  - restart openvpn
+
+- name: get ca crt
+  copy: src={{ pki_dir }}/ca.crt dest={{ pki_dir }}/ca.crt
+  notify:
+  - restart openvpn
+
+- name: get crl
+  copy: src={{ pki_dir }}/crl.pem dest={{ pki_dir }}/crl.pem
+
+- name: get dh
+  copy: src={{ pki_dir }}/dh.pem dest={{ pki_dir }}/dh.pem
+  notify:
+  - restart openvpn
+
+- name: write config
+  template: src=server.conf.j2 dest={{ pki_dir }}/server.conf owner=root group=root
+  notify:
+  - restart openvpn
diff --git a/xos/synchronizers/openvpn/steps/roles/openvpn/templates/server.conf.j2 b/xos/synchronizers/openvpn/steps/roles/openvpn/templates/server.conf.j2
new file mode 100644
index 0000000..4766e7b
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/roles/openvpn/templates/server.conf.j2
@@ -0,0 +1,24 @@
+# This file autogenerated by OpenVPNTenant synchronizer
+# It contains the OPENVPN config file for the server
+script-security 3 system
+port {{ port_number }}
+proto {{ protocol }}
+dev tun
+writepid {{ pki_dir }}/pid
+ca {{ pki_dir }}/ca.crt
+cert {{ pki_dir }}/server.crt
+key {{ pki_dir }}/server.key
+dh {{ pki_dir }}/dh.pem
+crl-verify {{ pki_dir }}/crl.pem
+server {{ server_network }} {{ vpn_subnet }}
+ifconfig-pool-persist {{ pki_dir }}/ipp.txt
+status {{ pki_dir }}/openvpn-status.log
+verb 3
+{% if is_persistent %}
+keepalive 10 60
+persist-tun
+persist-key
+{% endif %}
+{% if clients_can_see_each_other %}
+client-to-client
+{% endif %}
diff --git a/xos/synchronizers/openvpn/steps/sync_openvpntenant.py b/xos/synchronizers/openvpn/steps/sync_openvpntenant.py
new file mode 100644
index 0000000..b58dd94
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/sync_openvpntenant.py
@@ -0,0 +1,75 @@
+import os
+import shutil
+import sys
+
+from django.db.models import F, Q
+
+from services.openvpn.models import OpenVPNService, OpenVPNTenant
+from synchronizers.base.SyncInstanceUsingAnsible import \
+    SyncInstanceUsingAnsible
+
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+
+class SyncOpenVPNTenant(SyncInstanceUsingAnsible):
+    """Class for syncing a OpenVPNTenant using Ansible.
+
+    This SyncStep creates any necessary files for the OpenVPNTenant using ESAY RSA and then runs the
+    Ansible template to start the server on an instance.
+    """
+    provides = [OpenVPNTenant]
+    observes = OpenVPNTenant
+    requested_interval = 0
+    template_name = "sync_openvpntenant.yaml"
+    service_key_name = "/opt/xos/synchronizers/openvpn/openvpn_private_key"
+
+    def fetch_pending(self, deleted):
+        if (not deleted):
+            objs = OpenVPNTenant.get_tenant_objects().filter(
+                Q(enacted__lt=F('updated')) |
+                Q(enacted=None), Q(lazy_blocked=False))
+        else:
+            objs = OpenVPNTenant.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),
+                "port_number": tenant.port_number,
+                "protocol": tenant.protocol,
+                "pki_dir": OpenVPNService.get_pki_dir(tenant)
+                }
+
+    def sync_fields(self, o, fields):
+        pki_dir = OpenVPNService.get_pki_dir(o)
+
+        if (not os.path.isdir(pki_dir)):
+            OpenVPNService.execute_easyrsa_command(pki_dir, "init-pki")
+            OpenVPNService.execute_easyrsa_command(
+                pki_dir, "--req-cn=XOS build-ca nopass")
+
+        # Very hacky way to handle VPNs that need to share CAs
+        if (o.use_ca_from_id):
+            tenant = OpenVPNTenant.get_tenant_objects().filter(
+                pk=o.use_ca_from_id)[0]
+            other_pki_dir = OpenVPNService.get_pki_dir(tenant)
+            shutil.copy2(other_pki_dir + "/ca.crt", pki_dir)
+            shutil.copy2(other_pki_dir + "/private/ca.key",
+                         pki_dir + "/private")
+
+        # If the server has to be built then we need to build it
+        if (not os.path.isfile(pki_dir + "/issued/server.crt")):
+            OpenVPNService.execute_easyrsa_command(
+                pki_dir, "build-server-full server nopass")
+            OpenVPNService.execute_easyrsa_command(pki_dir, "gen-dh")
+
+        # Get the most recent list of revoked clients
+        OpenVPNService.execute_easyrsa_command(pki_dir, "gen-crl")
+
+        # Super runs the playbook
+        super(SyncOpenVPNTenant, self).sync_fields(o, fields)
diff --git a/xos/synchronizers/openvpn/steps/sync_openvpntenant.yaml b/xos/synchronizers/openvpn/steps/sync_openvpntenant.yaml
new file mode 100644
index 0000000..e36f51b
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/sync_openvpntenant.yaml
@@ -0,0 +1,17 @@
+---
+- 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 }}
+    port_number: {{ port_number }}
+    protocol: {{ protocol }}
+    pki_dir: {{ pki_dir }}
+
+  roles:
+    - openvpn
diff --git a/xos/synchronizers/openvpn/steps/sync_tenantprivilege.py b/xos/synchronizers/openvpn/steps/sync_tenantprivilege.py
new file mode 100644
index 0000000..51ee6df
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/sync_tenantprivilege.py
@@ -0,0 +1,79 @@
+import os
+import sys
+
+from core.models import TenantPrivilege
+from services.openvpn.models import OPENVPN_KIND, OpenVPNService, OpenVPNTenant
+from synchronizers.base.syncstep import DeferredException, SyncStep
+
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+
+class SyncTenantPrivilege(SyncStep):
+    """Class for syncing a TenantPrivilege for a OpenVPNTenant.
+
+    This SyncStep isolates the updated TenantPrivileges that are for OpenVPNTenants and performs
+    actions if the TenantPrivilege has been added or deleted. For added privileges a new client
+    certificate and key are made, signed with the ca.crt file used by this OpenVPNTenant. For deleted
+    privileges the client certificate is revoked and the files associated are deleted. In both
+    cases the associated OpenVPNTenant is saved causing the OpenVPNTenant synchronizer to run.
+    """
+    provides = [TenantPrivilege]
+    observes = TenantPrivilege
+    requested_interval = 0
+
+    def fetch_pending(self, deleted):
+        privs = super(SyncTenantPrivilege, self).fetch_pending(deleted)
+        # Get only the TenantPrivileges that relate to OpenVPNTenants
+        privs = [priv for priv in privs if priv.tenant.kind == OPENVPN_KIND]
+        return privs
+
+    def sync_record(self, record):
+        if (not record.tenant.id):
+            raise DeferredException("Privilege waiting on VPN Tenant ID")
+        certificate = self.get_certificate_name(record)
+        tenant = OpenVPNTenant.get_tenant_objects().filter(pk=record.tenant.id)[0]
+        if (not tenant):
+            raise DeferredException("Privilege waiting on VPN Tenant")
+        # Only add a certificate if ones does not yet exist
+        pki_dir = OpenVPNService.get_pki_dir(tenant)
+        if (not os.path.isfile(pki_dir + "/issued/" + certificate + ".crt")):
+            OpenVPNService.execute_easyrsa_command(
+                pki_dir, "build-client-full " + certificate + " nopass")
+            tenant.save()
+        record.save()
+
+    def delete_record(self, record):
+        if (not record.tenant.id):
+            return
+        certificate = self.get_certificate_name(record)
+        tenant = OpenVPNTenant.get_tenant_objects().filter(pk=record.tenant.id)[0]
+        if (not tenant):
+            return
+        # If the client has already been reovked don't do it again
+        pki_dir = OpenVPNService.get_pki_dir(tenant)
+        if (os.path.isfile(pki_dir + "/issued/" + certificate + ".crt")):
+            OpenVPNService.execute_easyrsa_command(
+                pki_dir, "revoke " + certificate)
+            # Revoking a client cert does not delete any of the files
+            # to make sure that we can add this user again we need to
+            # delete all of the files created by easyrsa
+            os.remove(pki_dir + "/issued/" + certificate + ".crt")
+            os.remove(pki_dir + "/private/" + certificate + ".key")
+            os.remove(pki_dir + "/reqs/" + certificate + ".req")
+            tenant.save()
+
+        record.delete()
+
+    def get_certificate_name(self, tenant_privilege):
+        """Gets the name of a certificate for the given TenantPrivilege
+
+        Parameters:
+            tenant_privilege (core.models.TenantPrivilege): The TenantPrivilege to use to generate
+                the certificate name.
+
+        Returns:
+            str: The certificate name.
+        """
+        return (str(tenant_privilege.user.email) +
+                "-" + str(tenant_privilege.tenant.id))
diff --git a/xos/synchronizers/openvpn/stop.sh b/xos/synchronizers/openvpn/stop.sh
new file mode 100755
index 0000000..4a83aca
--- /dev/null
+++ b/xos/synchronizers/openvpn/stop.sh
@@ -0,0 +1,2 @@
+# Kill the observer
+pkill -9 -f openvpn-synchronizer.py
diff --git a/xos/tools/xos-manage b/xos/tools/xos-manage
index 7fed845..e955b5d 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
@@ -144,8 +144,8 @@
     python ./manage.py makemigrations syndicate_storage
     python ./manage.py makemigrations cord
     python ./manage.py makemigrations ceilometer
-    python ./manage.py makemigrations helloworldservice_complete
     python ./manage.py makemigrations onos
+    python ./manage.py makemigrations openvpn
     python ./manage.py makemigrations vtr
     python ./manage.py makemigrations vrouter
     python ./manage.py makemigrations vtn
diff --git a/xos/tosca/custom_types/xos.m4 b/xos/tosca/custom_types/xos.m4
index 9bd504a..32eefa4 100644
--- a/xos/tosca/custom_types/xos.m4
+++ b/xos/tosca/custom_types/xos.m4
@@ -282,6 +282,16 @@
             mgmtSubnetBits:
                 type: string
                 required: false
+            xosEndpoint:
+                type: string
+                required: false
+            xosUser:
+                type: string
+                required: false
+            xosPassword:
+                type: string
+                required: false
+
 
     tosca.nodes.CDNService:
         derived_from: tosca.nodes.Root
@@ -385,16 +395,17 @@
                 type: tosca.capabilities.xos.User
 
         properties:
+            xos_base_props
             password:
                 type: string
                 required: false
             firstname:
                 type: string
-                required: true
+                required: false
                 description: First name of User.
             lastname:
                 type: string
-                required: true
+                required: false
                 description: Last name of User.
             phone:
                 type: string
@@ -410,11 +421,13 @@
                 description: Public key that will be installed in Instances.
             is_active:
                 type: boolean
-                default: true
+                required: false
+                #default: true
                 description: If True, the user may log in.
             is_admin:
                 type: boolean
-                default: false
+                required: false
+                #default: false
                 description: If True, the user has root admin privileges.
             login_page:
                 type: string
@@ -428,6 +441,9 @@
             An XOS network parameter type. May be applied to Networks and/or
             Ports.
 
+        properties:
+            xos_base_props
+
         capabilities:
             network_parameter_type:
                 type: tosca.capabilities.xos.NetworkParameterType
@@ -444,13 +460,14 @@
                 type: tosca.capabilities.xos.NetworkTemplate
 
         properties:
+            xos_base_props
             visibility:
                 type: string
-                default: private
+                required: false
                 description: Indicates whether network is publicly routable.
             translation:
                 type: string
-                default: none
+                required: false
                 description: Indicates whether network uses address translation.
             shared_network_name:
                 type: string
@@ -462,7 +479,7 @@
                 description: Attaches this template to a specific OpenStack network.
             topology_kind:
                 type: string
-                default: BigSwitch
+                required: false
                 description: Describes the topology of the network.
             controller_kind:
                 type: string
@@ -748,13 +765,6 @@
                 type: string
                 required: false
                 description: default isolation to use when bringing up instances (default to 'vm')
-            default_flavor:
-                # Note: we should probably formally introduce flavors to Tosca
-                # at some point, and use a requirement/relationship instead of
-                # a text string.
-                type: string
-                required: false
-                description: default flavor to use for slice
             network:
                 type: string
                 required: false
@@ -785,6 +795,60 @@
             node:
                 type: tosca.capabilities.xos.NodeLabel
 
+    tosca.nodes.Flavor:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Flavor.
+        properties:
+            xos_base_props
+            flavor:
+                type: string
+                required: false
+                description: openstack flavor name
+        capabilities:
+            flavor:
+                type: tosca.capabilities.xos.Flavor
+
+    tosca.nodes.SiteRole:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Site Role.
+        properties:
+            xos_base_props
+        capabilities:
+            siterole:
+                type: tosca.capabilities.xos.SiteRole
+
+    tosca.nodes.SliceRole:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Slice Role.
+        properties:
+            xos_base_props
+        capabilities:
+            slicerole:
+                type: tosca.capabilities.xos.SliceRole
+
+    tosca.nodes.TenantRole:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Tenant Role.
+        properties:
+            xos_base_props
+        capabilities:
+            tenantrole:
+                type: tosca.capabilities.xos.TenantRole
+
+    tosca.nodes.DeploymentRole:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Deployment Role.
+        properties:
+            xos_base_props
+        capabilities:
+            deploymentrole:
+                type: tosca.capabilities.xos.DeploymentRole
+
     tosca.nodes.DashboardView:
         derived_from: tosca.nodes.Root
         description: >
@@ -802,6 +866,21 @@
                 required: false
                 description: URL to the dashboard
 
+    tosca.nodes.Tag:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Tag
+        properties:
+            xos_base_props
+            name:
+                type: string
+                required: true
+                descrption: name of tag
+            value:
+                type: string
+                required: false
+                descrption: value of tag
+
     tosca.nodes.Compute.Container:
       derived_from: tosca.nodes.Compute
       description: >
@@ -923,6 +1002,14 @@
         derived_from: tosca.relationships.Root
         valid_target_types: [ tosca.capabilities.xos.NodeLabel ]
 
+    tosca.relationships.SupportsFlavor:
+        derived_from: tosca.relationships.Root
+        valid_target_types: [ tosca.capabilities.xos.Flavor ]
+
+    tosca.relationships.DefaultFlavor:
+        derived_from: tosca.relationships.Root
+        valid_target_types: [ tosca.capabilities.xos.Flavor ]
+
     tosca.relationships.ProvidesAddresses:
         derived_from: tosca.relationships.Root
         valid_target_types: [ tosca.capabilities.xos.AddressPool ]
@@ -930,6 +1017,9 @@
     tosca.relationships.DependsOn:
         derived_from: tosca.relationships.Root
 
+    tosca.relationships.TagsObject:
+        derived_from: tosca.relationships.Root
+
     tosca.capabilities.xos.Service:
         derived_from: tosca.capabilities.Root
         description: An XOS Service
@@ -978,6 +1068,26 @@
         derived_from: tosca.capabilities.Root
         description: An XOS NodeLabel
 
+    tosca.capabilities.xos.Flavor:
+        derived_from: tosca.capabilities.Root
+        description: An XOS Flavor
+
+    tosca.capabilities.xos.DeploymentRole:
+        derived_from: tosca.capabilities.Root
+        description: An XOS DeploymentRole
+
+    tosca.capabilities.xos.SliceRole:
+        derived_from: tosca.capabilities.Root
+        description: An XOS SliceRole
+
+    tosca.capabilities.xos.SiteRole:
+        derived_from: tosca.capabilities.Root
+        description: An XOS SiteRole
+
+    tosca.capabilities.xos.TenantRole:
+        derived_from: tosca.capabilities.Root
+        description: An XOS TenantRole
+
     tosca.capabilities.xos.Image:
         derived_from: tosca.capabilities.Root
         description: An XOS Image
diff --git a/xos/tosca/custom_types/xos.yaml b/xos/tosca/custom_types/xos.yaml
index f790cc5..8102d1c 100644
--- a/xos/tosca/custom_types/xos.yaml
+++ b/xos/tosca/custom_types/xos.yaml
@@ -516,6 +516,15 @@
             mgmtSubnetBits:
                 type: string
                 required: false
+            xosEndpoint:
+                type: string
+                required: false
+            xosUser:
+                type: string
+                required: false
+            xosPassword:
+                type: string
+                required: false
 
     tosca.nodes.CDNService:
         derived_from: tosca.nodes.Root
@@ -730,16 +739,28 @@
                 type: tosca.capabilities.xos.User
 
         properties:
+            no-delete:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to delete this object
+            no-create:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to create this object
+            no-update:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to update this object
             password:
                 type: string
                 required: false
             firstname:
                 type: string
-                required: true
+                required: false
                 description: First name of User.
             lastname:
                 type: string
-                required: true
+                required: false
                 description: Last name of User.
             phone:
                 type: string
@@ -755,11 +776,13 @@
                 description: Public key that will be installed in Instances.
             is_active:
                 type: boolean
-                default: true
+                required: false
+                #default: true
                 description: If True, the user may log in.
             is_admin:
                 type: boolean
-                default: false
+                required: false
+                #default: false
                 description: If True, the user has root admin privileges.
             login_page:
                 type: string
@@ -773,6 +796,20 @@
             An XOS network parameter type. May be applied to Networks and/or
             Ports.
 
+        properties:
+            no-delete:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to delete this object
+            no-create:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to create this object
+            no-update:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to update this object
+
         capabilities:
             network_parameter_type:
                 type: tosca.capabilities.xos.NetworkParameterType
@@ -789,13 +826,25 @@
                 type: tosca.capabilities.xos.NetworkTemplate
 
         properties:
+            no-delete:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to delete this object
+            no-create:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to create this object
+            no-update:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to update this object
             visibility:
                 type: string
-                default: private
+                required: false
                 description: Indicates whether network is publicly routable.
             translation:
                 type: string
-                default: none
+                required: false
                 description: Indicates whether network uses address translation.
             shared_network_name:
                 type: string
@@ -807,7 +856,7 @@
                 description: Attaches this template to a specific OpenStack network.
             topology_kind:
                 type: string
-                default: BigSwitch
+                required: false
                 description: Describes the topology of the network.
             controller_kind:
                 type: string
@@ -1159,13 +1208,6 @@
                 type: string
                 required: false
                 description: default isolation to use when bringing up instances (default to 'vm')
-            default_flavor:
-                # Note: we should probably formally introduce flavors to Tosca
-                # at some point, and use a requirement/relationship instead of
-                # a text string.
-                type: string
-                required: false
-                description: default flavor to use for slice
             network:
                 type: string
                 required: false
@@ -1218,6 +1260,115 @@
             node:
                 type: tosca.capabilities.xos.NodeLabel
 
+    tosca.nodes.Flavor:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Flavor.
+        properties:
+            no-delete:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to delete this object
+            no-create:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to create this object
+            no-update:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to update this object
+            flavor:
+                type: string
+                required: false
+                description: openstack flavor name
+        capabilities:
+            flavor:
+                type: tosca.capabilities.xos.Flavor
+
+    tosca.nodes.SiteRole:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Site Role.
+        properties:
+            no-delete:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to delete this object
+            no-create:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to create this object
+            no-update:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to update this object
+        capabilities:
+            siterole:
+                type: tosca.capabilities.xos.SiteRole
+
+    tosca.nodes.SliceRole:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Slice Role.
+        properties:
+            no-delete:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to delete this object
+            no-create:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to create this object
+            no-update:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to update this object
+        capabilities:
+            slicerole:
+                type: tosca.capabilities.xos.SliceRole
+
+    tosca.nodes.TenantRole:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Tenant Role.
+        properties:
+            no-delete:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to delete this object
+            no-create:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to create this object
+            no-update:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to update this object
+        capabilities:
+            tenantrole:
+                type: tosca.capabilities.xos.TenantRole
+
+    tosca.nodes.DeploymentRole:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Deployment Role.
+        properties:
+            no-delete:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to delete this object
+            no-create:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to create this object
+            no-update:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to update this object
+        capabilities:
+            deploymentrole:
+                type: tosca.capabilities.xos.DeploymentRole
+
     tosca.nodes.DashboardView:
         derived_from: tosca.nodes.Root
         description: >
@@ -1246,6 +1397,32 @@
                 required: false
                 description: URL to the dashboard
 
+    tosca.nodes.Tag:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Tag
+        properties:
+            no-delete:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to delete this object
+            no-create:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to create this object
+            no-update:
+                type: boolean
+                default: false
+                description: Do not allow Tosca to update this object
+            name:
+                type: string
+                required: true
+                descrption: name of tag
+            value:
+                type: string
+                required: false
+                descrption: value of tag
+
     tosca.nodes.Compute.Container:
       derived_from: tosca.nodes.Compute
       description: >
@@ -1367,6 +1544,14 @@
         derived_from: tosca.relationships.Root
         valid_target_types: [ tosca.capabilities.xos.NodeLabel ]
 
+    tosca.relationships.SupportsFlavor:
+        derived_from: tosca.relationships.Root
+        valid_target_types: [ tosca.capabilities.xos.Flavor ]
+
+    tosca.relationships.DefaultFlavor:
+        derived_from: tosca.relationships.Root
+        valid_target_types: [ tosca.capabilities.xos.Flavor ]
+
     tosca.relationships.ProvidesAddresses:
         derived_from: tosca.relationships.Root
         valid_target_types: [ tosca.capabilities.xos.AddressPool ]
@@ -1374,6 +1559,9 @@
     tosca.relationships.DependsOn:
         derived_from: tosca.relationships.Root
 
+    tosca.relationships.TagsObject:
+        derived_from: tosca.relationships.Root
+
     tosca.capabilities.xos.Service:
         derived_from: tosca.capabilities.Root
         description: An XOS Service
@@ -1422,6 +1610,26 @@
         derived_from: tosca.capabilities.Root
         description: An XOS NodeLabel
 
+    tosca.capabilities.xos.Flavor:
+        derived_from: tosca.capabilities.Root
+        description: An XOS Flavor
+
+    tosca.capabilities.xos.DeploymentRole:
+        derived_from: tosca.capabilities.Root
+        description: An XOS DeploymentRole
+
+    tosca.capabilities.xos.SliceRole:
+        derived_from: tosca.capabilities.Root
+        description: An XOS SliceRole
+
+    tosca.capabilities.xos.SiteRole:
+        derived_from: tosca.capabilities.Root
+        description: An XOS SiteRole
+
+    tosca.capabilities.xos.TenantRole:
+        derived_from: tosca.capabilities.Root
+        description: An XOS TenantRole
+
     tosca.capabilities.xos.Image:
         derived_from: tosca.capabilities.Root
         description: An XOS Image
diff --git a/xos/tosca/resources/CORDUser.py b/xos/tosca/resources/CORDUser.py
index 566e205..705a895 100644
--- a/xos/tosca/resources/CORDUser.py
+++ b/xos/tosca/resources/CORDUser.py
@@ -28,12 +28,12 @@
         if not sub:
            return []
         for user in sub.users:
-            if user["name"] == self.nodetemplate.name:
+            if user["name"] == self.obj_name:
                 result.append(user)
         return result
 
     def get_xos_args(self):
-        args = {"name": self.nodetemplate.name,
+        args = {"name": self.obj_name,
                 "level": self.get_property("level"),
                 "mac": self.get_property("mac")}
         return args
@@ -46,7 +46,7 @@
         sub.create_user(**xos_args)
         sub.save()
 
-        self.info("Created CORDUser %s for Subscriber %s" % (self.nodetemplate.name, sub.name))
+        self.info("Created CORDUser %s for Subscriber %s" % (self.obj_name, sub.name))
 
     def update(self, obj):
         pass
diff --git a/xos/tosca/resources/cdnprefix.py b/xos/tosca/resources/cdnprefix.py
index 5faaca8..8daf7fb 100644
--- a/xos/tosca/resources/cdnprefix.py
+++ b/xos/tosca/resources/cdnprefix.py
@@ -16,7 +16,7 @@
     copyin_props = []
 
     def get_xos_args(self):
-        args = {"prefix": self.nodetemplate.name}
+        args = {"prefix": self.obj_name}
 
         cp_name = self.get_requirement("tosca.relationships.MemberOfContentProvider")
         if cp_name:
diff --git a/xos/tosca/resources/compute.py b/xos/tosca/resources/compute.py
index 37ba390..2af010a 100644
--- a/xos/tosca/resources/compute.py
+++ b/xos/tosca/resources/compute.py
@@ -39,7 +39,7 @@
         nodetemplate = self.nodetemplate
 
         if not name:
-            name = nodetemplate.name
+            name = self.obj_name
 
         args = {"name": name}
 
@@ -105,7 +105,7 @@
         if scalable:
             default_instances = scalable.get("default_instances",1)
             for i in range(0, default_instances):
-                name = "%s-%d" % (self.nodetemplate.name, i)
+                name = "%s-%d" % (self.obj_name, i)
                 existing_instances = Instance.objects.filter(name=name)
                 if existing_instances:
                     self.info("%s %s already exists" % (self.xos_model.__name__, name))
@@ -121,7 +121,7 @@
             existing_instances = []
             max_instances = scalable.get("max_instances",1)
             for i in range(0, max_instances):
-                name = "%s-%d" % (self.nodetemplate.name, i)
+                name = "%s-%d" % (self.obj_name, i)
                 existing_instances = existing_instances + list(Instance.objects.filter(name=name))
             return existing_instances
         else:
diff --git a/xos/tosca/resources/contentprovider.py b/xos/tosca/resources/contentprovider.py
index 06ca02e..66742ea 100644
--- a/xos/tosca/resources/contentprovider.py
+++ b/xos/tosca/resources/contentprovider.py
@@ -17,7 +17,7 @@
     def get_xos_args(self):
         sp_name = self.get_requirement("tosca.relationships.MemberOfServiceProvider", throw_exception=True)
         sp = self.get_xos_object(ServiceProvider, name=sp_name)
-        return {"name": self.nodetemplate.name,
+        return {"name": self.obj_name,
                 "serviceProvider": sp}
 
     def can_delete(self, obj):
diff --git a/xos/tosca/resources/dashboardview.py b/xos/tosca/resources/dashboardview.py
index 9f7687c..3bce58d 100644
--- a/xos/tosca/resources/dashboardview.py
+++ b/xos/tosca/resources/dashboardview.py
@@ -17,6 +17,14 @@
     def get_xos_args(self):
         return super(XOSDashboardView, self).get_xos_args()
 
+    def postprocess(self, obj):
+        for deployment_name in self.get_requirements("tosca.relationships.SupportsDeployment"):
+            deployment = self.get_xos_object(Deployment, deployment_name)
+            if not deployment in obj.deployments.all():
+                print "attaching dashboardview %s to deployment %s" % (obj, deployment)
+                obj.deployments.add(deployment)
+                obj.save()
+
     def can_delete(self, obj):
         return super(XOSDashboardView, self).can_delete(obj)
 
diff --git a/xos/tosca/resources/deployment.py b/xos/tosca/resources/deployment.py
index ed6734c..e5ab4b1 100644
--- a/xos/tosca/resources/deployment.py
+++ b/xos/tosca/resources/deployment.py
@@ -31,9 +31,8 @@
                 imageDep = ImageDeployments(deployment=obj, image=image)
                 imageDep.save()
 
-        # Be a little more lightweight with 'flavors'. Since we install flavors
-        # as a fixture rather than using TOSCA, we can just let the user
-        # use a comma-separated list.
+        # DEPRECATED - should switch to using a requirement, so tosca can do
+        # the topsort properly
 
         flavors = self.get_property("flavors")
         if flavors:
@@ -47,6 +46,15 @@
                     flavor.deployments.add(obj)
                     flavor.save()
 
+        # The new, right way
+        for flavor in self.get_requirements("tosca.relationships.SupportsFlavor"):
+            flavor = self.get_xos_object(Flavor, name=flavor)
+            if not flavor.deployments.filter(id=obj.id).exists():
+                self.info("Attached flavor %s to deployment %s" % (flavor, obj))
+                flavor.deployments.add(obj)
+                flavor.save()
+
+
         rolemap = ( ("tosca.relationships.AdminPrivilege", "admin"), )
         self.postprocess_privileges(DeploymentRole, DeploymentPrivilege, rolemap, obj, "deployment")
 
diff --git a/xos/tosca/resources/deploymentrole.py b/xos/tosca/resources/deploymentrole.py
new file mode 100644
index 0000000..4339026
--- /dev/null
+++ b/xos/tosca/resources/deploymentrole.py
@@ -0,0 +1,29 @@
+# note: this module named xossite.py instead of site.py due to conflict with
+#    /usr/lib/python2.7/site.py
+
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from core.models import User, Deployment, DeploymentRole
+
+from xosresource import XOSResource
+
+class XOSDeploymentRole(XOSResource):
+    provides = "tosca.nodes.DeploymentRole"
+    xos_model = DeploymentRole
+    name_field = "role"
+
+    def get_xos_args(self):
+        args = super(XOSDeploymentRole, self).get_xos_args()
+
+        return args
+
+    def delete(self, obj):
+        super(XOSDeploymentRole, self).delete(obj)
+
+
+
diff --git a/xos/tosca/resources/flavor.py b/xos/tosca/resources/flavor.py
new file mode 100644
index 0000000..f61ccad
--- /dev/null
+++ b/xos/tosca/resources/flavor.py
@@ -0,0 +1,37 @@
+# note: this module named xossite.py instead of site.py due to conflict with
+#    /usr/lib/python2.7/site.py
+
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from core.models import User, Deployment, Flavor
+
+from xosresource import XOSResource
+
+class XOSFlavor(XOSResource):
+    provides = "tosca.nodes.Flavor"
+    xos_model = Flavor
+    copyin_props = ["flavor"]
+
+    def get_xos_args(self):
+        args = super(XOSFlavor, self).get_xos_args()
+
+        # Support the default where the OpenStack flavor is the same as the
+        # flavor name
+        if "flavor" not in args:
+            args["flavor"] = args["name"]
+
+        return args
+
+    def delete(self, obj):
+        if obj.instance_set.exists():
+            self.info("Flavor %s has active instances; skipping delete" % obj.name)
+            return
+        super(XOSFlavor, self).delete(obj)
+
+
+
diff --git a/xos/tosca/resources/node.py b/xos/tosca/resources/node.py
index 99e756f..128aaed 100644
--- a/xos/tosca/resources/node.py
+++ b/xos/tosca/resources/node.py
@@ -14,7 +14,7 @@
     xos_model = Node
 
     def get_xos_args(self):
-        args = {"name": self.nodetemplate.name}
+        args = {"name": self.obj_name}
 
         site = None
         siteName = self.get_requirement("tosca.relationships.MemberOfSite", throw_exception=False)
@@ -44,9 +44,6 @@
             obj.save()
 
     def create(self):
-        nodetemplate = self.nodetemplate
-        sliceName = nodetemplate.name
-
         xos_args = self.get_xos_args()
 
         if not xos_args.get("site", None):
diff --git a/xos/tosca/resources/onosapp.py b/xos/tosca/resources/onosapp.py
index 72511b3..dccc8db 100644
--- a/xos/tosca/resources/onosapp.py
+++ b/xos/tosca/resources/onosapp.py
@@ -33,7 +33,7 @@
 
     def get_existing_objs(self):
         objs = ONOSApp.get_tenant_objects().all()
-        objs = [x for x in objs if x.name == self.nodetemplate.name]
+        objs = [x for x in objs if x.name == self.obj_name]
         return objs
 
     def set_tenant_attr(self, obj, prop_name, value):
diff --git a/xos/tosca/resources/originserver.py b/xos/tosca/resources/originserver.py
index 196ce2e..46cf87e 100644
--- a/xos/tosca/resources/originserver.py
+++ b/xos/tosca/resources/originserver.py
@@ -15,18 +15,18 @@
     name_field = "url"
     copyin_props = []
 
-    def nodetemplate_name_to_url(self):
-        url = self.nodetemplate.name
+    def obj_name_to_url(self):
+        url = self.obj_name
         if url.startswith("http_"):
             url = url[5:]
         return url
 
     def get_existing_objs(self):
-        url = self.nodetemplate_name_to_url()
+        url = self.obj_name_to_url()
         return self.xos_model.objects.filter(**{self.name_field: url})
 
     def get_xos_args(self):
-        url = self.nodetemplate_name_to_url()
+        url = self.obj_name_to_url()
         cp_name = self.get_requirement("tosca.relationships.MemberOfContentProvider", throw_exception=True)
         cp = self.get_xos_object(ContentProvider, name=cp_name)
         return {"url": url,
diff --git a/xos/tosca/resources/serviceprovider.py b/xos/tosca/resources/serviceprovider.py
index 8faec6c..2c9a167 100644
--- a/xos/tosca/resources/serviceprovider.py
+++ b/xos/tosca/resources/serviceprovider.py
@@ -17,7 +17,7 @@
     def get_xos_args(self):
         hpc_service_name = self.get_requirement("tosca.relationships.MemberOfService", throw_exception=True)
         hpc_service = self.get_xos_object(HpcService, name=hpc_service_name)
-        return {"name": self.nodetemplate.name,
+        return {"name": self.obj_name,
                 "hpcService": hpc_service}
 
     def can_delete(self, obj):
diff --git a/xos/tosca/resources/siterole.py b/xos/tosca/resources/siterole.py
new file mode 100644
index 0000000..abb1f0d
--- /dev/null
+++ b/xos/tosca/resources/siterole.py
@@ -0,0 +1,29 @@
+# note: this module named xossite.py instead of site.py due to conflict with
+#    /usr/lib/python2.7/site.py
+
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from core.models import User, Deployment, SiteRole
+
+from xosresource import XOSResource
+
+class XOSSiteRole(XOSResource):
+    provides = "tosca.nodes.SiteRole"
+    xos_model = SiteRole
+    name_field = "role"
+
+    def get_xos_args(self):
+        args = super(XOSSiteRole, self).get_xos_args()
+
+        return args
+
+    def delete(self, obj):
+        super(XOSSiteRole, self).delete(obj)
+
+
+
diff --git a/xos/tosca/resources/slice.py b/xos/tosca/resources/slice.py
index 48e5eb0..0add5ac 100644
--- a/xos/tosca/resources/slice.py
+++ b/xos/tosca/resources/slice.py
@@ -31,7 +31,7 @@
             default_image = self.get_xos_object(Image, name=default_image_name, throw_exception=True)
             args["default_image"] = default_image
 
-        default_flavor_name = self.get_property_default("default_flavor", None)
+        default_flavor_name = self.get_requirement("tosca.relationships.DefaultFlavor", throw_exception=False)
         if default_flavor_name:
             default_flavor = self.get_xos_object(Flavor, name=default_flavor_name, throw_exception=True)
             args["default_flavor"] = default_flavor
@@ -50,19 +50,6 @@
                     ("tosca.relationships.PIPrivilege", "pi"), ("tosca.relationships.TechPrivilege", "tech") )
         self.postprocess_privileges(SliceRole, SlicePrivilege, rolemap, obj, "slice")
 
-    def create(self):
-        nodetemplate = self.nodetemplate
-        sliceName = nodetemplate.name
-
-        xos_args = self.get_xos_args()
-        slice = Slice(**xos_args)
-        slice.caller = self.user
-        slice.save()
-
-        self.postprocess(slice)
-
-        self.info("Created Slice '%s' on Site '%s'" % (str(slice), str(slice.site)))
-
     def delete(self, obj):
         if obj.instances.exists():
             self.info("Slice %s has active instances; skipping delete" % obj.name)
diff --git a/xos/tosca/resources/slicerole.py b/xos/tosca/resources/slicerole.py
new file mode 100644
index 0000000..fc7d3f1
--- /dev/null
+++ b/xos/tosca/resources/slicerole.py
@@ -0,0 +1,29 @@
+# note: this module named xossite.py instead of site.py due to conflict with
+#    /usr/lib/python2.7/site.py
+
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from core.models import User, Deployment, SliceRole
+
+from xosresource import XOSResource
+
+class XOSSliceRole(XOSResource):
+    provides = "tosca.nodes.SliceRole"
+    xos_model = SliceRole
+    name_field = "role"
+
+    def get_xos_args(self):
+        args = super(XOSSliceRole, self).get_xos_args()
+
+        return args
+
+    def delete(self, obj):
+        super(XOSSliceRole, self).delete(obj)
+
+
+
diff --git a/xos/tosca/resources/tag.py b/xos/tosca/resources/tag.py
new file mode 100644
index 0000000..001cba8
--- /dev/null
+++ b/xos/tosca/resources/tag.py
@@ -0,0 +1,57 @@
+import importlib
+import os
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+from django.contrib.contenttypes.models import ContentType
+
+from core.models import Tag, Service
+
+from xosresource import XOSResource
+
+class XOSTag(XOSResource):
+    provides = "tosca.nodes.Tag"
+    xos_model = Tag
+    name_field = None
+    copyin_props = ("name", "value")
+
+    def get_xos_args(self, throw_exception=True):
+        args = super(XOSTag, self).get_xos_args()
+
+        # Find the Tosca object that this Tag is pointing to, and return its
+        # content_type and object_id, which will be used in the GenericForeignKey
+        # django relation.
+
+        target_name = self.get_requirement("tosca.relationships.TagsObject", throw_exception=throw_exception)
+        if target_name:
+            target_model = self.engine.name_to_xos_model(self.user, target_name)
+            args["content_type"] = ContentType.objects.get_for_model(target_model)
+            args["object_id"] = target_model.id
+
+        service_name = self.get_requirement("tosca.relationships.MemberOfService", throw_exception=throw_exception)
+        if service_name:
+            args["service"] = self.get_xos_object(Service, name=service_name)
+
+        # To uniquely identify a Tag, we must know the object that it is attached
+        # to as well as the name of the Tag.
+
+        if ("content_type" not in args) or ("object_id" not in args) or ("name" not in args):
+           if throw_exception:
+               raise Exception("Tag must specify TagsObject requirement and Name property")
+
+        return args
+
+    def get_existing_objs(self):
+        args = self.get_xos_args(throw_exception=True)
+
+        return Tag.objects.filter(content_type=args["content_type"],
+                                  object_id=args["object_id"],
+                                  name=args["name"])
+
+    def postprocess(self, obj):
+        pass
+
+    def can_delete(self, obj):
+        return super(XOSTag, self).can_delete(obj)
+
diff --git a/xos/tosca/resources/tenantrole.py b/xos/tosca/resources/tenantrole.py
new file mode 100644
index 0000000..316a5a3
--- /dev/null
+++ b/xos/tosca/resources/tenantrole.py
@@ -0,0 +1,29 @@
+# note: this module named xossite.py instead of site.py due to conflict with
+#    /usr/lib/python2.7/site.py
+
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from core.models import User, Deployment, TenantRole
+
+from xosresource import XOSResource
+
+class XOSTenantRole(XOSResource):
+    provides = "tosca.nodes.TenantRole"
+    xos_model = TenantRole
+    name_field = "role"
+
+    def get_xos_args(self):
+        args = super(XOSTenantRole, self).get_xos_args()
+
+        return args
+
+    def delete(self, obj):
+        super(XOSTenantRole, self).delete(obj)
+
+
+
diff --git a/xos/tosca/resources/user.py b/xos/tosca/resources/user.py
index 8587c89..55c0423 100644
--- a/xos/tosca/resources/user.py
+++ b/xos/tosca/resources/user.py
@@ -25,7 +25,7 @@
         return args
 
     def get_existing_objs(self):
-        return self.xos_model.objects.filter(email = self.nodetemplate.name)
+        return self.xos_model.objects.filter(email = self.obj_name)
 
     def postprocess(self, obj):
         rolemap = ( ("tosca.relationships.AdminPrivilege", "admin"), ("tosca.relationships.AccessPrivilege", "access"),
@@ -62,12 +62,12 @@
                         udv.save()
 
     def create(self):
-        nodetemplate = self.nodetemplate
-
         xos_args = self.get_xos_args()
 
         if not xos_args.get("site",None):
              raise Exception("Site name must be specified when creating user")
+        if ("firstname" not in xos_args) or ("lastname" not in xos_args):
+             raise Exception("firstname and lastname must be specified when creating user")
 
         user = User(**xos_args)
         user.save()
diff --git a/xos/tosca/resources/vtnservice.py b/xos/tosca/resources/vtnservice.py
index f0940d6..2a5738f 100644
--- a/xos/tosca/resources/vtnservice.py
+++ b/xos/tosca/resources/vtnservice.py
@@ -12,5 +12,4 @@
 class XOSVTNService(XOSService):
     provides = "tosca.nodes.VTNService"
     xos_model = VTNService
-    copyin_props = ["view_url", "icon_url", "enabled", "published", "public_key", "versionNumber", 'privateGatewayMac', 'localManagementIp', 'ovsdbPort', 'sshPort', 'sshUser', 'sshKeyFile', 'mgmtSubnetBits']
-
+    copyin_props = ["view_url", "icon_url", "enabled", "published", "public_key", "versionNumber", 'privateGatewayMac', 'localManagementIp', 'ovsdbPort', 'sshPort', 'sshUser', 'sshKeyFile', 'mgmtSubnetBits', 'xosEndpoint', 'xosUser', 'xosPassword']
diff --git a/xos/tosca/resources/xosresource.py b/xos/tosca/resources/xosresource.py
index cc4672b..e70cfa9 100644
--- a/xos/tosca/resources/xosresource.py
+++ b/xos/tosca/resources/xosresource.py
@@ -19,6 +19,17 @@
         self.nodetemplate = nodetemplate
         self.engine = engine
 
+    @property
+    def full_name(self):
+        return self.nodetemplate.name
+
+    @property
+    def obj_name(self):
+        if "#" in self.nodetemplate.name:
+            return self.nodetemplate.name.split("#",1)[1]
+        else:
+            return self.nodetemplate.name
+
     def get_all_required_node_names(self):
         results = []
         for reqs in self.nodetemplate.requirements:
@@ -38,7 +49,7 @@
                     results.append(v["node"])
 
         if (not results) and throw_exception:
-            raise Exception("Failed to find requirement in %s using relationship %s" % (self.nodetemplate.name, relationship_name))
+            raise Exception("Failed to find requirement in %s using relationship %s" % (self.full_name, relationship_name))
 
         return results
 
@@ -75,7 +86,7 @@
         return objs[0]
 
     def get_existing_objs(self):
-        return self.xos_model.objects.filter(**{self.name_field: self.nodetemplate.name})
+        return self.xos_model.objects.filter(**{self.name_field: self.obj_name})
 
     def get_model_class_name(self):
         return self.xos_model.__name__
@@ -84,19 +95,19 @@
         existing_objs = self.get_existing_objs()
         if existing_objs:
             if self.get_property_default("no-update", False):
-                self.info("%s %s already exists. Skipping update due to 'no-update' property" % (self.get_model_class_name(), self.nodetemplate.name))
+                self.info("%s:%s (%s) already exists. Skipping update due to 'no-update' property" % (self.get_model_class_name(), self.obj_name, self.full_name))
             else:
-                self.info("%s %s already exists" % (self.get_model_class_name(), self.nodetemplate.name))
+                self.info("%s:%s (%s) already exists" % (self.get_model_class_name(), self.obj_name, self.full_name))
                 self.update(existing_objs[0])
         else:
             if self.get_property_default("no-create", False):
-                self.info("%s %s does not exist, but 'no-create' is specified" % (self.get_model_class_name(), self.nodetemplate.name))
+                self.info("%s:%s (%s) does not exist, but 'no-create' is specified" % (self.get_model_class_name(), self.obj_name, self.full_name))
             else:
                 self.create()
 
     def can_delete(self, obj):
         if self.get_property_default("no-delete",False):
-            self.info("%s %s is marked 'no-delete'. Skipping delete." % (self.get_model_class_name(), self.nodetemplate.name))
+            self.info("%s:%s %s is marked 'no-delete'. Skipping delete." % (self.get_model_class_name(), self.obj_name, self.full_name))
             return False
         return True
 
@@ -170,7 +181,7 @@
         args = {}
 
         if self.name_field:
-            args[self.name_field] = self.nodetemplate.name
+            args[self.name_field] = self.obj_name
 
         # copy simple string properties from the template into the arguments
         for prop in self.copyin_props:
@@ -186,7 +197,8 @@
     def create(self):
         xos_args = self.get_xos_args()
         xos_obj = self.xos_model(**xos_args)
-        xos_obj.caller = self.user
+        if self.user:
+            xos_obj.caller = self.user
         xos_obj.save()
 
         self.info("Created %s '%s'" % (self.xos_model.__name__,str(xos_obj)))
diff --git a/xos/tosca/resources/xossite.py b/xos/tosca/resources/xossite.py
index 0db2705..9b03bc5 100644
--- a/xos/tosca/resources/xossite.py
+++ b/xos/tosca/resources/xossite.py
@@ -19,9 +19,9 @@
     def get_xos_args(self):
         display_name = self.get_property("display_name")
         if not display_name:
-            display_name = self.nodetemplate.name
+            display_name = self.obj_name
 
-        args = {"login_base": self.nodetemplate.name,
+        args = {"login_base": self.obj_name,
                 "name": display_name}
 
         # copy simple string properties from the template into the arguments
@@ -33,7 +33,7 @@
         return args
 
     def get_existing_objs(self):
-        return self.xos_model.objects.filter(login_base = self.nodetemplate.name)
+        return self.xos_model.objects.filter(login_base = self.obj_name)
 
     def postprocess(self, obj):
         results = []
@@ -44,19 +44,20 @@
                     deployment = self.get_xos_object(Deployment, name=deployment_name)
 
                     controller_name = None
-                    for sd_req in v["requirements"]:
+                    for sd_req in v.get("requirements", []):
                         for (sd_req_k, sd_req_v) in sd_req.items():
                             if sd_req_v["relationship"] == "tosca.relationships.UsesController":
                                 controller_name = sd_req_v["node"]
-                    if not controller_name:
-                        raise Exception("Controller must be specified in SiteDeployment relationship")
-
-                    controller = self.get_xos_object(Controller, name=controller_name, throw_exception=True)
+                    if controller_name:
+                        controller = self.get_xos_object(Controller, name=controller_name, throw_exception=True)
+                    else:
+                        controller = None
+                        # raise Exception("Controller must be specified in SiteDeployment relationship")
 
                     existing_sitedeps = SiteDeployment.objects.filter(deployment=deployment, site=obj)
                     if existing_sitedeps:
                         sd = existing_sitedeps[0]
-                        if sd.controller != controller:
+                        if (sd.controller != controller) and (controller != None):
                             sd.controller = controller
                             sd.save()
                             self.info("SiteDeployment from %s to %s updated controller" % (str(obj), str(deployment)))
@@ -67,20 +68,6 @@
                         sitedep.save()
                         self.info("Created SiteDeployment from %s to %s" % (str(obj), str(deployment)))
 
-    def create(self):
-        nodetemplate = self.nodetemplate
-        siteName = nodetemplate.name
-
-        xos_args = self.get_xos_args()
-
-        site = Site(**xos_args)
-        site.caller = self.user
-        site.save()
-
-        self.postprocess(site)
-
-        self.info("Created Site '%s'" % (str(site), ))
-
     def delete(self, obj):
         if obj.slices.exists():
             self.info("Site %s has active slices; skipping delete" % obj.name)
diff --git a/xos/tosca/run.py b/xos/tosca/run.py
index 591582b..58dc22b 100644
--- a/xos/tosca/run.py
+++ b/xos/tosca/run.py
@@ -25,7 +25,10 @@
     username = sys.argv[1]
     template_name = sys.argv[2]
 
-    u = User.objects.get(email=username)
+    if username.lower()=="none":
+        u=None
+    else:
+        u = User.objects.get(email=username)
 
     xt = XOSTosca(file(template_name).read(), parent_dir=currentdir, log_to_console=True)
     xt.execute(u)
diff --git a/xos/tosca/samples/helloworld-chain.yaml b/xos/tosca/samples/helloworld-chain.yaml
deleted file mode 100644
index 8b49106..0000000
--- a/xos/tosca/samples/helloworld-chain.yaml
+++ /dev/null
@@ -1,76 +0,0 @@
-tosca_definitions_version: tosca_simple_yaml_1_0
-
-description: Two services "service_one" and "service_two" with a tenancy relationship.
-
-imports:
-   - custom_types/xos.yaml
-
-topology_template:
-  node_templates:
-
-    Private-Indirect:
-      type: tosca.nodes.NetworkTemplate
-      properties:
-          access: indirect
-
-    mysite:
-      type: tosca.nodes.Site
-
-    trusty-server-multi-nic:
-      type: tosca.nodes.Image
-
-    service_vsg:
-      type: tosca.nodes.VSGService
-      requirements:
-          - helloworld_tenant:
-              node: service_helloworld
-              relationship: tosca.relationships.TenantOfService
-
-    service_helloworld:
-      type: tosca.nodes.Service
-      properties:
-          kind: helloworldservice_complete
-          view_url: /admin/helloworldservice_complete/helloworldservicecomplete/$id$/
-
-    tenant_helloworld:
-       type: tosca.nodes.Tenant
-       properties:
-           kind: helloworldservice_complete
-           service_specific_attribute: "{\"display_message\": \"Hello World from Tosca\"}"
-           model: services.helloworldservice_complete.models.HelloWorldTenantComplete
-       requirements:
-           - provider_service:
-               node: service_helloworld
-               relationship: tosca.relationships.MemberOfService
-
-
-    mysite_helloworld:
-      type: tosca.nodes.Slice
-      requirements:
-          - service:
-              node: service_helloworld
-              relationship: tosca.relationships.MemberOfService
-          - site:
-              node: mysite
-              relationship: tosca.relationships.MemberOfSite
-          - default_image:
-                node: trusty-server-multi-nic
-                relationship: tosca.relationships.DefaultImage
-      properties:
-          default_flavor: m1.small
-
-    helloworld_access:
-      type: tosca.nodes.network.Network
-      properties:
-          ip_version: 4
-      requirements:
-          - network_template:
-              node: Private-Indirect
-              relationship: tosca.relationships.UsesNetworkTemplate
-          - owner:
-              node: mysite_helloworld
-              relationship: tosca.relationships.MemberOfSlice
-          - connection:
-              node: mysite_helloworld
-              relationship: tosca.relationships.ConnectsToSlice
-
diff --git a/xos/tosca/samples/slice_default_image.yaml b/xos/tosca/samples/slice_default_image.yaml
index 91b95c7..ff63373 100644
--- a/xos/tosca/samples/slice_default_image.yaml
+++ b/xos/tosca/samples/slice_default_image.yaml
@@ -16,6 +16,9 @@
     trusty-server-multi-nic:
       type: tosca.nodes.Image
 
+    m1.small:
+      type: tosca.nodes.Flavor
+
     mysite_test1:
       type: tosca.nodes.Slice
       requirements:
@@ -25,6 +28,6 @@
           - default_image:
                 node: trusty-server-multi-nic
                 relationship: tosca.relationships.DefaultImage
-      properties:
-          default_flavor: m1.small
-
+          -default_flavor:
+                node: m1.small
+                relationship: tosca.relationships.DefaultFlavor
diff --git a/xos/tosca/samples/slicetag.yaml b/xos/tosca/samples/slicetag.yaml
new file mode 100644
index 0000000..ec064e3
--- /dev/null
+++ b/xos/tosca/samples/slicetag.yaml
@@ -0,0 +1,35 @@
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+description: Setup CORD-related services -- vOLT, vCPE, vBNG.
+
+imports:
+   - custom_types/xos.yaml
+
+topology_template:
+  node_templates:
+    mysite_vsg:
+      type: tosca.nodes.Slice
+      properties:
+          no-create: True
+          no-delete: True
+          no-update: True
+
+    service_vsg:
+      type: tosca.nodes.Service
+      properties:
+          no-create: True
+          no-delete: True
+          no-update: True
+
+    mysite_vsg_foobar_tag:
+      type: tosca.nodes.Tag
+      properties:
+          name: foobar
+          value: xyz
+      requirements:
+          - target:
+              node: mysite_vsg
+              relationship: tosca.relationships.TagsObject
+          - service:
+              node: service_vsg
+              relationship: tosca.relationships.MemberOfService
diff --git a/xos/xos/settings.py b/xos/xos/settings.py
index ad08777..61f4ac2 100644
--- a/xos/xos/settings.py
+++ b/xos/xos/settings.py
@@ -175,11 +175,11 @@
     'core',
     'services.hpc',
     'services.cord',
-    'services.helloworldservice_complete',
     'services.onos',
     'services.ceilometer',
     'services.requestrouter',
     'services.syndicate_storage',
+    'services.openvpn',
     'services.vtr',
     'services.vrouter',
     'services.vtn',
diff --git a/xos/xos/urls.py b/xos/xos/urls.py
index 1bc3885..570b768 100644
--- a/xos/xos/urls.py
+++ b/xos/xos/urls.py
@@ -9,7 +9,6 @@
 
 from core.views.legacyapi import LegacyXMLRPC
 from core.views.serviceGraph import ServiceGridView, ServiceGraphView
-from services.helloworld.view import *
 from core.models import *
 from rest_framework import generics
 from core.dashboard.sites import SitePlus
@@ -27,7 +26,6 @@
 urlpatterns = patterns('',
     # Examples:
     url(r'^observer', 'core.views.observer.Observer', name='observer'),
-    url(r'^helloworld', HelloWorldView.as_view(), name='helloWorld'),
     url(r'^serviceGrid', ServiceGridView.as_view(), name='serviceGrid'),
     url(r'^serviceGraph.png', ServiceGraphView.as_view(), name='serviceGraph'),
     url(r'^hpcConfig', 'core.views.hpc_config.HpcConfig', name='hpcConfig'),
diff --git a/xos/xos/wsgi.py b/xos/xos/wsgi.py
index 9b70770..55c6c1a 100644
--- a/xos/xos/wsgi.py
+++ b/xos/xos/wsgi.py
@@ -28,5 +28,3 @@
 application = get_wsgi_application()
 
 # Apply WSGI middleware here.
-# from helloworld.wsgi import HelloWorldApplication
-# application = HelloWorldApplication(application)