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