]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/crush-rule-form-modal/crush-rule-form-modal.component.ts
import 15.2.1 Octopus source
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / pool / crush-rule-form-modal / crush-rule-form-modal.component.ts
1 import { Component, EventEmitter, OnInit, Output } from '@angular/core';
2 import { Validators } from '@angular/forms';
3
4 import { I18n } from '@ngx-translate/i18n-polyfill';
5 import * as _ from 'lodash';
6 import { BsModalRef } from 'ngx-bootstrap/modal';
7
8 import { CrushRuleService } from '../../../shared/api/crush-rule.service';
9 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
10 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
11 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
12 import { CdValidators } from '../../../shared/forms/cd-validators';
13 import { CrushNode } from '../../../shared/models/crush-node';
14 import { FinishedTask } from '../../../shared/models/finished-task';
15 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
16
17 @Component({
18 selector: 'cd-crush-rule-form-modal',
19 templateUrl: './crush-rule-form-modal.component.html',
20 styleUrls: ['./crush-rule-form-modal.component.scss']
21 })
22 export class CrushRuleFormModalComponent implements OnInit {
23 @Output()
24 submitAction = new EventEmitter();
25
26 buckets: CrushNode[] = [];
27 failureDomains: { [type: string]: CrushNode[] } = {};
28 devices: string[] = [];
29 tooltips = this.crushRuleService.formTooltips;
30
31 form: CdFormGroup;
32 names: string[];
33 action: string;
34 resource: string;
35
36 private nodes: CrushNode[] = [];
37 private easyNodes: { [id: number]: CrushNode } = {};
38
39 constructor(
40 private formBuilder: CdFormBuilder,
41 public bsModalRef: BsModalRef,
42 private taskWrapper: TaskWrapperService,
43 private crushRuleService: CrushRuleService,
44 private i18n: I18n,
45 public actionLabels: ActionLabelsI18n
46 ) {
47 this.action = this.actionLabels.CREATE;
48 this.resource = this.i18n('Crush Rule');
49 this.createForm();
50 }
51
52 createForm() {
53 this.form = this.formBuilder.group({
54 // name: string
55 name: [
56 '',
57 [
58 Validators.required,
59 Validators.pattern('[A-Za-z0-9_-]+'),
60 CdValidators.custom(
61 'uniqueName',
62 (value: any) => this.names && this.names.indexOf(value) !== -1
63 )
64 ]
65 ],
66 // root: CrushNode
67 root: null, // Replaced with first root
68 // failure_domain: string
69 failure_domain: '', // Replaced with most common type
70 // device_class: string
71 device_class: '' // Replaced with device type if only one exists beneath domain
72 });
73 }
74
75 ngOnInit() {
76 this.crushRuleService
77 .getInfo()
78 .subscribe(({ names, nodes }: { names: string[]; nodes: CrushNode[] }) => {
79 this.nodes = nodes;
80 nodes.forEach((node) => {
81 this.easyNodes[node.id] = node;
82 });
83 this.buckets = _.sortBy(
84 nodes.filter((n) => n.children),
85 'name'
86 );
87 this.names = names;
88 this.preSelectRoot();
89 });
90 this.form.get('root').valueChanges.subscribe((root: CrushNode) => this.updateRoot(root));
91 this.form
92 .get('failure_domain')
93 .valueChanges.subscribe((domain: string) => this.updateDevices(domain));
94 }
95
96 private preSelectRoot() {
97 const rootNode = this.nodes.find((node) => node.type === 'root');
98 this.form.silentSet('root', rootNode);
99 this.updateRoot(rootNode);
100 }
101
102 private updateRoot(rootNode: CrushNode) {
103 const nodes = this.getSubNodes(rootNode);
104 const domains = {};
105 nodes.forEach((node) => {
106 if (!domains[node.type]) {
107 domains[node.type] = [];
108 }
109 domains[node.type].push(node);
110 });
111 Object.keys(domains).forEach((type) => {
112 if (domains[type].length <= 1) {
113 delete domains[type];
114 }
115 });
116 this.failureDomains = domains;
117 this.updateFailureDomain();
118 }
119
120 private getSubNodes(node: CrushNode): CrushNode[] {
121 let subNodes = [node]; // Includes parent node
122 if (!node.children) {
123 return subNodes;
124 }
125 node.children.forEach((id) => {
126 const childNode = this.easyNodes[id];
127 subNodes = subNodes.concat(this.getSubNodes(childNode));
128 });
129 return subNodes;
130 }
131
132 private updateFailureDomain() {
133 let failureDomain = this.getIncludedCustomValue(
134 'failure_domain',
135 Object.keys(this.failureDomains)
136 );
137 if (failureDomain === '') {
138 failureDomain = this.setMostCommonDomain();
139 }
140 this.updateDevices(failureDomain);
141 }
142
143 private getIncludedCustomValue(controlName: string, includedIn: string[]) {
144 const control = this.form.get(controlName);
145 return control.dirty && includedIn.includes(control.value) ? control.value : '';
146 }
147
148 private setMostCommonDomain(): string {
149 let winner = { n: 0, type: '' };
150 Object.keys(this.failureDomains).forEach((type) => {
151 const n = this.failureDomains[type].length;
152 if (winner.n < n) {
153 winner = { n, type };
154 }
155 });
156 this.form.silentSet('failure_domain', winner.type);
157 return winner.type;
158 }
159
160 updateDevices(failureDomain: string) {
161 const subNodes = _.flatten(
162 this.failureDomains[failureDomain].map((node) => this.getSubNodes(node))
163 );
164 this.devices = _.uniq(subNodes.filter((n) => n.device_class).map((n) => n.device_class)).sort();
165 const device =
166 this.devices.length === 1
167 ? this.devices[0]
168 : this.getIncludedCustomValue('device_class', this.devices);
169 this.form.get('device_class').setValue(device);
170 }
171
172 failureDomainKeys(): string[] {
173 return Object.keys(this.failureDomains).sort();
174 }
175
176 onSubmit() {
177 if (this.form.invalid) {
178 this.form.setErrors({ cdSubmitButton: true });
179 return;
180 }
181 const rule = _.cloneDeep(this.form.value);
182 rule.root = rule.root.name;
183 if (rule.device_class === '') {
184 delete rule.device_class;
185 }
186 this.taskWrapper
187 .wrapTaskAroundCall({
188 task: new FinishedTask('crushRule/create', rule),
189 call: this.crushRuleService.create(rule)
190 })
191 .subscribe(
192 undefined,
193 () => {
194 this.form.setErrors({ cdSubmitButton: true });
195 },
196 () => {
197 this.bsModalRef.hide();
198 this.submitAction.emit(rule);
199 }
200 );
201 }
202 }