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}} + + {{else}} + + {{/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.
  • +
+

+

+ +

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}} + + +
+ + +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ + https:// + + {{input type="text" value=userUrl type="text" classNames="form-control"}} +
+

This should be a FQDN that resolves to the addresses of or is a load balancer for {{#if (eq model.createScript.clusterSize 1)}}the HA host{{else}}all {{model.createScript.clusterSize}} HA hosts{{/if}}. Do not include /v1 or any other path.

+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ + {{#liquid-if (not selfSign)}} +
+
+
+ + {{read-text-file accept="text/*, .pem, .pkey, .key" action=(action "readFile" "key")}} +
+ {{textarea name="key" value=model.createScript.key classNames="form-control no-resize" rows="5" placeholder="Paste in the private key, starting with -----BEGIN RSA PRIVATE KEY-----"}} +
+ +
+
+ + {{read-text-file accept="text/*, .pem, .crt" action=(action "readFile" "cert")}} +
+ {{textarea name="cert" value=model.createScript.cert classNames="form-control no-resize" rows="5" placeholder="Paste in the primary certificate, starting with -----BEGIN CERTIFICATE-----"}} +
+ +
+
+ + {{read-text-file accept="text/*, .pem, .crt" action=(action "readFile" "certChain")}} +
+ {{textarea name="certChain" value=model.createScript.certChain classNames="form-control no-resize" rows="5" placeholder="Optional; Paste in the additional chained certificates, starting with -----BEGIN CERTIFICATE-----"}} +
+
+ {{/liquid-if}} + + {{#advanced-section}} +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
HTTPSRequired{{input name="httpsPort" class="form-control input-sm r-mb5" type="number" min=1 max=65535 value=model.createScript.httpsPort}}
HTTPRequired{{input name="httpPort" class="form-control input-sm r-mb5" type="number" min=1 max=65535 value=model.createScript.httpPort disabled=(not model.createScript.httpEnabled)}}
SwarmRequired{{input name="swarmPort" class="form-control input-sm r-mb5" type="number" min=1 max=65535 value=model.createScript.swarmPort disabled=(not model.createScript.swarmEnabled)}}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
RedisRequired{{input name="redisPort" class="form-control input-sm r-mb5" type="number" min=1 max=65535 value=model.createScript.redisPort}}
ZooKeeper ClientRequired{{input name="zookeeperClientPort" class="form-control input-sm r-mb5" type="number" min=1 max=65535 value=model.createScript.zookeeperClientPort}}
ZooKeeper QuorumRequired{{input name="zookeeperQuorumPort" class="form-control input-sm r-mb5" type="number" min=1 max=65535 value=model.createScript.zookeeperQuorumPort}}
ZooKeeper LeaderRequired{{input name="zookeeperLeaderPort" class="form-control input-sm r-mb5" type="number" min=1 max=65535 value=model.createScript.zookeeperLeaderPort}}
+
+
+ {{/advanced-section}} + +

+ {{top-errors errors=errors}} + + {{#if generating}} + + {{else}} + + {{/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. +
+

+ + + {{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 @@
- {{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);