From: Aashish Sharma Date: Thu, 23 Sep 2021 10:52:56 +0000 (+0530) Subject: mgr/dashboard: Cluster Creation Add multiple hosts at once X-Git-Tag: v17.1.0~590^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=caa177265c34530b1de10f9fe3664c23202d1b5f;p=ceph.git mgr/dashboard: Cluster Creation Add multiple hosts at once Add multiple hosts at once in cluster creation wizard Fixes: https://tracker.ceph.com/issues/52759 Signed-off-by: Aashish Sharma --- diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/02-create-cluster-add-host.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/02-create-cluster-add-host.e2e-spec.ts index a35bd6c10748e..8ea195f89e7aa 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/02-create-cluster-add-host.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/02-create-cluster-add-host.e2e-spec.ts @@ -9,12 +9,15 @@ describe('Create cluster add host page', () => { const hostnames = [ 'ceph-node-00.cephlab.com', 'ceph-node-01.cephlab.com', - 'ceph-node-02.cephlab.com' + 'ceph-node-02.cephlab.com', + 'ceph-node-[01-02].cephlab.com' ]; - const addHost = (hostname: string, exist?: boolean) => { + const addHost = (hostname: string, exist?: boolean, pattern?: boolean) => { cy.get('.btn.btn-accent').first().click({ force: true }); createClusterHostPage.add(hostname, exist, false); - createClusterHostPage.checkExist(hostname, true); + if (!pattern) { + createClusterHostPage.checkExist(hostname, true); + } }; beforeEach(() => { @@ -35,6 +38,9 @@ describe('Create cluster add host page', () => { addHost(hostnames[1], false); addHost(hostnames[2], false); + createClusterHostPage.delete(hostnames[1]); + createClusterHostPage.delete(hostnames[2]); + addHost(hostnames[3], false, true); }); it('should delete a host and add it back', () => { diff --git a/src/pybind/mgr/dashboard/frontend/package-lock.json b/src/pybind/mgr/dashboard/frontend/package-lock.json index b07688bf3b45f..40e999c7d36b2 100644 --- a/src/pybind/mgr/dashboard/frontend/package-lock.json +++ b/src/pybind/mgr/dashboard/frontend/package-lock.json @@ -3802,6 +3802,12 @@ "@babel/types": "^7.3.0" } }, + "@types/brace-expansion": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/brace-expansion/-/brace-expansion-1.1.0.tgz", + "integrity": "sha512-SaU/Kgp6z40CiF9JxlsrSrBEa+8YIry9IiCPhhYSNekeEhIAkY7iyu9aZ+5dSQIdo7mf86MUVvxWYm5GAzB/0g==", + "dev": true + }, "@types/chart.js": { "version": "2.9.34", "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.34.tgz", @@ -9503,11 +9509,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "fsevents": { - "dev": true, - "optional": true, - "version": "2.1.3" - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -15277,8 +15278,6 @@ } }, "fsevents": { - "dev": true, - "optional": true, "version": "2.1.3" }, "glob-parent": { @@ -24662,8 +24661,6 @@ } }, "fsevents": { - "dev": true, - "optional": true, "version": "2.1.3" }, "glob-parent": { @@ -25494,8 +25491,6 @@ } }, "fsevents": { - "dev": true, - "optional": true, "version": "2.1.3" }, "glob-parent": { diff --git a/src/pybind/mgr/dashboard/frontend/package.json b/src/pybind/mgr/dashboard/frontend/package.json index 2978a3f3128e5..aa5c44b80353d 100644 --- a/src/pybind/mgr/dashboard/frontend/package.json +++ b/src/pybind/mgr/dashboard/frontend/package.json @@ -114,6 +114,7 @@ "@angular/language-service": "11.2.14", "@applitools/eyes-cypress": "^3.22.0", "@compodoc/compodoc": "1.1.11", + "@types/brace-expansion": "^1.1.0", "@types/jest": "26.0.14", "@types/lodash": "4.14.161", "@types/node": "12.12.62", diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html index a3477b9bd4102..8ff4cfa5c179e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.html @@ -16,8 +16,17 @@
+ for="hostname"> + Hostname + +

To add multiple hosts at once, you can enter:

+
    +
  • a comma-separated list of hostnames (e.g.: example-01,example-02,example-03),
  • +
  • a range expression (e.g.: example-[01-03].ceph),
  • +
  • a comma separated range expression (e.g.: example-[01-05].lab.com,example2-[1-4].lab.com,example3-[001-006].lab.com)
  • +
+
+
+ autofocus + (keyup)="checkHostNameValue()"> This field is required. @@ -36,7 +46,8 @@
-
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts index ed3daf1e4b49b..ed85d96cb1bab 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.spec.ts @@ -81,4 +81,88 @@ describe('HostFormComponent', () => { component.submit(); expect(component.status).toBe('maintenance'); }); + + it('should expand the hostname correctly', () => { + component.hostForm.get('hostname').setValue('ceph-node-00.cephlab.com'); + fixture.detectChanges(); + component.submit(); + expect(component.hostnameArray).toStrictEqual(['ceph-node-00.cephlab.com']); + + component.hostnameArray = []; + + component.hostForm.get('hostname').setValue('ceph-node-[00-10].cephlab.com'); + fixture.detectChanges(); + component.submit(); + expect(component.hostnameArray).toStrictEqual([ + 'ceph-node-00.cephlab.com', + 'ceph-node-01.cephlab.com', + 'ceph-node-02.cephlab.com', + 'ceph-node-03.cephlab.com', + 'ceph-node-04.cephlab.com', + 'ceph-node-05.cephlab.com', + 'ceph-node-06.cephlab.com', + 'ceph-node-07.cephlab.com', + 'ceph-node-08.cephlab.com', + 'ceph-node-09.cephlab.com', + 'ceph-node-10.cephlab.com' + ]); + + component.hostnameArray = []; + + component.hostForm.get('hostname').setValue('ceph-node-00.cephlab.com,ceph-node-1.cephlab.com'); + fixture.detectChanges(); + component.submit(); + expect(component.hostnameArray).toStrictEqual([ + 'ceph-node-00.cephlab.com', + 'ceph-node-1.cephlab.com' + ]); + + component.hostnameArray = []; + + component.hostForm + .get('hostname') + .setValue('ceph-mon-[01-05].lab.com,ceph-osd-[1-4].lab.com,ceph-rgw-[001-006].lab.com'); + fixture.detectChanges(); + component.submit(); + expect(component.hostnameArray).toStrictEqual([ + 'ceph-mon-01.lab.com', + 'ceph-mon-02.lab.com', + 'ceph-mon-03.lab.com', + 'ceph-mon-04.lab.com', + 'ceph-mon-05.lab.com', + 'ceph-osd-1.lab.com', + 'ceph-osd-2.lab.com', + 'ceph-osd-3.lab.com', + 'ceph-osd-4.lab.com', + 'ceph-rgw-001.lab.com', + 'ceph-rgw-002.lab.com', + 'ceph-rgw-003.lab.com', + 'ceph-rgw-004.lab.com', + 'ceph-rgw-005.lab.com', + 'ceph-rgw-006.lab.com' + ]); + + component.hostnameArray = []; + + component.hostForm + .get('hostname') + .setValue('ceph-(mon-[00-04],osd-[001-005],rgw-[1-3]).lab.com'); + fixture.detectChanges(); + component.submit(); + expect(component.hostnameArray).toStrictEqual([ + 'ceph-mon-00.lab.com', + 'ceph-mon-01.lab.com', + 'ceph-mon-02.lab.com', + 'ceph-mon-03.lab.com', + 'ceph-mon-04.lab.com', + 'ceph-osd-001.lab.com', + 'ceph-osd-002.lab.com', + 'ceph-osd-003.lab.com', + 'ceph-osd-004.lab.com', + 'ceph-osd-005.lab.com', + 'ceph-rgw-1.lab.com', + 'ceph-rgw-2.lab.com', + 'ceph-rgw-3.lab.com' + ]); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts index 704b659127f26..6e84da688e581 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-form/host-form.component.ts @@ -3,6 +3,7 @@ import { FormControl, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import expand from 'brace-expansion'; import { HostService } from '~/app/shared/api/host.service'; import { SelectMessages } from '~/app/shared/components/select/select-messages.model'; @@ -23,10 +24,12 @@ export class HostFormComponent extends CdForm implements OnInit { action: string; resource: string; hostnames: string[]; + hostnameArray: string[] = []; addr: string; status: string; allLabels: string[]; pageURL: string; + hostPattern = false; messages = new SelectMessages({ empty: $localize`There are no labels.`, @@ -59,6 +62,12 @@ export class HostFormComponent extends CdForm implements OnInit { }); } + // check if hostname is a single value or pattern to hide network address field + checkHostNameValue() { + const hostNames = this.hostForm.get('hostname').value; + hostNames.match(/[()\[\]{},]/g) ? (this.hostPattern = true) : (this.hostPattern = false); + } + private createForm() { this.hostForm = new CdFormGroup({ hostname: new FormControl('', { @@ -77,30 +86,77 @@ export class HostFormComponent extends CdForm implements OnInit { }); } + private isCommaSeparatedPattern(hostname: string) { + // eg. ceph-node-01.cephlab.com,ceph-node-02.cephlab.com + return hostname.includes(','); + } + + private isRangeTypePattern(hostname: string) { + // check if it is a range expression or comma separated range expression + // eg. ceph-mon-[01-05].lab.com,ceph-osd-[02-08].lab.com,ceph-rgw-[01-09] + return hostname.includes('[') && hostname.includes(']') && !hostname.match(/(?![^(]*\)),/g); + } + + private replaceBraces(hostname: string) { + // pattern to replace range [0-5] to [0..5](valid expression for brace expansion) + // replace any kind of brackets with curly braces + return hostname + .replace(/(?<=\d)\s*-\s*(?=\d)/g, '..') + .replace(/\(/g, '{') + .replace(/\)/g, '}') + .replace(/\[/g, '{') + .replace(/]/g, '}'); + } + + // expand hostnames in case hostname is a pattern + private checkHostNamePattern(hostname: string) { + if (this.isRangeTypePattern(hostname)) { + const hostnameRange = this.replaceBraces(hostname); + this.hostnameArray = expand(hostnameRange); + } else if (this.isCommaSeparatedPattern(hostname)) { + let hostArray = []; + hostArray = hostname.split(','); + hostArray.forEach((host: string) => { + if (this.isRangeTypePattern(host)) { + const hostnameRange = this.replaceBraces(host); + this.hostnameArray = this.hostnameArray.concat(expand(hostnameRange)); + } else { + this.hostnameArray.push(host); + } + }); + } else { + // single hostname + this.hostnameArray.push(hostname); + } + } + submit() { const hostname = this.hostForm.get('hostname').value; + this.checkHostNamePattern(hostname); this.addr = this.hostForm.get('addr').value; this.status = this.hostForm.get('maintenance').value ? 'maintenance' : ''; this.allLabels = this.hostForm.get('labels').value; if (this.pageURL !== 'hosts' && !this.allLabels.includes('_no_schedule')) { this.allLabels.push('_no_schedule'); } - this.taskWrapper - .wrapTaskAroundCall({ - task: new FinishedTask('host/' + URLVerbs.ADD, { - hostname: hostname - }), - call: this.hostService.create(hostname, this.addr, this.allLabels, this.status) - }) - .subscribe({ - error: () => { - this.hostForm.setErrors({ cdSubmitButton: true }); - }, - complete: () => { - this.pageURL === 'hosts' - ? this.router.navigate([this.pageURL, { outlets: { modal: null } }]) - : this.activeModal.close(); - } - }); + this.hostnameArray.forEach((hostName: string) => { + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('host/' + URLVerbs.ADD, { + hostname: hostName + }), + call: this.hostService.create(hostName, this.addr, this.allLabels, this.status) + }) + .subscribe({ + error: () => { + this.hostForm.setErrors({ cdSubmitButton: true }); + }, + complete: () => { + this.pageURL === 'hosts' + ? this.router.navigate([this.pageURL, { outlets: { modal: null } }]) + : this.activeModal.close(); + } + }); + }); } }