diff --git a/app/admin-tab/ha/controller.js b/app/admin-tab/ha/controller.js
new file mode 100644
index 000000000..b845c1125
--- /dev/null
+++ b/app/admin-tab/ha/controller.js
@@ -0,0 +1,206 @@
+import Ember from 'ember';
+import Util from 'ui/utils/util';
+import C from 'ui/utils/constants';
+
+export default Ember.Controller.extend({
+ settings: Ember.inject.service(),
+ growl: Ember.inject.service(),
+ cookies: Ember.inject.service(),
+ projects: Ember.inject.service(),
+
+ csrf: Ember.computed.alias('cookies.CSRF'),
+
+ userUrl: '',
+ selfSign: true,
+ generating: false,
+ justGenerated: false,
+ errors: null,
+ confirmPanic: false,
+ haProject: null,
+
+ configExecute: function() {
+ return 'sudo bash ./rancher-ha.sh rancher/server:' + (this.get('settings.rancherVersion') || 'latest');
+ }.property('settings.rancherVersion'),
+
+ runCode: function() {
+ let version = this.get('settings.rancherVersion') || 'latest';
+
+ return `sudo docker run -d --restart=always -p 8080:8080 \\
+-e CATTLE_DB_CATTLE_MYSQL_HOST= \\
+-e CATTLE_DB_CATTLE_MYSQL_PORT= \\
+-e CATTLE_DB_CATTLE_MYSQL_NAME= \\
+-e CATTLE_DB_CATTLE_USERNAME= \\
+-e CATTLE_DB_CATTLE_PASSWORD= \\
+rancher/server:${version}`;
+ }.property('settings.rancherVersion'),
+
+ isLocalDb: function() {
+ return (this.get('model.haConfig.dbHost')||'').toLowerCase() === 'localhost';
+ }.property('model.haConfig.dbHost'),
+
+ actions: {
+ exportDatabase() {
+ Util.download(this.get('model.haConfig').linkFor('dbdump'));
+ },
+
+ readFile(field, text) {
+ this.set('model.createScript.'+field, text.trim());
+ },
+
+ promptPanic() {
+ this.set('confirmPanic', true);
+ Ember.run.later(() => {
+ if ( this._state !== 'destroying' )
+ {
+ this.set('confirmPanic', false);
+ }
+ }, 5000);
+ },
+
+ panic() {
+ var orig = this.get('model.haConfig');
+ var clone = orig.clone();
+ clone.set('enabled', false);
+ clone.save({headers: {[C.HEADER.PROJECT]: undefined}}).then(() => {
+ orig.set('enabled', false);
+ }).catch((err) => {
+ this.get('growl').fromError(err);
+ });
+ },
+
+ generateConfig() {
+ if ( !this.validate() ) {
+ return;
+ }
+
+ this.set('generating',true);
+ this.set('haProject', null);
+
+ Ember.run.later(() => {
+ this.set('generating',false);
+ this.set('justGenerated',true);
+ }, 500);
+ },
+
+ downloadConfig() {
+ var form = $('#haConfigForm')[0];
+ form.submit();
+ this.set('downloaded',true);
+
+ var ha = this.get('model.haConfig');
+ var clone = ha.clone();
+ clone.set('enabled',true);
+ clone.save({headers: {[C.HEADER.PROJECT]: undefined}}).then((neu) => {
+ ha.merge(neu);
+ this.findProject();
+ });
+ },
+ },
+
+ userUrlChanged: function() {
+ let val = this.get('userUrl')||'';
+ let match = val.match(/^https?:\/\//);
+ if ( match )
+ {
+ val = val.substr(match[0].length);
+ }
+
+ if ( match = val.match(/^(.*):(\d+)$/) )
+ {
+ let port = parseInt(match[2],10);
+ if ( port > 0 )
+ {
+ this.set('model.httpsPort', port);
+ }
+ }
+
+ let pos = val.indexOf('/',1);
+ if ( pos >= 1 && val.substr(pos-2,2) !== ':/' && val.substr(pos-2,2) !== 's:' )
+ {
+ val = val.substr(0,pos);
+ }
+
+ this.set('userUrl', val);
+ this.set('model.createScript.hostRegistrationUrl', 'https://'+val);
+ }.observes('userUrl'),
+
+ selfSignChanged: function() {
+ if ( this.get('selfSign') )
+ {
+ this.get('model.createScript').setProperties({
+ key: null,
+ cert: null,
+ certChain: null,
+ });
+ }
+ }.observes('selfSign'),
+
+ validate() {
+ var errors = this.get('model.createScript').validationErrors();
+ this.set('errors',errors);
+ return errors.length === 0;
+ },
+
+ findProject: function() {
+ this.get('store').find('project', null, {authAsUser: true, filter: {all: true}, forceReload: true}).then((projects) => {
+ var matches = projects.filter((project) => {
+ return project.get('uuid').match(/^system-ha-(\d+)$/) || project.get('uuid').match(/^system-management-(\d+)$/);
+ });
+
+ if ( matches.length )
+ {
+ this.set('haProject', matches.objectAt(0));
+ if ( this.get('projects.current.id') === this.get('haProject.id') )
+ {
+ this.getHosts();
+ }
+ else
+ {
+ this.send('switchProject', this.get('haProject.id'), false);
+ }
+ }
+ else
+ {
+ Ember.run.later(this,'findProject', 5000);
+ }
+ });
+ },
+
+ hosts: null,
+ getHosts: function() {
+ return this.get('store').findAll('host', null, {forceReload: true}).then((hosts) => {
+ this.set('hosts', hosts);
+ });
+ }.observes('haProject'),
+
+ expectedHosts: Ember.computed.alias('model.haConfig.clusterSize'),
+ activeHosts: function() {
+ return (this.get('hosts')||[]).filterBy('state','active').get('length');
+ }.property('hosts.@each.state'),
+
+ hostBlurb: function() {
+ clearInterval(this.get('hostTimer'));
+ var total = this.get('expectedHosts');
+ var active = this.get('activeHosts');
+
+ if ( active < total )
+ {
+ this.set('hostTimer', setInterval(() => {
+ this.getHosts();
+ }, 5000));
+ return active + '/' + total;
+ }
+ else
+ {
+ return total;
+ }
+ }.property('hosts.@each.state','model.haConfig.clusterSize'),
+
+ cert: null,
+ getCertificate: function() {
+ return this.get('store').find('certificate', null, {filter: {name: 'system-ssl'}}).then((certs) => {
+ this.set('cert', certs.objectAt(0));
+ this.getHosts();
+ });
+ }.observes('haProject'),
+});
diff --git a/app/admin-tab/ha/route.js b/app/admin-tab/ha/route.js
new file mode 100644
index 000000000..91007d8ad
--- /dev/null
+++ b/app/admin-tab/ha/route.js
@@ -0,0 +1,27 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ settings: Ember.inject.service(),
+
+ beforeModel() {
+ return Ember.RSVP.all([
+ this.get('store').find('schema','haconfig', {authAsUser: true}),
+ this.get('store').find('schema','haconfiginput', {authAsUser: true}),
+ ]);
+ },
+
+ model() {
+ var store = this.get('store');
+ return store.find('haConfig', null, {authAsUser: true, forceReload: true}).then((res) => {
+ return Ember.Object.create({
+ haConfig: res.objectAt(0),
+ createScript: store.createRecord({type: 'haConfigInput'})
+ });
+ });
+ },
+
+ setupController(controller/*, model*/) {
+ this._super(...arguments);
+ controller.findProject();
+ }
+});
diff --git a/app/admin-tab/ha/template.hbs b/app/admin-tab/ha/template.hbs
new file mode 100644
index 000000000..7cc4312da
--- /dev/null
+++ b/app/admin-tab/ha/template.hbs
@@ -0,0 +1,312 @@
+
+ High Availability is {{#if model.haConfig.enabled}}enabled {{else}}not configured {{/if}}
+
+
+{{#if model.haConfig.enabled}}
+ {{#if (lt activeHosts expectedHosts)}}
+
+ 5. Add hosts
+
+
+ Copy the downloaded script and run it on to each HA host to register them:
+ {{code-block code=configExecute language="bash" constrained=false}}
+
+
+ {{/if}}
+
+
+
+
+
Hosts:
+ {{#if hosts}}
+ {{hostBlurb}}
+ {{else}}
+ Waiting for a host...
+ {{/if}}
+
+
+
+
+
Management Server Certificate: {{#if cert.cert}}{{copy-to-clipboard size="small" clipboardText=cert.cert}}{{/if}}
+ {{#if cert.cert}}
+
{{cert.cert}}
+ {{else}}
+
Waiting for a host...
+ {{/if}}
+
+
+
+
+
+ Danger Zone™
+
+
+
+ {{#if confirmPanic}}
+
+ Are you sure? Click again to really disable access HA
+
+ {{else}}
+
+ Disable HA
+
+ {{/if}}
+
+
+{{else}}
+
+
+ {{#if isLocalDb}}
+ 1. Setup an external database
+
+
+ This {{settings.appName}} installation is currently configured to use the built-in database server, but HA requires a standalone installation of MySQL.
+
+
+
+
+ Setup an external database instance.
+
+ This can be a hosted solution like Amazon RDS or Google Cloud SQL,
+ Or a self-hosted instance or multi-master cluster.
+
+
+ Click the export button below to export the entire contents of the current database.
+ Import the data into the new external database.
+
+
+
+ Export Database
+
Uncompressed Size: {{format-mib model.haConfig.dbSize}}
+
+ {{else}}
+ 1. Setup an external database
+
+ Complete, running off of an external database.
+
+ {{/if}}
+
+
+
+ {{#if isLocalDb}}
+ 2. Use the new external database
+
+
+ Re-launch the server container pointed at the external database:
+ {{code-block code=runCode language="bash" constrained=false}}
+
+ {{#unless settings.isPrivateLabel}}
+
+ See docs for more detail.
+
+ {{/unless}}
+
+ {{else}}
+ 2. Use the new external database
+
+ Complete, {{model.haConfig.dbHost}} will be used as the external database for HA.
+
+ {{/if}}
+
+
+
+ {{#if justGenerated}}
+ 3. Generate HA config script
+
+ Complete.
+
+ {{else}}
+ 3. Generate HA config script
+
+ {{#if isLocalDb}}
+ Come back here once you are running off the external database...
+ {{/if}}
+ {{/if}}
+
+
+
+
+
+
+ {{#if downloaded}}
+ 4. Download script
+
+ Complete, check your Downloads folder.
+
+ {{else}}
+ 4. Download script
+
+ {{#if justGenerated}}
+
+ Click the button below to download a shell script.
+
+ The script generates new encryption keys that will be used for the HA hosts to communicate with each other, so keep it safe.
+ New keys are generated every time you download the config script, and all hosts must have the same keys for HA to work.
+
+
+
+ Download Config Script
+ {{else}}
+
+ Generate the script in step 3.
+
+ {{/if}}
+ {{/if}}
+
+{{/if}}
diff --git a/app/authenticated/route.js b/app/authenticated/route.js
index 347741831..16fde2c76 100644
--- a/app/authenticated/route.js
+++ b/app/authenticated/route.js
@@ -205,9 +205,11 @@ export default Ember.Route.extend(Subscribe, {
this.controllerFor('application').set('showAbout', true);
},
- switchProject(projectId) {
+ switchProject(projectId, transition=true) {
this.reset();
- this.intermediateTransitionTo('authenticated');
+ if ( transition ) {
+ this.intermediateTransitionTo('authenticated');
+ }
this.set(`tab-session.${C.TABSESSION.PROJECT}`, projectId);
this.refresh();
},
diff --git a/app/components/code-block/component.js b/app/components/code-block/component.js
index 273c77ad9..e680878e2 100644
--- a/app/components/code-block/component.js
+++ b/app/components/code-block/component.js
@@ -4,10 +4,11 @@ export default Ember.Component.extend({
language: 'javascript',
code: '',
hide: false,
+ constrained: true,
tagName: 'PRE',
- classNames: ['line-numbers','constrained'],
- classNameBindings: ['languageClass','hide:hide'],
+ classNames: ['line-numbers'],
+ classNameBindings: ['languageClass','hide:hide','constrained:constrained'],
languageClass: function() {
var lang = this.get('language');
diff --git a/app/router.js b/app/router.js
index 95ff6b3e1..3b89e2987 100644
--- a/app/router.js
+++ b/app/router.js
@@ -47,6 +47,7 @@ Router.map(function() {
});
this.route('audit-logs');
+ this.route('ha');
});
this.route('project', {path: '/env/:project_id'}, function() {
diff --git a/app/services/settings.js b/app/services/settings.js
index 83dbc15ad..bd66f0230 100644
--- a/app/services/settings.js
+++ b/app/services/settings.js
@@ -6,7 +6,7 @@ export function normalizeName(str) {
}
export function denormalizeName(str) {
- return str.replace(C.SETTING.DOT_CHAR,'.').toLowerCase();
+ return str.replace(new RegExp('['+C.SETTING.DOT_CHAR+']','g'),'.').toLowerCase();
}
export default Ember.Service.extend(Ember.Evented, {
@@ -85,6 +85,12 @@ export default Ember.Service.extend(Ember.Evented, {
return this.get('asMap')[normalizeName(name)];
},
+ findAsUser(key) {
+ return this.get('store').find('setting', denormalizeName(key), {authAsUser: true, forceReload: true}).then(() => {
+ return Ember.RSVP.resolve(this.unknownProperty(key));
+ });
+ },
+
asMap: function() {
var out = {};
(this.get('all')||[]).forEach((setting) => {
diff --git a/app/templates/service-addtl-info.hbs b/app/templates/service-addtl-info.hbs
index 350bf0480..29e7175b6 100644
--- a/app/templates/service-addtl-info.hbs
+++ b/app/templates/service-addtl-info.hbs
@@ -11,7 +11,7 @@
Image:
- {{service.launchConfig.imageUuid}}
+ {{service.launchConfig.displayImage}}
diff --git a/app/templates/tabs/admin-tab.hbs b/app/templates/tabs/admin-tab.hbs
index ff08a9566..731f852f9 100644
--- a/app/templates/tabs/admin-tab.hbs
+++ b/app/templates/tabs/admin-tab.hbs
@@ -3,3 +3,4 @@
{{#link-to "admin-tab.accounts"}} Accounts{{/link-to}}
{{#link-to "admin-tab.auth"}} Access Control{{/link-to}}
{{#link-to "admin-tab.settings"}} Settings{{/link-to}}
+{{#link-to "admin-tab.ha"}} HA{{/link-to}}
diff --git a/app/utils/constants.js b/app/utils/constants.js
index 8445e34f3..d1e7bf784 100644
--- a/app/utils/constants.js
+++ b/app/utils/constants.js
@@ -166,23 +166,23 @@ var C = {
SETTING: {
// Dots in key names do not mix well with Ember, so use $ in their place.
- DOT_CHAR: '$',
- VERSION_RANCHER: 'rancher$server$image',
- VERSION_COMPOSE: 'rancher$compose$version',
- VERSION_CATTLE: 'cattle$version',
- VERSION_MACHINE: 'docker$machine$version',
- VERSION_GMS: 'go$machine$service$version',
+ DOT_CHAR: '$',
+ VERSION_RANCHER: 'rancher$server$image',
+ VERSION_COMPOSE: 'rancher$compose$version',
+ VERSION_CATTLE: 'cattle$version',
+ VERSION_MACHINE: 'docker$machine$version',
+ VERSION_GMS: 'go$machine$service$version',
COMPOSE_URL: {
- DARWIN: 'rancher$compose$darwin$url',
- WINDOWS: 'rancher$compose$windows$url',
- LINUX: 'rancher$compose$linux$url',
+ DARWIN: 'rancher$compose$darwin$url',
+ WINDOWS: 'rancher$compose$windows$url',
+ LINUX: 'rancher$compose$linux$url',
},
- API_HOST: 'api$host',
- CATALOG_URL: 'catalog$url',
- VM_ENABLED: 'vm$enabled',
- HELP_ENABLED: 'help$enabled',
- SWARM_PORT: 'swarm$tls$port',
- ENGINE_URL: 'engine$install$url'
+ API_HOST: 'api$host',
+ CATALOG_URL: 'catalog$url',
+ VM_ENABLED: 'vm$enabled',
+ HELP_ENABLED: 'help$enabled',
+ SWARM_PORT: 'swarm$tls$port',
+ ENGINE_URL: 'engine$install$url',
},
USER: {
diff --git a/package.json b/package.json
index 43cedc0fa..f6a09be04 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ui",
- "version": "0.100.4",
+ "version": "1.0.1",
"private": true,
"directories": {
"doc": "doc",
diff --git a/server/proxies/api.js b/server/proxies/api.js
index 5732e9007..f76862a29 100644
--- a/server/proxies/api.js
+++ b/server/proxies/api.js
@@ -10,6 +10,7 @@ module.exports = function(app, options) {
ws: true,
xfwd: false,
target: config.apiServer,
+ secure: false,
});
proxy.on('error', onProxyError);