]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
import ceph 15.2.14
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / rgw / rgw-bucket-form / rgw-bucket-form.component.ts
1 import { Component, OnInit } from '@angular/core';
2 import { AbstractControl, AsyncValidatorFn, ValidationErrors, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
4
5 import { I18n } from '@ngx-translate/i18n-polyfill';
6 import * as _ from 'lodash';
7 import { Observable, of as observableOf, timer as observableTimer } from 'rxjs';
8 import { map, switchMapTo } from 'rxjs/operators';
9
10 import { RgwBucketService } from '../../../shared/api/rgw-bucket.service';
11 import { RgwSiteService } from '../../../shared/api/rgw-site.service';
12 import { RgwUserService } from '../../../shared/api/rgw-user.service';
13 import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
14 import { Icons } from '../../../shared/enum/icons.enum';
15 import { NotificationType } from '../../../shared/enum/notification-type.enum';
16 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
17 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
18 import { CdValidators } from '../../../shared/forms/cd-validators';
19 import { NotificationService } from '../../../shared/services/notification.service';
20 import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
21 import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
22
23 @Component({
24 selector: 'cd-rgw-bucket-form',
25 templateUrl: './rgw-bucket-form.component.html',
26 styleUrls: ['./rgw-bucket-form.component.scss']
27 })
28 export class RgwBucketFormComponent implements OnInit {
29 bucketForm: CdFormGroup;
30 editing = false;
31 error = false;
32 loading = false;
33 owners: string[] = null;
34 action: string;
35 resource: string;
36 zonegroup: string;
37 placementTargets: object[] = [];
38 isVersioningEnabled = false;
39 isVersioningAlreadyEnabled = false;
40 isMfaDeleteEnabled = false;
41 isMfaDeleteAlreadyEnabled = false;
42 icons = Icons;
43
44 constructor(
45 private route: ActivatedRoute,
46 private router: Router,
47 private formBuilder: CdFormBuilder,
48 private rgwBucketService: RgwBucketService,
49 private rgwSiteService: RgwSiteService,
50 private rgwUserService: RgwUserService,
51 private notificationService: NotificationService,
52 private i18n: I18n,
53 public actionLabels: ActionLabelsI18n
54 ) {
55 this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`);
56 this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
57 this.resource = this.i18n('bucket');
58 this.createForm();
59 }
60
61 createForm() {
62 const self = this;
63 const eitherDaysOrYears = CdValidators.custom('eitherDaysOrYears', () => {
64 if (!self.bucketForm || !_.get(self.bucketForm.getRawValue(), 'lock_enabled')) {
65 return false;
66 }
67 const years = self.bucketForm.getValue('lock_retention_period_years');
68 const days = self.bucketForm.getValue('lock_retention_period_days');
69 return (days > 0 && years > 0) || (days === 0 && years === 0);
70 });
71 const lockPeriodDefinition = [0, [CdValidators.number(false), eitherDaysOrYears]];
72 this.bucketForm = this.formBuilder.group({
73 id: [null],
74 bid: [null, [Validators.required], this.editing ? [] : [this.bucketNameValidator()]],
75 owner: [null, [Validators.required]],
76 'placement-target': [null, this.editing ? [] : [Validators.required]],
77 versioning: [null],
78 'mfa-delete': [null],
79 'mfa-token-serial': [''],
80 'mfa-token-pin': [''],
81 lock_enabled: [{ value: false, disabled: this.editing }],
82 lock_mode: ['COMPLIANCE'],
83 lock_retention_period_days: lockPeriodDefinition,
84 lock_retention_period_years: lockPeriodDefinition
85 });
86 }
87
88 ngOnInit() {
89 // Get the list of possible owners.
90 this.rgwUserService.enumerate().subscribe((resp: string[]) => {
91 this.owners = resp.sort();
92 });
93
94 if (!this.editing) {
95 // Get placement targets:
96 this.rgwSiteService.get('placement-targets').subscribe((placementTargets: any) => {
97 this.zonegroup = placementTargets['zonegroup'];
98 _.forEach(placementTargets['placement_targets'], (placementTarget) => {
99 placementTarget['description'] = `${placementTarget['name']} (${this.i18n('pool')}: ${
100 placementTarget['data_pool']
101 })`;
102 this.placementTargets.push(placementTarget);
103 });
104
105 // If there is only 1 placement target, select it by default:
106 if (this.placementTargets.length === 1) {
107 this.bucketForm.get('placement-target').setValue(this.placementTargets[0]['name']);
108 }
109 });
110 }
111
112 // Process route parameters.
113 this.route.params.subscribe((params: { bid: string }) => {
114 if (!params.hasOwnProperty('bid')) {
115 return;
116 }
117 const bid = decodeURIComponent(params.bid);
118 this.loading = true;
119
120 this.rgwBucketService.get(bid).subscribe((resp: object) => {
121 this.loading = false;
122 // Get the default values (incl. the values from disabled fields).
123 const defaults = _.clone(this.bucketForm.getRawValue());
124 // Get the values displayed in the form. We need to do that to
125 // extract those key/value pairs from the response data, otherwise
126 // the Angular react framework will throw an error if there is no
127 // field for a given key.
128 let value: object = _.pick(resp, _.keys(defaults));
129 value['placement-target'] = resp['placement_rule'];
130 // Append default values.
131 value = _.merge(defaults, value);
132 // Update the form.
133 this.bucketForm.setValue(value);
134 if (this.editing) {
135 this.setVersioningStatus(resp['versioning']);
136 this.isVersioningAlreadyEnabled = this.isVersioningEnabled;
137 this.setMfaDeleteStatus(resp['mfa_delete']);
138 this.isMfaDeleteAlreadyEnabled = this.isMfaDeleteEnabled;
139 this.setMfaDeleteValidators();
140 }
141 });
142 });
143 }
144
145 goToListView() {
146 this.router.navigate(['/rgw/bucket']);
147 }
148
149 submit() {
150 // Exit immediately if the form isn't dirty.
151 if (this.bucketForm.pristine) {
152 this.goToListView();
153 return;
154 }
155 const values = this.bucketForm.value;
156 if (this.editing) {
157 // Edit
158 const versioning = this.getVersioningStatus();
159 const mfaDelete = this.getMfaDeleteStatus();
160 this.rgwBucketService
161 .update(
162 values['bid'],
163 values['id'],
164 values['owner'],
165 versioning,
166 mfaDelete,
167 values['mfa-token-serial'],
168 values['mfa-token-pin'],
169 values['lock_mode'],
170 values['lock_retention_period_days'],
171 values['lock_retention_period_years']
172 )
173 .subscribe(
174 () => {
175 this.notificationService.show(
176 NotificationType.success,
177 this.i18n('Updated Object Gateway bucket "{{bid}}".', values)
178 );
179 this.goToListView();
180 },
181 () => {
182 // Reset the 'Submit' button.
183 this.bucketForm.setErrors({ cdSubmitButton: true });
184 }
185 );
186 } else {
187 // Add
188 this.rgwBucketService
189 .create(
190 values['bid'],
191 values['owner'],
192 this.zonegroup,
193 values['placement-target'],
194 values['lock_enabled'],
195 values['lock_mode'],
196 values['lock_retention_period_days'],
197 values['lock_retention_period_years']
198 )
199 .subscribe(
200 () => {
201 this.notificationService.show(
202 NotificationType.success,
203 this.i18n('Created Object Gateway bucket "{{bid}}"', values)
204 );
205 this.goToListView();
206 },
207 () => {
208 // Reset the 'Submit' button.
209 this.bucketForm.setErrors({ cdSubmitButton: true });
210 }
211 );
212 }
213 }
214
215 /**
216 * Validate the bucket name. In general, bucket names should follow domain
217 * name constraints:
218 * - Bucket names must be unique.
219 * - Bucket names cannot be formatted as IP address.
220 * - Bucket names can be between 3 and 63 characters long.
221 * - Bucket names must not contain uppercase characters or underscores.
222 * - Bucket names must start with a lowercase letter or number.
223 * - Bucket names must be a series of one or more labels. Adjacent
224 * labels are separated by a single period (.). Bucket names can
225 * contain lowercase letters, numbers, and hyphens. Each label must
226 * start and end with a lowercase letter or a number.
227 */
228 bucketNameValidator(): AsyncValidatorFn {
229 return (control: AbstractControl): Observable<ValidationErrors | null> => {
230 // Exit immediately if user has not interacted with the control yet
231 // or the control value is empty.
232 if (control.pristine || control.value === '') {
233 return observableOf(null);
234 }
235 const constraints = [];
236 let errorName: string;
237 // - Bucket names cannot be formatted as IP address.
238 constraints.push(() => {
239 const ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
240 const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
241 const name = this.bucketForm.get('bid').value;
242 let notIP = true;
243 if (ipv4Rgx.test(name) || ipv6Rgx.test(name)) {
244 errorName = 'ipAddress';
245 notIP = false;
246 }
247 return notIP;
248 });
249 // - Bucket names can be between 3 and 63 characters long.
250 constraints.push((name: string) => {
251 if (!_.inRange(name.length, 3, 64)) {
252 errorName = 'shouldBeInRange';
253 return false;
254 }
255 return true;
256 });
257 // - Bucket names must not contain uppercase characters or underscores.
258 // - Bucket names must start with a lowercase letter or number.
259 // - Bucket names must be a series of one or more labels. Adjacent
260 // labels are separated by a single period (.). Bucket names can
261 // contain lowercase letters, numbers, and hyphens. Each label must
262 // start and end with a lowercase letter or a number.
263 constraints.push((name: string) => {
264 const labels = _.split(name, '.');
265 return _.every(labels, (label) => {
266 // Bucket names must not contain uppercase characters or underscores.
267 if (label !== _.toLower(label) || label.includes('_')) {
268 errorName = 'containsUpperCase';
269 return false;
270 }
271 // Bucket names can contain lowercase letters, numbers, and hyphens.
272 if (!/^\S*$/.test(name) || !/[0-9a-z-]/.test(label)) {
273 errorName = 'onlyLowerCaseAndNumbers';
274 return false;
275 }
276 // Each label must start and end with a lowercase letter or a number.
277 return _.every([0, label.length - 1], (index) => {
278 errorName = 'lowerCaseOrNumber';
279 return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index]));
280 });
281 });
282 });
283 if (!_.every(constraints, (func: Function) => func(control.value))) {
284 return observableTimer().pipe(
285 map(() => {
286 switch (errorName) {
287 case 'onlyLowerCaseAndNumbers':
288 return { onlyLowerCaseAndNumbers: true };
289 case 'shouldBeInRange':
290 return { shouldBeInRange: true };
291 case 'ipAddress':
292 return { ipAddress: true };
293 case 'containsUpperCase':
294 return { containsUpperCase: true };
295 case 'lowerCaseOrNumber':
296 return { lowerCaseOrNumber: true };
297 default:
298 return { bucketNameInvalid: true };
299 }
300 })
301 );
302 }
303 // - Bucket names must be unique.
304 return observableTimer().pipe(
305 switchMapTo(this.rgwBucketService.exists.call(this.rgwBucketService, control.value)),
306 map((resp: boolean) => {
307 if (!resp) {
308 return null;
309 } else {
310 return { bucketNameExists: true };
311 }
312 })
313 );
314 };
315 }
316
317 areMfaCredentialsRequired() {
318 return (
319 this.isMfaDeleteEnabled !== this.isMfaDeleteAlreadyEnabled ||
320 (this.isMfaDeleteAlreadyEnabled &&
321 this.isVersioningEnabled !== this.isVersioningAlreadyEnabled)
322 );
323 }
324
325 setMfaDeleteValidators() {
326 const mfaTokenSerialControl = this.bucketForm.get('mfa-token-serial');
327 const mfaTokenPinControl = this.bucketForm.get('mfa-token-pin');
328
329 if (this.areMfaCredentialsRequired()) {
330 mfaTokenSerialControl.setValidators(Validators.required);
331 mfaTokenPinControl.setValidators(Validators.required);
332 } else {
333 mfaTokenSerialControl.setValidators(null);
334 mfaTokenPinControl.setValidators(null);
335 }
336
337 mfaTokenSerialControl.updateValueAndValidity();
338 mfaTokenPinControl.updateValueAndValidity();
339 }
340
341 getVersioningStatus() {
342 return this.isVersioningEnabled ? RgwBucketVersioning.ENABLED : RgwBucketVersioning.SUSPENDED;
343 }
344
345 setVersioningStatus(status: RgwBucketVersioning) {
346 this.isVersioningEnabled = status === RgwBucketVersioning.ENABLED;
347 }
348
349 updateVersioning() {
350 this.isVersioningEnabled = !this.isVersioningEnabled;
351 this.setMfaDeleteValidators();
352 }
353
354 getMfaDeleteStatus() {
355 return this.isMfaDeleteEnabled ? RgwBucketMfaDelete.ENABLED : RgwBucketMfaDelete.DISABLED;
356 }
357
358 setMfaDeleteStatus(status: RgwBucketMfaDelete) {
359 this.isMfaDeleteEnabled = status === RgwBucketMfaDelete.ENABLED;
360 }
361
362 updateMfaDelete() {
363 this.isMfaDeleteEnabled = !this.isMfaDeleteEnabled;
364 this.setMfaDeleteValidators();
365 }
366 }