Merge branch 'master' of github.com:jermowery/xos into AddVPNService
diff --git a/containers/synchronizer/Makefile b/containers/synchronizer/Makefile
index 352616a..733e7b8 100644
--- a/containers/synchronizer/Makefile
+++ b/containers/synchronizer/Makefile
@@ -1,6 +1,6 @@
IMAGE_NAME:=xosproject/xos-synchronizer-openstack
CONTAINER_NAME:=xos-synchronizer
-NO_DOCKER_CACHE?=false
+NO_DOCKER_CACHE?=true
.PHONY: build
build: ; sudo docker build --no-cache=${NO_DOCKER_CACHE} --rm -t ${IMAGE_NAME} .
diff --git a/containers/xos/Dockerfile.devel b/containers/xos/Dockerfile.devel
index 7bed082..55dcdee 100644
--- a/containers/xos/Dockerfile.devel
+++ b/containers/xos/Dockerfile.devel
@@ -34,7 +34,8 @@
python-novaclient \
python-neutronclient \
python-glanceclient \
- python-ceilometerclient
+ python-ceilometerclient \
+ openvpn
RUN pip install \
django==1.7 \
@@ -92,3 +93,14 @@
# Define default command.
CMD update-ca-certificates && python /opt/xos/manage.py runserver 0.0.0.0:8000 --insecure --makemigrations
+
+# for OpenVPN
+RUN mkdir -p /opt/openvpn
+RUN chmod 777 /opt/openvpn
+RUN git clone https://github.com/OpenVPN/easy-rsa.git /opt/openvpn
+RUN git -C /opt/openvpn pull origin master
+RUN echo 'set_var EASYRSA "/opt/openvpn/easyrsa3"' | tee /opt/openvpn/vars
+RUN echo 'set_var EASYRSA_BATCH "true"' | tee -a /opt/openvpn/vars
+RUN /opt/openvpn/easyrsa3/easyrsa --pki-dir="/opt/openvpn/init_pki" --vars=/opt/openvpn/vars init-pki
+RUN /opt/openvpn/easyrsa3/easyrsa --pki-dir="/opt/openvpn/init_pki" --vars=/opt/openvpn/vars gen-dh
+RUN chmod 777 /opt/openvpn/init_pki/dh.pem
diff --git a/containers/xos/Makefile b/containers/xos/Makefile
index 0ba043d..d5cc57b 100644
--- a/containers/xos/Makefile
+++ b/containers/xos/Makefile
@@ -3,7 +3,7 @@
TOSCA_CONFIG_PATH:=/opt/xos/configurations/opencloud/opencloud.yaml
XOS_GIT_REPO?=git://github.com/open-cloud/xos.git
XOS_GIT_BRANCH?=master
-NO_DOCKER_CACHE?=false
+NO_DOCKER_CACHE?=true
.PHONY: build
build: ; sudo docker build --no-cache=${NO_DOCKER_CACHE} --rm -t ${IMAGE_NAME} .
diff --git a/views/ngXosViews/vpnDashboard/.bowerrc b/views/ngXosViews/vpnDashboard/.bowerrc
new file mode 100644
index 0000000..e491038
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "src/vendor/"
+}
\ No newline at end of file
diff --git a/views/ngXosViews/vpnDashboard/.eslintrc b/views/ngXosViews/vpnDashboard/.eslintrc
new file mode 100644
index 0000000..c852748
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/.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/vpnDashboard/.gitignore b/views/ngXosViews/vpnDashboard/.gitignore
new file mode 100644
index 0000000..567aee4
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/.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/vpnDashboard/bower.json b/views/ngXosViews/vpnDashboard/bower.json
new file mode 100644
index 0000000..b7c354e
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/bower.json
@@ -0,0 +1,29 @@
+{
+ "name": "xos-vpnDashboard",
+ "version": "0.0.0",
+ "authors": [
+ "Jeremy Mowery <jermowery@email.arizona.edu>"
+ ],
+ "description": "The vpnDashboard view",
+ "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/vpnDashboard/env/default.js b/views/ngXosViews/vpnDashboard/env/default.js
new file mode 100644
index 0000000..5b198ec
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/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/vpnDashboard/gulp/build.js b/views/ngXosViews/vpnDashboard/gulp/build.js
new file mode 100644
index 0000000..1ecffbd
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/gulp/build.js
@@ -0,0 +1,119 @@
+'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 TEMPLATE_FOOTER = `}]);
+angular.module('xos.vpnDashboard').run(function($location){$location.path('/')});
+angular.bootstrap(angular.element('#xosVpnDashboard'), ['xos.vpnDashboard']);`;
+
+module.exports = function(options){
+
+ // delete previous builded file
+ gulp.task('clean', function(){
+ return del(
+ [options.dashboards + 'xosVpnDashboard.html'],
+ {force: true}
+ );
+ });
+
+ // compile and minify scripts
+ gulp.task('scripts', function() {
+ return gulp.src([
+ options.tmp + '**/*.js'
+ ])
+ .pipe(ngAnnotate())
+ .pipe(angularFilesort())
+ .pipe(concat('xosVpnDashboard.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.vpnDashboard',
+ 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/, ''))
+ // injecting minified files
+ .pipe(
+ inject(
+ gulp.src([
+ options.static + 'js/vendor/xosVpnDashboardVendor.js',
+ options.static + 'js/xosVpnDashboard.js'
+ ]),
+ {ignorePath: '/../../../xos/core/xoslib'}
+ )
+ )
+ .pipe(rename('xosVpnDashboard.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('xosVpnDashboardVendor.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(
+ 'templates',
+ 'babel',
+ 'scripts',
+ 'wiredep',
+ 'copyHtml',
+ 'cleanTmp'
+ );
+ });
+};
\ No newline at end of file
diff --git a/views/ngXosViews/vpnDashboard/gulp/server.js b/views/ngXosViews/vpnDashboard/gulp/server.js
new file mode 100644
index 0000000..7605294
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/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/vpnDashboard/gulpfile.js b/views/ngXosViews/vpnDashboard/gulpfile.js
new file mode 100644
index 0000000..b2cdab8
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/gulpfile.js
@@ -0,0 +1,24 @@
+'use strict';
+
+var gulp = require('gulp');
+var wrench = require('wrench');
+
+var options = {
+ src: 'src/',
+ 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/vpnDashboard/karma.conf.js b/views/ngXosViews/vpnDashboard/karma.conf.js
new file mode 100644
index 0000000..83d3f63
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/karma.conf.js
@@ -0,0 +1,88 @@
+// 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([
+ '../../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/vpnDashboard/package.json b/views/ngXosViews/vpnDashboard/package.json
new file mode 100644
index 0000000..a4d9e6f
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "xos-vpnDashboard",
+ "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/vpnDashboard/spec/sample.test.js b/views/ngXosViews/vpnDashboard/spec/sample.test.js
new file mode 100644
index 0000000..ad20c13
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/spec/sample.test.js
@@ -0,0 +1,37 @@
+'use strict';
+
+describe('The User List', () => {
+
+ var scope, element, isolatedScope, httpBackend;
+
+ beforeEach(module('xos.vpnDashboard'));
+ 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/vpnDashboard/src/css/dev.css b/views/ngXosViews/vpnDashboard/src/css/dev.css
new file mode 100644
index 0000000..4c504c9
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/src/css/dev.css
@@ -0,0 +1,14 @@
+#xosVpnDashboard{
+ width: 70%;
+ margin: auto;
+}
+.row {
+ display: table-row;
+}
+.cell {
+ display: table-cell;
+ padding: 5px;
+}
+.header {
+ font-weight: bold;
+}
diff --git a/views/ngXosViews/vpnDashboard/src/index.html b/views/ngXosViews/vpnDashboard/src/index.html
new file mode 100644
index 0000000..b19740a
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/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/dev.css">
+<!-- endinject -->
+
+<div ng-app="xos.vpnDashboard" id="xosVpnDashboard">
+ <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/vpnDashboard/src/js/main.js b/views/ngXosViews/vpnDashboard/src/js/main.js
new file mode 100644
index 0000000..a93f720
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/src/js/main.js
@@ -0,0 +1,61 @@
+'use strict';
+
+angular.module('xos.vpnDashboard', [
+ 'ngResource',
+ 'ngCookies',
+ 'ngLodash',
+ 'ui.router',
+ 'xos.helpers'
+])
+.config(($stateProvider) => {
+ $stateProvider
+ .state('vpnList', {
+ url: '/',
+ template: '<vpn-list></vpn-list>'
+ });
+})
+.config(($compileProvider) => {
+ $compileProvider.aHrefSanitizationWhitelist(
+ /^\s*(https?|ftp|mailto|tel|file|blob):/);
+})
+.service('Vpn', function($http, $q){
+
+ this.getVpnTenants = () => {
+ let deferred = $q.defer();
+
+ $http.get('/xoslib/vpntenant/')
+ .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/vpn-list.tpl.html',
+ controller: function(Vpn){
+ Vpn.getVpnTenants()
+ .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/vpnDashboard/src/templates/vpn-list.tpl.html b/views/ngXosViews/vpnDashboard/src/templates/vpn-list.tpl.html
new file mode 100644
index 0000000..a8fa8f3
--- /dev/null
+++ b/views/ngXosViews/vpnDashboard/src/templates/vpn-list.tpl.html
@@ -0,0 +1,19 @@
+<div style="display: table;">
+ <div class="row">
+ <h1 class="cell">VPN List</h1>
+ </div>
+ <div class="row">
+ <div class="cell header">ID</div>
+ <div class="cell header">VPN Network</div>
+ <div class="cell header">VPN Subnet</div>
+ <div class="cell header">Script Link</div>
+ </div>
+ <div class="row" ng-repeat="vpn in vm.vpns">
+ <div class="cell">{{ vpn.id }}</div>
+ <div class="cell">{{ vpn.server_network }}</div>
+ <div class="cell">{{ vpn.vpn_subnet }}</div>
+ <div class="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/configurations/common/Dockerfile.common b/xos/configurations/common/Dockerfile.common
index aedd245..1189808 100644
--- a/xos/configurations/common/Dockerfile.common
+++ b/xos/configurations/common/Dockerfile.common
@@ -28,7 +28,8 @@
python-dev \
libyaml-dev \
pkg-config \
- python-pycurl
+ python-pycurl \
+ openvpn
RUN pip install django==1.7
RUN pip install djangorestframework==2.4.4
@@ -131,6 +132,19 @@
RUN pip install python-dateutil
RUN bash /opt/xos/tosca/install_tosca.sh
+# for OpenVPN
+RUN mkdir -p /opt/openvpn
+RUN git clone https://github.com/OpenVPN/easy-rsa.git /opt/openvpn
+RUN git -C /opt/openvpn pull origin master
+RUN cp /opt/xos/services/vpn/vars /opt/openvpn/vars
+RUN source /opt/openvpn/vars
+RUN /opt/openvpn/clean-all
+RUN /opt/openvpn/build-ca --batch
+RUN /opt/openvpn/build-key-server --batch server
+RUN /opt/openvpn/build-dh
+RUN chmod 777 /opt/openvpn/keys/server.key
+RUN chmod 777 /opt/openvpn/keys/dh2048.pem
+
EXPOSE 8000
# Set environment variables.
diff --git a/xos/configurations/devel/Makefile b/xos/configurations/devel/Makefile
index 1e650f3..402309b 100644
--- a/xos/configurations/devel/Makefile
+++ b/xos/configurations/devel/Makefile
@@ -46,3 +46,8 @@
rebuild_synchronizer:
make -C ../../../containers/synchronizer
+
+cleanup_docker: rm
+ sudo docker rm -v $(docker ps -a -q -f status=exited) || true
+ sudo docker rmi $(docker images -f "dangling=true" -q) || 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/devel/docker-compose.yml b/xos/configurations/devel/docker-compose.yml
index 9ef6fc7..d7d3b2a 100644
--- a/xos/configurations/devel/docker-compose.yml
+++ b/xos/configurations/devel/docker-compose.yml
@@ -17,6 +17,32 @@
- ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
- ./images:/opt/xos/images:ro
+xos_synchronizer_vpn:
+ image: xosproject/xos-synchronizer-openstack
+ command: bash -c "sleep 120 ; python /opt/xos/synchronizers/vpn/vpn-synchronizer.py -C /opt/xos/synchronizers/vpn/vpn_config"
+ labels:
+ org.xosproject.kind: synchronizer
+ org.xosproject.target: vpn
+ links:
+ - xos_db
+ extra_hosts:
+ - ctl:${MYIP}
+ volumes:
+ - ../setup/id_rsa:/opt/xos/synchronizers/vpn/vpn_private_key:ro # private key
+ volumes_from:
+ - xos_synchronizer_vpn_data:rw
+
+xos_synchronizer_vpn_data:
+ image: xosproject/xos-synchronizer-openstack
+ labels:
+ org.xosproject.kind: synchronizer
+ org.xosproject.target: vpn
+ links:
+ - xos_db
+ extra_hosts:
+ - ctl:${MYIP}
+ volumes:
+ - /opt/openvpn
# FUTURE
#xos_swarm_synchronizer:
# image: xosproject/xos-swarm-synchronizer
@@ -34,3 +60,5 @@
volumes:
- ../setup:/root/setup:ro
- ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
+ volumes_from:
+ - xos_synchronizer_vpn_data:rw
diff --git a/xos/configurations/frontend/Makefile b/xos/configurations/frontend/Makefile
index 2b46441..3de63b8 100644
--- a/xos/configurations/frontend/Makefile
+++ b/xos/configurations/frontend/Makefile
@@ -32,3 +32,8 @@
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
+
+cleanup_docker: rm
+ sudo docker rm -v $(docker ps -a -q -f status=exited) || true
+ sudo docker rmi $(docker images -f "dangling=true" -q) || 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/core/admin.py b/xos/core/admin.py
index 5169f6a..8679cbd 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -1,29 +1,28 @@
-from core.models import Site
-from core.models import *
-from openstack.manager import OpenStackManager
+import threading
+from cgi import escape as html_escape
-from django.contrib import admin
-from django.contrib.auth.models import Group
+from core.models import *
+from core.models import Site
from django import forms
-from django.utils.safestring import mark_safe
+from django.contrib import admin, messages
+from django.contrib.admin.widgets import (AdminTextareaWidget,
+ FilteredSelectMultiple)
from django.contrib.auth.admin import UserAdmin
-from django.contrib.admin.widgets import FilteredSelectMultiple, AdminTextareaWidget
-from django.contrib.auth.forms import ReadOnlyPasswordHashField, AdminPasswordChangeForm
+from django.contrib.auth.forms import (AdminPasswordChangeForm,
+ ReadOnlyPasswordHashField)
+from django.contrib.auth.models import Group
from django.contrib.auth.signals import user_logged_in
-from django.utils import timezone
from django.contrib.contenttypes import generic
-from suit.widgets import LinkedSelect
-from django.core.exceptions import PermissionDenied
-from django.core.urlresolvers import reverse, resolve, NoReverseMatch
+from django.core.exceptions import PermissionDenied, ValidationError
+from django.core.urlresolvers import NoReverseMatch, resolve, reverse
+from django.forms.utils import flatatt, to_current_timezone
+from django.utils import timezone
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.html import conditional_escape, format_html
+from django.utils.safestring import mark_safe
from django.utils.text import capfirst
-from django.forms.utils import flatatt, to_current_timezone
-from django.core.exceptions import PermissionDenied, ValidationError
-from cgi import escape as html_escape
-from django.contrib import messages
-
-import threading
+from openstack.manager import OpenStackManager
+from suit.widgets import LinkedSelect
# thread locals necessary to work around a django-suit issue
_thread_locals = threading.local()
@@ -488,7 +487,7 @@
class SiteHostsUsersInline(SiteInline):
def queryset(self, request):
- return Site.select_by_user(request.user).filter(hosts_users=True)
+ return Site.select_by_user(request.user).filter(hosts_users=True)
class UserInline(XOSTabularInline):
model = User
@@ -565,7 +564,7 @@
kwargs['queryset'] = Service.select_by_user(request.user)
if db_field.name == 'user':
kwargs['queryset'] = User.select_by_user(request.user)
- return super(ServicePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
+ return super(ServicePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
def queryset(self, request):
return ServicePrivilege.select_by_user(request.user)
@@ -817,12 +816,12 @@
def save_model(self, request, obj, form, change):
# update openstack connection to use this site/tenant
obj.save_by_user(request.user)
-
+
def delete_model(self, request, obj):
obj.delete_by_user(request.user)
def queryset(self, request):
- return Controller.select_by_user(request.user)
+ return Controller.select_by_user(request.user)
@property
def suit_form_tabs(self):
@@ -893,6 +892,20 @@
('tenantrootprivileges','Privileges')
)
+class TenantRoleAdmin(XOSBaseAdmin):
+ model = TenantRole
+ fields = ('role',)
+
+class TenantPrivilegeInline(XOSTabularInline):
+ 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']
@@ -1015,11 +1028,11 @@
def save_model(self, request, obj, form, change):
# update openstack connection to use this site/tenant
- obj.save_by_user(request.user)
+ obj.save_by_user(request.user)
def delete_model(self, request, obj):
obj.delete_by_user(request.user)
-
+
class SitePrivilegeAdmin(XOSBaseAdmin):
fieldList = ['backend_status_text', 'user', 'site', 'role']
@@ -1117,7 +1130,7 @@
('slicenetworks','Networks'),
('sliceprivileges','Privileges'),
('instances','Instances'),
- #('reservations','Reservations'),
+ #('reservations','Reservations'),
('tags','Tags'),
]
@@ -1126,7 +1139,7 @@
tabs.append( ('admin-only', 'Admin-Only') )
return tabs
-
+
def add_view(self, request, form_url='', extra_context=None):
# Ugly hack for CORD
self.inlines = self.normal_inlines
@@ -1223,7 +1236,7 @@
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'slice':
kwargs['queryset'] = Slice.select_by_user(request.user)
-
+
if db_field.name == 'user':
kwargs['queryset'] = User.select_by_user(request.user)
@@ -1637,12 +1650,12 @@
login_details_fields.remove('profile')
#if len(request.user.siteprivileges.filter(role__role = 'pi')) > 0:
# only admins and pis can change a user's site
- # self.readonly_fields = ('backend_status_text', 'site')
+ # self.readonly_fields = ('backend_status_text', 'site')
self.fieldsets = (
('Login Details', {'fields': login_details_fields, 'classes':['suit-tab suit-tab-general']}),
('Contact Information', {'fields': self.fieldListContactInfo, 'classes':['suit-tab suit-tab-contact']}),
)
- return super(UserAdmin, self).get_form(request, obj, **kwargs)
+ return super(UserAdmin, self).get_form(request, obj, **kwargs)
class ControllerDashboardViewInline(XOSTabularInline):
model = ControllerDashboardView
@@ -2115,7 +2128,7 @@
# unregister the Group model from admin.
#admin.site.unregister(Group)
-# When debugging it is often easier to see all the classes, but for regular use
+# When debugging it is often easier to see all the classes, but for regular use
# only the top-levels should be displayed
showAll = False
@@ -2150,6 +2163,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(Container, ContainerAdmin)
-
diff --git a/xos/core/fixtures/core_initial_data.json b/xos/core/fixtures/core_initial_data.json
index 86658bb..e4f86f9 100644
--- a/xos/core/fixtures/core_initial_data.json
+++ b/xos/core/fixtures/core_initial_data.json
@@ -65,6 +65,34 @@
},
{
"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.tenantrole",
+ "pk": 1
+},
+{
+ "fields": {
+ "updated": "2015-02-17T22:06:38.620Z",
+ "policed": null,
+ "created": "2015-02-17T22:06:38.620Z",
+ "deleted": false,
+ "backend_register": "{}",
+ "role": "access",
+ "backend_status": "0 - Provisioning in progress",
+ "enacted": null
+ },
+ "model": "core.tenantrole",
+ "pk": 2
+},
+{
+ "fields": {
"accessControl": "allow all",
"updated": "2015-02-17T22:06:37.789Z",
"policed": null,
diff --git a/xos/core/migrations/0001_initial.py b/xos/core/migrations/0001_initial.py
index db7dad0..33af6a6 100644
--- a/xos/core/migrations/0001_initial.py
+++ b/xos/core/migrations/0001_initial.py
@@ -1,15 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from django.db import models, migrations
-import timezones.fields
import core.models.instance
import core.models.network
-import geoposition.fields
-import encrypted_fields.fields
import core.models.serviceclass
import django.utils.timezone
+import encrypted_fields.fields
+import geoposition.fields
+import timezones.fields
from django.conf import settings
+from django.db import migrations, models
class Migration(migrations.Migration):
@@ -1145,6 +1145,39 @@
},
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..6b5826a 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
@@ -29,4 +29,3 @@
from .network import Network, NetworkParameterType, NetworkParameter, Port, NetworkTemplate, Router, NetworkSlice, ControllerNetwork, AddressPool
from .billing import Account, Invoice, Charge, UsableObject, Payment
from .program import Program
-
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 124feb5..f53f2e7 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -1,9 +1,10 @@
-from django.db import models
-from core.models import PlCoreBase,SingletonModel,PlCoreBaseManager
-from core.models.plcorebase import StrippedCharField
-from xos.exceptions import *
-from operator import attrgetter
import json
+from operator import attrgetter
+
+from core.models import PlCoreBase, PlCoreBaseManager, SingletonModel
+from core.models.plcorebase import StrippedCharField
+from django.db import models
+from xos.exceptions import *
COARSE_KIND="coarse"
@@ -656,14 +657,14 @@
for slice in slices:
if slice.instances.all().count() > 0:
for instance in slice.instances.all():
- #Pick the first instance that has lesser than 5 tenants
+ #Pick the first instance that has lesser than 5 tenants
if self.count_of_tenants_of_an_instance(instance) < 5:
return instance
return None
- #TODO: Ideally the tenant count for an instance should be maintained using a
- #many-to-one relationship attribute, however this model being proxy, it does
- #not permit any new attributes to be defined. Find if any better solutions
+ #TODO: Ideally the tenant count for an instance should be maintained using a
+ #many-to-one relationship attribute, however this model being proxy, it does
+ #not permit any new attributes to be defined. Find if any better solutions
def count_of_tenants_of_an_instance(self, instance):
tenant_count = 0
for tenant in self.get_tenant_objects().all():
@@ -817,4 +818,39 @@
return cls.objects.filter(id__in=trp_ids)
+class TenantRole(PlCoreBase):
+ 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):
+ user = models.ForeignKey('User', related_name="tenantprivileges")
+ tenant = models.ForeignKey('Tenant', related_name="tenantprivileges")
+ role = models.ForeignKey('TenantRole', related_name="tenantprivileges")
+
+ class Meta:
+ unique_together = ('user', 'tenant', 'role')
+
+ 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 0b8e3af..43a4e2b 100644
--- a/xos/core/models/user.py
+++ b/xos/core/models/user.py
@@ -1,21 +1,22 @@
-import os
import datetime
-import sys
import hashlib
+import os
+import sys
from collections import defaultdict
-from django.forms.models import model_to_dict
+from operator import attrgetter, itemgetter
+
+import synchronizers.model_policy
+from core.middleware import get_request
+from core.models import DashboardView, PlCoreBase, PlModelMixIn, Site
+from core.models.plcorebase import StrippedCharField
+from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
+from django.core.exceptions import PermissionDenied
+from django.core.mail import EmailMultiAlternatives
from django.db import models
from django.db.models import F, Q
+from django.forms.models import model_to_dict
from django.utils import timezone
-from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
-from django.core.mail import EmailMultiAlternatives
-from django.core.exceptions import PermissionDenied
-from core.models import PlCoreBase,Site, DashboardView, PlModelMixIn
-from core.models.plcorebase import StrippedCharField
from timezones.fields import TimeZoneField
-from operator import itemgetter, attrgetter
-from core.middleware import get_request
-import synchronizers.model_policy
# ------ from plcorebase.py ------
try:
@@ -165,7 +166,7 @@
purge = purge or observer_disabled
except NameError:
pass
-
+
if (purge):
super(User, self).delete(*args, **kwds)
else:
@@ -219,7 +220,7 @@
# roles[site_privilege.role.role_type].append(site_privilege.site.login_base)
# for slice_membership in slice_memberships:
# roles[slice_membership.role.role_type].append(slice_membership.slice.name)
-# return roles
+# return roles
def save(self, *args, **kwds):
if not self.id:
@@ -254,7 +255,7 @@
site_privs = SitePrivilege.objects.filter(user=user, site=self.site)
for site_priv in site_privs:
if site_priv.role.role == 'admin':
- return True
+ return True
if site_priv.role.role == 'pi':
for fieldName in self.diff.keys():
if fieldName in self.PI_FORBIDDEN_FIELDS:
@@ -272,26 +273,26 @@
def can_update_root(self):
"""
- Return True if user has root (global) write access.
+ Return True if user has root (global) write access.
"""
if self.is_readonly:
return False
if self.is_admin:
return True
- return False
+ return False
def can_update_deployment(self, deployment):
from core.models.site import DeploymentPrivilege
if self.can_update_root():
- return True
-
+ return True
+
if DeploymentPrivilege.objects.filter(
deployment=deployment,
user=self,
role__role__in=['admin', 'Admin']):
return True
- return False
+ return False
def can_update_site(self, site, allow=[]):
from core.models.site import SitePrivilege
@@ -301,7 +302,7 @@
site=site, user=self, role__role__in=['admin', 'Admin']+allow):
return True
return False
-
+
def can_update_slice(self, slice):
from core.models.slice import SlicePrivilege
if self.can_update_root():
@@ -310,7 +311,7 @@
return True
if self.can_update_site(slice.site, allow=['pi']):
return True
-
+
if SlicePrivilege.objects.filter(
slice=slice, user=self, role__role__in=['admin', 'Admin']):
return True
@@ -334,9 +335,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
@@ -351,18 +364,18 @@
return readable_objects
def get_permissions(self, filter_by=None):
- """ Return a list of objects for which the user has read or read/write
- access. The object will be an instance of a django model object.
+ """ Return a list of objects for which the user has read or read/write
+ access. The object will be an instance of a django model object.
Permissions will be either 'r' or 'rw'.
-
+
e.g.
[{'object': django_object_instance, 'permissions': 'rw'}, ...]
Returns:
- list of dicts
-
+ list of dicts
+
"""
- from core.models import Deployment, Flavor, Image, Network, NetworkTemplate, Node, PlModelMixIn, Site, Slice, SliceTag, Instance, Tag, User, DeploymentPrivilege, SitePrivilege, SlicePrivilege
+ from core.models import Deployment, Flavor, Image, Network, NetworkTemplate, Node, PlModelMixIn, Site, Slice, SliceTag, Instance, Tag, User, DeploymentPrivilege, SitePrivilege, SlicePrivilege
READ = 'r'
READWRITE = 'rw'
models = []
@@ -371,8 +384,8 @@
deployment_priv_objs = [Image, NetworkTemplate, Flavor]
site_priv_objs = [Node, Slice, User]
- slice_priv_objs = [Instance, Network]
-
+ slice_priv_objs = [Instance, Network]
+
# maps the set of objects a paticular role has write access
write_map = {
DeploymentPrivilege : {
@@ -382,12 +395,12 @@
'admin' : site_priv_objs,
'pi' : [Slice, User],
'tech': [Node],
- },
+ },
SlicePrivilege : {
- 'admin': slice_priv_objs,
- },
+ 'admin': slice_priv_objs,
+ },
}
-
+
privilege_map = {
DeploymentPrivilege : (Deployment, deployment_priv_objs),
SitePrivilege : (Site, site_priv_objs),
@@ -399,7 +412,7 @@
if models and model not in models:
continue
- # get the objects affected by this privilege model
+ # get the objects affected by this privilege model
affected_objects = []
for affected_model in affected_models:
affected_objects.extend(affected_model.select_by_user(self))
@@ -410,7 +423,7 @@
permissions.append(permission_dict(affected_object, READWRITE))
else:
# create a dict of the user's per object privileges
- # ex: {princeton_tmack : ['admin']
+ # ex: {princeton_tmack : ['admin']
privileges = privilege_model.objects.filter(user=self)
for privilege in privileges:
object_roles = defaultdict(list)
@@ -421,7 +434,7 @@
obj = getattr(privilege, field)
if obj:
object_roles[obj].append(privilege.role.role)
-
+
# loop through all objects the user has access to and determine
# if they also have write access
for affected_object in affected_objects:
@@ -438,15 +451,15 @@
permissions.append(permission_dict(affected_object, WRITE))
else:
permissions.append(permission_dict(affected_object, READ))
-
- return permissions
-
+
+ return permissions
+
def get_tenant_permissions(self):
from core.models import Site, Slice
return self.get_object_permissions(filter_by=[Site,Slice])
-
+
@staticmethod
def select_by_user(user):
if user.is_admin:
diff --git a/xos/core/xoslib/methods/vpnview.py b/xos/core/xoslib/methods/vpnview.py
new file mode 100644
index 0000000..0a9612f
--- /dev/null
+++ b/xos/core/xoslib/methods/vpnview.py
@@ -0,0 +1,69 @@
+from core.models import TenantPrivilege
+from plus import PlusSerializerMixin
+from rest_framework import serializers
+from services.vpn.models import VPNService, VPNTenant, VPN_KIND
+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_vpn_service():
+ vpn_services = VPNService.get_service_objects().all()
+ if vpn_services:
+ return vpn_services[0].id
+ return None
+
+
+class VPNTenantSerializer(serializers.ModelSerializer, PlusSerializerMixin):
+ id = ReadOnlyField()
+ service_specific_attribute = ReadOnlyField()
+ server_network = ReadOnlyField()
+ vpn_subnet = ReadOnlyField()
+ is_persistent = ReadOnlyField()
+ clients_can_see_each_other = ReadOnlyField()
+ ca_crt = ReadOnlyField()
+ port_number = ReadOnlyField()
+ protocol = ReadOnlyField()
+ failover_servers = ReadOnlyField()
+ creator = ReadOnlyField()
+ instance = ReadOnlyField()
+ use_ca_from = ReadOnlyField()
+ provider_service = serializers.PrimaryKeyRelatedField(
+ queryset=VPNService.get_service_objects().all(),
+ default=get_default_vpn_service)
+ script_text = serializers.SerializerMethodField(
+ "get_script_text")
+
+ class Meta:
+ model = VPNTenant
+ fields = ('id', 'provider_service', 'use_ca_from',
+ 'service_specific_attribute', 'vpn_subnet',
+ 'server_network', 'creator', 'instance', 'protocol',
+ 'is_persistent',
+ 'clients_can_see_each_other', 'ca_crt', 'port_number',
+ 'script_text', 'failover_servers')
+
+ def get_script_text(self, obj):
+ return obj.create_client_script(
+ self.context['request'].user.email + "-" + str(obj.id))
+
+
+class VPNTenantList(XOSListCreateAPIView):
+ serializer_class = VPNTenantSerializer
+ method_kind = "list"
+ method_name = "vpntenant"
+
+ 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(
+ VPNTenant.get_tenant_objects().filter(pk=priv.tenant.pk)[0])
+ return vpn_tenants
diff --git a/xos/services/vpn/__init__.py b/xos/services/vpn/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/services/vpn/__init__.py
diff --git a/xos/services/vpn/admin.py b/xos/services/vpn/admin.py
new file mode 100644
index 0000000..f6b31f9
--- /dev/null
+++ b/xos/services/vpn/admin.py
@@ -0,0 +1,284 @@
+import os
+import shutil
+
+from core.admin import ReadOnlyAwareAdmin, SliceInline, TenantPrivilegeInline
+from core.middleware import get_request
+from core.models import TenantPrivilege, User
+from django import forms
+from django.contrib import admin
+from services.vpn.models import VPN_KIND, VPNService, VPNTenant
+from xos.exceptions import XOSValidationError
+
+
+class VPNServiceForm(forms.ModelForm):
+
+ exposed_ports = forms.CharField(required=True)
+
+ def __init__(self, *args, **kwargs):
+ super(VPNServiceForm, 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(VPNServiceForm, 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 = VPNService
+
+
+class VPNServiceAdmin(ReadOnlyAwareAdmin):
+ """Defines the admin for the VPNService."""
+ model = VPNService
+ form = VPNServiceForm
+ verbose_name = "VPN Service"
+
+ list_display = ("backend_status_icon", "name", "enabled")
+
+ list_display_links = ('backend_status_icon', 'name', )
+
+ fieldsets = [(None, {'fields': ['backend_status_text', 'name', 'enabled',
+ 'versionNumber', 'description', "view_url",
+ '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'),
+ ('administration', 'Tenants'),
+ ('slices', 'Slices'),)
+
+ suit_form_includes = (('vpnserviceadmin.html',
+ 'top',
+ 'administration'),)
+
+ def queryset(self, request):
+ return VPNService.get_service_objects_by_user(request.user)
+
+
+class VPNTenantForm(forms.ModelForm):
+ """The form used to create and edit a VPNTenant.
+
+ Attributes:
+ creator (forms.ModelChoiceField): The XOS user that created this
+ tenant.
+ client_conf (forms.CharField): The readonly configuration used on the
+ client to connect to this Tenant.
+ server_address (forms.GenericIPAddressField): The ip address on the VPN
+ of this Tenant.
+ client_address (forms.GenericIPAddressField): The ip address on the VPN
+ of the client.
+ is_persistent (forms.BooleanField): Determines if this Tenant keeps
+ this connection alive through failures.
+ """
+ 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=VPNTenant.get_tenant_objects())
+ protocol = forms.ChoiceField(required=True, choices=[
+ ("tcp", "tcp"), ("udp", "udp")])
+ use_ca_from = forms.ModelChoiceField(
+ queryset=VPNTenant.get_tenant_objects(), required=False)
+
+ def __init__(self, *args, **kwargs):
+ super(VPNTenantForm, self).__init__(*args, **kwargs)
+ self.fields['kind'].widget.attrs['readonly'] = True
+ self.fields['failover_servers'].widget.attrs['rows'] = 100
+ # self.fields['script_name'].widget.attrs['readonly'] = True
+ self.fields[
+ 'provider_service'].queryset = (
+ VPNService.get_service_objects().all())
+
+ self.fields['kind'].initial = VPN_KIND
+
+ if self.instance:
+ self.fields['creator'].initial = self.instance.creator
+ self.fields['vpn_subnet'].initial = self.instance.vpn_subnet
+ self.fields[
+ 'server_network'].initial = self.instance.server_network
+ self.fields[
+ 'clients_can_see_each_other'].initial = (
+ self.instance.clients_can_see_each_other)
+ self.fields['is_persistent'].initial = self.instance.is_persistent
+ self.initial['protocol'] = self.instance.protocol
+ self.initial['failover_servers'] = self.instance.failover_servers
+ self.fields['failover_servers'].queryset = (
+ VPNTenant.get_tenant_objects().exclude(pk=self.instance.pk))
+ self.fields['use_ca_from'].queryset = (
+ VPNTenant.get_tenant_objects().exclude(pk=self.instance.pk))
+ if (self.instance.use_ca_from):
+ self.fields['use_ca_from'].initial = (
+ self.instance.use_ca_from[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 = (
+ VPNTenant.get_tenant_objects())
+ if VPNService.get_service_objects().exists():
+ self.fields["provider_service"].initial = (
+ VPNService.get_service_objects().all()[0])
+
+ def save(self, commit=True):
+ result = super(VPNTenantForm, self).save(commit=commit)
+ 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_servers[:] = []
+ for tenant in self.cleaned_data['failover_servers']:
+ self.instance.failover_servers.append(tenant)
+
+ self.instance.protocol = self.cleaned_data.get("protocol")
+ self.instance.port_number = (
+ self.instance.provider_service.get_next_available_port(
+ self.instance.protocol))
+
+ self.instance.use_ca_from[:] = []
+ self.instance.use_ca_from.append(self.cleaned_data.get('use_ca_from'))
+ result.save() # Need to do this so that we know the ID
+
+ self.instance.pki_dir = (
+ VPNService.OPENVPN_PREFIX + "server-" + str(result.id))
+
+ if (not os.path.isdir(self.instance.pki_dir)):
+ VPNService.execute_easyrsa_command(
+ self.instance.pki_dir, "init-pki")
+ if (self.instance.use_ca_from[0]):
+ shutil.copy2(
+ self.instance.use_ca_from[0].pki_dir + "/ca.crt",
+ self.instance.pki_dir)
+ else:
+ VPNService.execute_easyrsa_command(
+ self.instance.pki_dir, "--req-cn=XOS build-ca nopass")
+ elif (self.instance.use_ca_from[0]):
+ shutil.copy2(
+ self.instance.use_ca_from[0].pki_dir + "/ca.crt",
+ self.instance.pki_dir)
+
+ result.ca_crt = self.generate_ca_crt()
+
+ return result
+
+ def generate_ca_crt(self):
+ """str: Generates the ca cert by reading from the ca file"""
+ with open(self.instance.pki_dir + "/ca.crt") as crt:
+ return crt.readlines()
+
+ class Meta:
+ model = VPNTenant
+
+
+class VPNTenantAdmin(ReadOnlyAwareAdmin):
+ verbose_name = "VPN Tenant Admin"
+ list_display = ('id', 'backend_status_icon', 'instance',
+ 'server_network', 'vpn_subnet')
+ list_display_links = ('id', 'backend_status_icon',
+ 'instance', 'server_network', 'vpn_subnet')
+ fieldsets = [(None, {'fields': ['backend_status_text', 'kind',
+ 'provider_service', 'instance', 'creator',
+ 'server_network', 'vpn_subnet',
+ 'is_persistent', 'use_ca_from',
+ 'clients_can_see_each_other',
+ 'failover_servers', "protocol"],
+ 'classes': ['suit-tab suit-tab-general']})]
+ readonly_fields = ('backend_status_text', 'instance')
+ form = VPNTenantForm
+ inlines = [TenantPrivilegeInline]
+
+ suit_form_tabs = (('general', 'Details'),
+ ('tenantprivileges', 'Privileges'))
+
+ def queryset(self, request):
+ return VPNTenant.get_tenant_objects_by_user(request.user)
+
+ def certificate_name(self, tenant_privilege):
+ return (str(tenant_privilege.user.email) +
+ "-" + str(tenant_privilege.tenant.id))
+
+ def save_formset(self, request, form, formset, change):
+ super(VPNTenantAdmin, self).save_formset(
+ request, form, formset, change)
+ for obj in formset.deleted_objects:
+ # If anything deleated was a TenantPrivilege then revoke the
+ # certificate
+ if type(obj) is TenantPrivilege:
+ certificate = self.certificate_name(obj)
+ VPNService.execute_easyrsa_command(
+ obj.tenant.pki_dir, "revoke " + certificate)
+ obj.tenant.enacted = None
+ obj.tenant.save()
+ # TODO(jermowery): determine if this is necessary.
+ # if type(obj) is VPNTenant:
+ # if the tenant was deleted revoke all certs assoicated
+ # pass
+
+ for obj in formset.new_objects:
+ # If there were any new TenantPrivlege objects then create certs
+ if type(obj) is TenantPrivilege:
+ certificate = self.certificate_name(obj)
+ VPNService.execute_easyrsa_command(
+ obj.tenant.pki_dir,
+ "build-client-full " + certificate + " nopass")
+ obj.tenant.enacted = None
+ obj.tenant.save()
+
+# Associate the admin forms with the models.
+admin.site.register(VPNService, VPNServiceAdmin)
+admin.site.register(VPNTenant, VPNTenantAdmin)
diff --git a/xos/services/vpn/models.py b/xos/services/vpn/models.py
new file mode 100644
index 0000000..6b76575
--- /dev/null
+++ b/xos/services/vpn/models.py
@@ -0,0 +1,324 @@
+from subprocess import PIPE, Popen
+
+from core.models import Service, TenantWithContainer
+from django.db import transaction
+from xos.exceptions import XOSConfigurationError, XOSValidationError
+
+VPN_KIND = "vpn"
+
+
+class VPNService(Service):
+ """Defines the Service for creating VPN servers."""
+ KIND = VPN_KIND
+ OPENVPN_PREFIX = "/opt/openvpn/"
+ VARS = OPENVPN_PREFIX + "vars"
+ EASYRSA_LOC = OPENVPN_PREFIX + "easyrsa3/easyrsa"
+ EASYRSA_COMMAND = EASYRSA_LOC + " --vars=" + VARS
+
+ @classmethod
+ def execute_easyrsa_command(cls, pki_dir, command):
+ full_command = (
+ VPNService.EASYRSA_COMMAND + " --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))
+
+ class Meta:
+ proxy = True
+ # The name used to find this service, all directories are named this
+ app_label = "vpn"
+ verbose_name = "VPN Service"
+
+ default_attributes = {'exposed_ports': None,
+ 'exposed_ports_str': None}
+
+ @property
+ def exposed_ports(self):
+ 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):
+ 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):
+ 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 VPNTenant.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 VPNTenant(TenantWithContainer):
+ """Defines the Tenant for creating VPN servers."""
+
+ class Meta:
+ proxy = True
+ verbose_name = "VPN Tenant"
+
+ KIND = VPN_KIND
+
+ sync_attributes = ("nat_ip", "nat_mac",)
+
+ default_attributes = {'vpn_subnet': None,
+ 'server_network': None,
+ 'clients_can_see_each_other': True,
+ 'is_persistent': True,
+ 'ca_crt': None,
+ 'port': None,
+ 'script_text': None,
+ 'pki_dir': None,
+ 'use_ca_from': list(),
+ 'failover_servers': list(),
+ 'protocol': None}
+
+ def __init__(self, *args, **kwargs):
+ vpn_services = VPNService.get_service_objects().all()
+ if vpn_services:
+ self._meta.get_field(
+ "provider_service").default = vpn_services[0].id
+ super(VPNTenant, self).__init__(*args, **kwargs)
+
+ def save(self, *args, **kwargs):
+ super(VPNTenant, self).save(*args, **kwargs)
+ model_policy_vpn_tenant(self.pk)
+
+ def delete(self, *args, **kwargs):
+ self.cleanup_container()
+ super(VPNTenant, self).delete(*args, **kwargs)
+
+ @property
+ def protocol(self):
+ 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(self):
+ return self.get_attribute(
+ "use_ca_from", self.default_attributes["use_ca_from"])
+
+ @use_ca_from.setter
+ def use_ca_from(self, value):
+ self.set_attribute("use_ca_from", value)
+
+ @property
+ def pki_dir(self):
+ return self.get_attribute(
+ "pki_dir", self.default_attributes["pki_dir"])
+
+ @pki_dir.setter
+ def pki_dir(self, value):
+ self.set_attribute("pki_dir", 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_servers(self):
+ return self.get_attribute(
+ "failover_servers", self.default_attributes["failover_servers"])
+
+ @failover_servers.setter
+ def failover_servers(self, value):
+ self.set_attribute("failover_servers", 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 ca_crt(self):
+ """str: the string for the ca certificate"""
+ return self.get_attribute("ca_crt", self.default_attributes['ca_crt'])
+
+ @ca_crt.setter
+ def ca_crt(self, value):
+ self.set_attribute("ca_crt", value)
+
+ @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)
+
+ @property
+ def script_text(self):
+ return self.get_attribute(
+ "script_text", self.default_attributes['script_text'])
+
+ @script_text.setter
+ def script_text(self, value):
+ self.set_attribute("script_text", value)
+
+ def create_client_script(self, client_name):
+ script = ""
+ # write the configuration portion
+ script += ("printf \"%b\" \"")
+ script += self.generate_client_conf(client_name)
+ script += ("\" > client.conf\n")
+ script += ("printf \"%b\" \"")
+ for line in self.ca_crt:
+ script += (line.rstrip() + r"\n")
+ script += ("\" > ca.crt\n")
+ script += ("printf \"%b\" \"")
+ for line in self.get_client_cert(client_name):
+ script += (line.rstrip() + r"\n")
+ script += ("\" > " + client_name + ".crt\n")
+ for line in self.get_client_key(client_name):
+ script += (line.rstrip() + r"\n")
+ script += ("\" > " + client_name + ".key\n")
+ # make sure openvpn is installed
+ script += ("apt-get update\n")
+ script += ("apt-get install openvpn\n")
+ script += ("openvpn client.conf &\n")
+ # close the script
+ return script
+
+ def get_client_cert(self, client_name):
+ with open(self.pki_dir + "/issued/" + client_name + ".crt", 'r') as f:
+ return f.readlines()
+
+ def get_client_key(self, client_name):
+ with open(self.pki_dir + "/private/" + client_name + ".key", 'r') as f:
+ return f.readlines()
+
+ def generate_client_conf(self, client_name):
+ """str: Generates the client configuration to use to connect to this
+ VPN server.
+ """
+ conf = ("client\n" +
+ "dev tun\n" +
+ self.get_remote_line(
+ self.nat_ip, self.port_number, self.protocol))
+ for remote in self.failover_servers:
+ conf += self.get_remote_line(
+ remote.nat_ip, remote.port_number, remote.protocol)
+
+ conf += ("resolv-retry 60\n" +
+ "nobind\n" +
+ "ca ca.crt\n" +
+ "cert " + client_name + ".crt\n" +
+ "key " + client_name + ".key\n" +
+ "comp-lzo\n" +
+ "verb 3\n")
+
+ if self.is_persistent:
+ conf += "persist-tun\n"
+ conf += "persist-key\n"
+
+ return conf
+
+ def get_remote_line(self, host, port_number, protocol):
+ return ("remote " + str(host) + " " + str(port_number) + " " +
+ str(protocol) + "\n")
+
+
+def model_policy_vpn_tenant(pk):
+ """Manages the contain for the VPN Tenant."""
+ # This section of code is atomic to prevent race conditions
+ with transaction.atomic():
+ # We find all of the tenants that are waiting to update
+ tenant = VPNTenant.objects.select_for_update().filter(pk=pk)
+ if not tenant:
+ return
+ # Since this code is atomic it is safe to always use the first tenant
+ tenant = tenant[0]
+ tenant.manage_container()
diff --git a/xos/services/vpn/templates/vpnserviceadmin.html b/xos/services/vpn/templates/vpnserviceadmin.html
new file mode 100644
index 0000000..d983771
--- /dev/null
+++ b/xos/services/vpn/templates/vpnserviceadmin.html
@@ -0,0 +1,10 @@
+<!-- Template used to for the button leading to the HelloWorldTenantComplete form. -->
+<div class = "left-nav">
+ <ul>
+ <li>
+ <a href="/admin/vpn/vpntenant/">
+ VPN Tenants
+ </a>
+ </li>
+ </ul>
+</div>
diff --git a/xos/synchronizers/vpn/__init__.py b/xos/synchronizers/vpn/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/synchronizers/vpn/__init__.py
diff --git a/xos/synchronizers/vpn/model-deps b/xos/synchronizers/vpn/model-deps
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/xos/synchronizers/vpn/model-deps
@@ -0,0 +1 @@
+{}
diff --git a/xos/synchronizers/vpn/run.sh b/xos/synchronizers/vpn/run.sh
new file mode 100755
index 0000000..9a2e69b
--- /dev/null
+++ b/xos/synchronizers/vpn/run.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+python vpn-synchronizer.py -C $XOS_DIR/synchronizers/vpn/vpn_config
diff --git a/xos/synchronizers/vpn/steps/__init__.py b/xos/synchronizers/vpn/steps/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xos/synchronizers/vpn/steps/__init__.py
diff --git a/xos/synchronizers/vpn/steps/sync_vpntenant.py b/xos/synchronizers/vpn/steps/sync_vpntenant.py
new file mode 100644
index 0000000..58a9287
--- /dev/null
+++ b/xos/synchronizers/vpn/steps/sync_vpntenant.py
@@ -0,0 +1,51 @@
+import os
+import sys
+
+from django.db.models import F, Q
+from services.vpn.models import VPNService, VPNTenant
+from synchronizers.base.SyncInstanceUsingAnsible import \
+ SyncInstanceUsingAnsible
+
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+
+class SyncVPNTenant(SyncInstanceUsingAnsible):
+ """Class for syncing a VPNTenant using Ansible."""
+ provides = [VPNTenant]
+ observes = VPNTenant
+ requested_interval = 0
+ template_name = "sync_vpntenant.yaml"
+ service_key_name = "/opt/xos/synchronizers/vpn/vpn_private_key"
+
+ def __init__(self, *args, **kwargs):
+ super(SyncVPNTenant, self).__init__(*args, **kwargs)
+
+ def fetch_pending(self, deleted):
+ if (not deleted):
+ objs = VPNTenant.get_tenant_objects().filter(
+ Q(enacted__lt=F('updated')) |
+ Q(enacted=None), Q(lazy_blocked=False))
+ else:
+ objs = VPNTenant.get_deleted_tenant_objects()
+
+ return objs
+
+ def get_extra_attributes(self, tenant):
+ return {"is_persistent": tenant.is_persistent,
+ "vpn_subnet": tenant.vpn_subnet,
+ "server_network": tenant.server_network,
+ "clients_can_see_each_other": (
+ tenant.clients_can_see_each_other),
+ "port_number": tenant.port_number,
+ "protocol": tenant.protocol,
+ "pki_dir": tenant.pki_dir
+ }
+
+ def run_playbook(self, o, fields):
+ # Generate the server files
+ if (not os.path.isfile(o.pki_dir + "/issued/server.crt")):
+ VPNService.execute_easyrsa_command(
+ o.pki_dir, "build-server-full server nopass")
+ VPNService.execute_easyrsa_command(o.pki_dir, "gen-crl")
+ super(SyncVPNTenant, self).run_playbook(o, fields)
diff --git a/xos/synchronizers/vpn/steps/sync_vpntenant.yaml b/xos/synchronizers/vpn/steps/sync_vpntenant.yaml
new file mode 100644
index 0000000..d70a34d
--- /dev/null
+++ b/xos/synchronizers/vpn/steps/sync_vpntenant.yaml
@@ -0,0 +1,80 @@
+---
+- 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 }}
+
+ tasks:
+ - name: install openvpn
+ apt: name=openvpn state=present update_cache=yes
+
+ - name: stop openvpn
+ shell: kill -9 $(cat {{ pki_dir }}/pid) || true
+
+ - 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
+
+ - name: get server crt
+ copy: src={{ pki_dir }}/issued/server.crt dest={{ pki_dir }}/server.crt
+
+ - name: get ca crt
+ copy: src={{ pki_dir }}/ca.crt dest={{ pki_dir }}/ca.crt
+
+ - name: get crl
+ copy: src={{ pki_dir }}/crl.pem dest={{ pki_dir }}/crl.pem
+
+ - name: get dh
+ copy: src=/opt/openvpn/init_pki/dh.pem dest={{ pki_dir }}/dh.pem
+
+ - name: erase config
+ shell: rm -f {{ pki_dir }}/server.conf
+
+ - name: write base config
+ shell:
+ |
+ printf "script-security 3 system
+ port {{ port_number }}
+ proto {{ protocol }}
+ dev tun
+ 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
+ comp-lzo
+ status {{ pki_dir }}/openvpn-status.log
+ verb 3
+ " > {{ pki_dir }}/server.conf
+
+ - name: write persistent config
+ shell:
+ |
+ printf "keepalive 10 60
+ persist-tun
+ persist-key
+ " >> {{ pki_dir }}/server.conf
+ when: {{ is_persistent }}
+
+ - name: write client-to-client config
+ shell: printf "client-to-client\n" >> {{ pki_dir }}/server.conf
+ when: {{ clients_can_see_each_other }}
+
+ - name: start openvpn
+ shell: openvpn --writepid {{ pki_dir }}/pid {{ pki_dir }}/server.conf &
diff --git a/xos/synchronizers/vpn/stop.sh b/xos/synchronizers/vpn/stop.sh
new file mode 100755
index 0000000..4178688
--- /dev/null
+++ b/xos/synchronizers/vpn/stop.sh
@@ -0,0 +1,2 @@
+# Kill the observer
+pkill -9 -f vpn-synchronizer.py
diff --git a/xos/synchronizers/vpn/vpn-synchronizer.py b/xos/synchronizers/vpn/vpn-synchronizer.py
new file mode 100755
index 0000000..3227ed9
--- /dev/null
+++ b/xos/synchronizers/vpn/vpn-synchronizer.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+
+import importlib
+import os
+import sys
+observer_path = os.path.join(os.path.dirname(
+ os.path.realpath(__file__)), "../../synchronizers/base")
+sys.path.append(observer_path)
+mod = importlib.import_module("xos-synchronizer")
+mod.main()
diff --git a/xos/synchronizers/vpn/vpn_config b/xos/synchronizers/vpn/vpn_config
new file mode 100644
index 0000000..2cdb192
--- /dev/null
+++ b/xos/synchronizers/vpn/vpn_config
@@ -0,0 +1,23 @@
+# Required by XOS
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+# Required by XOS
+[api]
+nova_enabled=True
+
+# Sets options for the synchronizer
+[observer]
+name=vpn
+dependency_graph=/opt/xos/synchronizers/vpn/model-deps
+steps_dir=/opt/xos/synchronizers/vpn/steps
+sys_dir=/opt/xos/synchronizers/vpn/sys
+logfile=/var/log/xos_backend.log
+pretend=False
+backoff_disabled=True
+save_ansible_output=True
+proxy_ssh=False
diff --git a/xos/tools/xos-manage b/xos/tools/xos-manage
index 5410f37..d07e623 100755
--- a/xos/tools/xos-manage
+++ b/xos/tools/xos-manage
@@ -60,13 +60,13 @@
echo Waiting for postgres to start
sleep 1
sudo -u postgres psql -c '\q'
- done
+ done
}
function db_exists {
- sudo -u postgres psql $DBNAME -c '\q' 2>/dev/null
+ sudo -u postgres psql $DBNAME -c '\q' 2>/dev/null
return $?
-}
+}
function createdb {
wait_postgres
@@ -145,6 +145,7 @@
python ./manage.py makemigrations cord
python ./manage.py makemigrations ceilometer
python ./manage.py makemigrations helloworldservice_complete
+ python ./manage.py makemigrations vpn
python ./manage.py makemigrations onos
python ./manage.py makemigrations vtr
#python ./manage.py makemigrations servcomp
diff --git a/xos/xos/settings.py b/xos/xos/settings.py
index aa50e7d..784f62f 100644
--- a/xos/xos/settings.py
+++ b/xos/xos/settings.py
@@ -30,7 +30,7 @@
GEOIP_PATH = "/usr/share/GeoIP"
XOS_DIR = "/opt/xos"
-DEBUG = False
+DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = (
@@ -180,6 +180,7 @@
'services.ceilometer',
'services.requestrouter',
'services.syndicate_storage',
+ 'services.vpn',
'services.vtr',
'geoposition',
'rest_framework_swagger',