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(() => {
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', () => {
"@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",
"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",
}
},
"fsevents": {
- "dev": true,
- "optional": true,
"version": "2.1.3"
},
"glob-parent": {
}
},
"fsevents": {
- "dev": true,
- "optional": true,
"version": "2.1.3"
},
"glob-parent": {
}
},
"fsevents": {
- "dev": true,
- "optional": true,
"version": "2.1.3"
},
"glob-parent": {
"@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",
<!-- Hostname -->
<div class="form-group row">
<label class="cd-col-form-label required"
- for="hostname"
- i18n>Hostname</label>
+ for="hostname">
+ <ng-container i18n>Hostname</ng-container>
+ <cd-helper>
+ <p i18n>To add multiple hosts at once, you can enter:</p>
+ <ul>
+ <li i18n>a comma-separated list of hostnames <samp>(e.g.: example-01,example-02,example-03)</samp>,</li>
+ <li i18n>a range expression <samp>(e.g.: example-[01-03].ceph)</samp>,</li>
+ <li i18n>a comma separated range expression <samp>(e.g.: example-[01-05].lab.com,example2-[1-4].lab.com,example3-[001-006].lab.com)</samp></li>
+ </ul>
+ </cd-helper>
+ </label>
<div class="cd-col-form-input">
<input class="form-control"
type="text"
id="hostname"
name="hostname"
formControlName="hostname"
- autofocus>
+ autofocus
+ (keyup)="checkHostNameValue()">
<span class="invalid-feedback"
*ngIf="hostForm.showError('hostname', formDir, 'required')"
i18n>This field is required.</span>
</div>
<!-- Address -->
- <div class="form-group row">
+ <div class="form-group row"
+ *ngIf="!hostPattern">
<label class="cd-col-form-label"
for="addr"
i18n>Nework address</label>
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'
+ ]);
+ });
});
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';
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.`,
});
}
+ // 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('', {
});
}
+ 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();
+ }
+ });
+ });
}
}