]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.ts
update source to Ceph Pacific 16.2.2
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / shared / classes / crush.node.selection.class.ts
CommitLineData
e306af50
TL
1import { AbstractControl } from '@angular/forms';
2
f67539c2 3import _ from 'lodash';
e306af50
TL
4
5import { CrushNode } from '../models/crush-node';
6
7export class CrushNodeSelectionClass {
8 private nodes: CrushNode[] = [];
f6b5b4d7 9 private idTree: { [id: number]: CrushNode } = {};
e306af50
TL
10 private allDevices: string[] = [];
11 private controls: {
12 root: AbstractControl;
13 failure: AbstractControl;
14 device: AbstractControl;
15 };
16
17 buckets: CrushNode[] = [];
18 failureDomains: { [type: string]: CrushNode[] } = {};
19 failureDomainKeys: string[] = [];
20 devices: string[] = [];
21 deviceCount = 0;
22
f6b5b4d7
TL
23 static searchFailureDomains(
24 nodes: CrushNode[],
25 s: string
26 ): { [failureDomain: string]: CrushNode[] } {
27 return this.getFailureDomains(this.search(nodes, s));
28 }
29
30 /**
31 * Filters crush map for a node and it's tree.
32 * The node name as provided in crush rules attribute item_name is supported.
33 * This means that '$name~$deviceType' can be used and will result in a crush map
34 * that only include buckets with the specified device in use as their leaf.
35 */
36 static search(nodes: CrushNode[], s: string): CrushNode[] {
37 const [search, deviceType] = s.split('~'); // Used inside item_name in crush rules
38 const node = nodes.find((n) => ['name', 'id', 'type'].some((attr) => n[attr] === search));
39 if (!node) {
40 return [];
41 }
42 nodes = this.getSubNodes(node, this.createIdTreeFromNodes(nodes));
43 if (deviceType) {
44 nodes = this.filterNodesByDeviceType(nodes, deviceType);
45 }
46 return nodes;
47 }
48
49 static createIdTreeFromNodes(nodes: CrushNode[]): { [id: number]: CrushNode } {
50 const idTree = {};
51 nodes.forEach((node) => {
52 idTree[node.id] = node;
53 });
54 return idTree;
55 }
56
57 static getSubNodes(node: CrushNode, idTree: { [id: number]: CrushNode }): CrushNode[] {
58 let subNodes = [node]; // Includes parent node
59 if (!node.children) {
60 return subNodes;
61 }
62 node.children.forEach((id) => {
63 const childNode = idTree[id];
64 subNodes = subNodes.concat(this.getSubNodes(childNode, idTree));
65 });
66 return subNodes;
67 }
68
69 static filterNodesByDeviceType(nodes: CrushNode[], deviceType: string): any {
70 let doNotInclude = nodes
71 .filter((n) => n.device_class && n.device_class !== deviceType)
72 .map((n) => n.id);
73 let foundNewNode: boolean;
74 let childrenToRemove = doNotInclude;
75
76 // Filters out all unwanted nodes
77 do {
78 foundNewNode = false;
79 nodes = nodes.filter((n) => !doNotInclude.includes(n.id)); // Unwanted nodes
80 // Find nodes where all children were filtered
81 const toRemoveNext: number[] = [];
82 nodes.forEach((n) => {
83 if (n.children && n.children.every((id) => doNotInclude.includes(id))) {
84 toRemoveNext.push(n.id);
85 foundNewNode = true;
86 }
87 });
88 if (foundNewNode) {
89 doNotInclude = toRemoveNext; // Reduces array length
90 childrenToRemove = childrenToRemove.concat(toRemoveNext);
91 }
92 } while (foundNewNode);
93
94 // Removes filtered out children in all left nodes with children
95 nodes = _.cloneDeep(nodes); // Clone objects to not change original objects
96 nodes = nodes.map((n) => {
97 if (!n.children) {
98 return n;
99 }
100 n.children = n.children.filter((id) => !childrenToRemove.includes(id));
101 return n;
102 });
103
104 return nodes;
105 }
106
107 static getFailureDomains(nodes: CrushNode[]): { [failureDomain: string]: CrushNode[] } {
108 const domains = {};
109 nodes.forEach((node) => {
110 const type = node.type;
111 if (!domains[type]) {
112 domains[type] = [];
113 }
114 domains[type].push(node);
115 });
116 return domains;
117 }
118
e306af50
TL
119 initCrushNodeSelection(
120 nodes: CrushNode[],
121 rootControl: AbstractControl,
122 failureControl: AbstractControl,
123 deviceControl: AbstractControl
124 ) {
125 this.nodes = nodes;
f6b5b4d7 126 this.idTree = CrushNodeSelectionClass.createIdTreeFromNodes(nodes);
e306af50 127 nodes.forEach((node) => {
f6b5b4d7 128 this.idTree[node.id] = node;
e306af50
TL
129 });
130 this.buckets = _.sortBy(
131 nodes.filter((n) => n.children),
132 'name'
133 );
134 this.controls = {
135 root: rootControl,
136 failure: failureControl,
137 device: deviceControl
138 };
139 this.preSelectRoot();
140 this.controls.root.valueChanges.subscribe(() => this.onRootChange());
141 this.controls.failure.valueChanges.subscribe(() => this.onFailureDomainChange());
142 this.controls.device.valueChanges.subscribe(() => this.onDeviceChange());
143 }
144
145 private preSelectRoot() {
146 const rootNode = this.nodes.find((node) => node.type === 'root');
147 this.silentSet(this.controls.root, rootNode);
148 this.onRootChange();
149 }
150
151 private silentSet(control: AbstractControl, value: any) {
152 control.setValue(value, { emitEvent: false });
153 }
154
155 private onRootChange() {
f6b5b4d7
TL
156 const nodes = CrushNodeSelectionClass.getSubNodes(this.controls.root.value, this.idTree);
157 const domains = CrushNodeSelectionClass.getFailureDomains(nodes);
e306af50
TL
158 Object.keys(domains).forEach((type) => {
159 if (domains[type].length <= 1) {
160 delete domains[type];
161 }
162 });
163 this.failureDomains = domains;
164 this.failureDomainKeys = Object.keys(domains).sort();
165 this.updateFailureDomain();
166 }
167
e306af50
TL
168 private updateFailureDomain() {
169 let failureDomain = this.getIncludedCustomValue(
170 this.controls.failure,
171 Object.keys(this.failureDomains)
172 );
173 if (failureDomain === '') {
174 failureDomain = this.setMostCommonDomain(this.controls.failure);
175 }
176 this.updateDevices(failureDomain);
177 }
178
179 private getIncludedCustomValue(control: AbstractControl, includedIn: string[]) {
180 return control.dirty && includedIn.includes(control.value) ? control.value : '';
181 }
182
183 private setMostCommonDomain(failureControl: AbstractControl): string {
184 let winner = { n: 0, type: '' };
185 Object.keys(this.failureDomains).forEach((type) => {
186 const n = this.failureDomains[type].length;
187 if (winner.n < n) {
188 winner = { n, type };
189 }
190 });
191 this.silentSet(failureControl, winner.type);
192 return winner.type;
193 }
194
195 private onFailureDomainChange() {
196 this.updateDevices();
197 }
198
199 private updateDevices(failureDomain: string = this.controls.failure.value) {
200 const subNodes = _.flatten(
f6b5b4d7
TL
201 this.failureDomains[failureDomain].map((node) =>
202 CrushNodeSelectionClass.getSubNodes(node, this.idTree)
203 )
e306af50
TL
204 );
205 this.allDevices = subNodes.filter((n) => n.device_class).map((n) => n.device_class);
206 this.devices = _.uniq(this.allDevices).sort();
207 const device =
208 this.devices.length === 1
209 ? this.devices[0]
210 : this.getIncludedCustomValue(this.controls.device, this.devices);
211 this.silentSet(this.controls.device, device);
212 this.onDeviceChange(device);
213 }
214
215 private onDeviceChange(deviceType: string = this.controls.device.value) {
216 this.deviceCount =
217 deviceType === ''
218 ? this.allDevices.length
219 : this.allDevices.filter((type) => type === deviceType).length;
220 }
221}