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