]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Crush selection can handle any crush map
authorStephan Müller <smueller@suse.com>
Wed, 6 May 2020 08:13:28 +0000 (10:13 +0200)
committerStephan Müller <smueller@suse.com>
Tue, 2 Jun 2020 08:38:05 +0000 (10:38 +0200)
The crush selection class now provides a view static methods that can
take a crush map in search for sub nodes of a node and provide a list of
failure domains generated out of a list of nodes.

Fixes: https://tracker.ceph.com/issues/44620
Signed-off-by: Stephan Müller <smueller@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/classes/crush.node.selection.class.ts

index 59a26c5bd3601e6b69fc7542f692ed7835476330..fd67cdec30c0f888ab7b5e6233654dbdd4e074fb 100644 (file)
@@ -1,11 +1,13 @@
 import { FormControl } from '@angular/forms';
 
+import * as _ from 'lodash';
+
 import { configureTestBed, Mocks } from '../../../testing/unit-test-helper';
 import { CrushNode } from '../models/crush-node';
 import { CrushNodeSelectionClass } from './crush.node.selection.class';
 
 describe('CrushNodeSelectionService', () => {
-  const nodes = Mocks.getCrushNodes()
+  const nodes = Mocks.getCrushMap();
 
   let service: CrushNodeSelectionClass;
   let controls: {
@@ -22,15 +24,6 @@ describe('CrushNodeSelectionService', () => {
 
   // Expects that are used frequently
   const assert = {
-    failureDomains: (nodes: CrushNode[], types: string[]) => {
-      const expectation = {};
-      types.forEach((type) => (expectation[type] = nodes.filter((node) => node.type === type)));
-      const keys = service.failureDomainKeys;
-      expect(keys).toEqual(types);
-      keys.forEach((key) => {
-        expect(service.failureDomains[key].length).toBe(expectation[key].length);
-      });
-    },
     formFieldValues: (root: CrushNode, failureDomain: string, device: string) => {
       expect(controls.root.value).toEqual(root);
       expect(controls.failure.value).toBe(failureDomain);
@@ -44,6 +37,19 @@ describe('CrushNodeSelectionService', () => {
       const node = get.nodeByName(rootName);
       controls.root.setValue(node);
       assert.formFieldValues(node, expectedFailureDomain, expectedDevice);
+    },
+    failureDomainNodes: (
+      failureDomains: { [failureDomain: string]: CrushNode[] },
+      expected: { [failureDomains: string]: string[] | CrushNode[] }
+    ) => {
+      expect(Object.keys(failureDomains)).toEqual(Object.keys(expected));
+      Object.keys(failureDomains).forEach((key) => {
+        if (_.isString(expected[key][0])) {
+          expect(failureDomains[key]).toEqual(get.nodesByNames(expected[key] as string[]));
+        } else {
+          expect(failureDomains[key]).toEqual(expected[key]);
+        }
+      });
     }
   };
 
@@ -77,23 +83,31 @@ describe('CrushNodeSelectionService', () => {
     });
 
     it('has the following lists after init', () => {
-      assert.failureDomains(nodes, ['host', 'osd', 'osd-rack', 'rack']); // Not root as root only exist once
+      assert.failureDomainNodes(service.failureDomains, {
+        host: ['ssd-host', 'mix-host'],
+        osd: ['osd.1', 'osd.0', 'osd.2'],
+        rack: ['hdd-rack', 'ssd-rack'],
+        'osd-rack': ['osd2.0', 'osd2.1', 'osd3.0', 'osd3.1']
+      });
       expect(service.devices).toEqual(['hdd', 'ssd']);
     });
 
     it('has the following lists after selection of ssd-host', () => {
       controls.root.setValue(get.nodeByName('ssd-host'));
-      assert.failureDomains(get.nodesByNames(['osd.0', 'osd.1', 'osd.2']), ['osd']); // Not host as it only exist once
+      assert.failureDomainNodes(service.failureDomains, {
+        // Not host as it only exist once
+        osd: ['osd.1', 'osd.0', 'osd.2']
+      });
       expect(service.devices).toEqual(['ssd']);
     });
 
     it('has the following lists after selection of mix-host', () => {
       controls.root.setValue(get.nodeByName('mix-host'));
       expect(service.devices).toEqual(['hdd', 'ssd']);
-      assert.failureDomains(
-        get.nodesByNames(['hdd-rack', 'ssd-rack', 'osd2.0', 'osd2.1', 'osd3.0', 'osd3.1']),
-        ['osd-rack', 'rack']
-      );
+      assert.failureDomainNodes(service.failureDomains, {
+        rack: ['hdd-rack', 'ssd-rack'],
+        'osd-rack': ['osd2.0', 'osd2.1', 'osd3.0', 'osd3.1']
+      });
     });
   });
 
@@ -147,4 +161,60 @@ describe('CrushNodeSelectionService', () => {
       expect(service.deviceCount).toBe(3);
     });
   });
+
+  describe('search tree', () => {
+    it('returns the following list after searching for mix-host', () => {
+      const subNodes = CrushNodeSelectionClass.search(nodes, 'mix-host');
+      expect(subNodes).toEqual(
+        get.nodesByNames([
+          'mix-host',
+          'hdd-rack',
+          'osd2.0',
+          'osd2.1',
+          'ssd-rack',
+          'osd3.0',
+          'osd3.1'
+        ])
+      );
+    });
+
+    it('returns the following list after searching for mix-host with SSDs', () => {
+      const subNodes = CrushNodeSelectionClass.search(nodes, 'mix-host~ssd');
+      expect(subNodes.map((n) => n.name)).toEqual(['mix-host', 'ssd-rack', 'osd3.0', 'osd3.1']);
+    });
+
+    it('returns an empty array if node can not be found', () => {
+      expect(CrushNodeSelectionClass.search(nodes, 'not-there')).toEqual([]);
+    });
+
+    it('returns the following list after searching for mix-host failure domains', () => {
+      const subNodes = CrushNodeSelectionClass.search(nodes, 'mix-host');
+      assert.failureDomainNodes(CrushNodeSelectionClass.getFailureDomains(subNodes), {
+        host: ['mix-host'],
+        rack: ['hdd-rack', 'ssd-rack'],
+        'osd-rack': ['osd2.0', 'osd2.1', 'osd3.0', 'osd3.1']
+      });
+    });
+
+    it('returns the following list after searching for mix-host failure domains for a specific type', () => {
+      const subNodes = CrushNodeSelectionClass.search(nodes, 'mix-host~hdd');
+      const hddHost = _.cloneDeep(get.nodesByNames(['mix-host'])[0]);
+      hddHost.children = [-4];
+      assert.failureDomainNodes(CrushNodeSelectionClass.getFailureDomains(subNodes), {
+        host: [hddHost],
+        rack: ['hdd-rack'],
+        'osd-rack': ['osd2.0', 'osd2.1']
+      });
+      const ssdHost = _.cloneDeep(get.nodesByNames(['mix-host'])[0]);
+      ssdHost.children = [-5];
+      assert.failureDomainNodes(
+        CrushNodeSelectionClass.searchFailureDomains(nodes, 'mix-host~ssd'),
+        {
+          host: [ssdHost],
+          rack: ['ssd-rack'],
+          'osd-rack': ['osd3.0', 'osd3.1']
+        }
+      );
+    });
+  });
 });
index 7cc11df163fe392dc2f1fb1845693cb93a3b9647..e1cf4b0ec3caf85b564f0fdab3c6d84f79ccb781 100644 (file)
@@ -6,7 +6,7 @@ import { CrushNode } from '../models/crush-node';
 
 export class CrushNodeSelectionClass {
   private nodes: CrushNode[] = [];
-  private easyNodes: { [id: number]: CrushNode } = {};
+  private idTree: { [id: number]: CrushNode } = {};
   private allDevices: string[] = [];
   private controls: {
     root: AbstractControl;
@@ -20,6 +20,102 @@ export class CrushNodeSelectionClass {
   devices: string[] = [];
   deviceCount = 0;
 
+  static searchFailureDomains(
+    nodes: CrushNode[],
+    s: string
+  ): { [failureDomain: string]: CrushNode[] } {
+    return this.getFailureDomains(this.search(nodes, s));
+  }
+
+  /**
+   * Filters crush map for a node and it's tree.
+   * The node name as provided in crush rules attribute item_name is supported.
+   * This means that '$name~$deviceType' can be used and will result in a crush map
+   * that only include buckets with the specified device in use as their leaf.
+   */
+  static search(nodes: CrushNode[], s: string): CrushNode[] {
+    const [search, deviceType] = s.split('~'); // Used inside item_name in crush rules
+    const node = nodes.find((n) => ['name', 'id', 'type'].some((attr) => n[attr] === search));
+    if (!node) {
+      return [];
+    }
+    nodes = this.getSubNodes(node, this.createIdTreeFromNodes(nodes));
+    if (deviceType) {
+      nodes = this.filterNodesByDeviceType(nodes, deviceType);
+    }
+    return nodes;
+  }
+
+  static createIdTreeFromNodes(nodes: CrushNode[]): { [id: number]: CrushNode } {
+    const idTree = {};
+    nodes.forEach((node) => {
+      idTree[node.id] = node;
+    });
+    return idTree;
+  }
+
+  static getSubNodes(node: CrushNode, idTree: { [id: number]: CrushNode }): CrushNode[] {
+    let subNodes = [node]; // Includes parent node
+    if (!node.children) {
+      return subNodes;
+    }
+    node.children.forEach((id) => {
+      const childNode = idTree[id];
+      subNodes = subNodes.concat(this.getSubNodes(childNode, idTree));
+    });
+    return subNodes;
+  }
+
+  static filterNodesByDeviceType(nodes: CrushNode[], deviceType: string): any {
+    let doNotInclude = nodes
+      .filter((n) => n.device_class && n.device_class !== deviceType)
+      .map((n) => n.id);
+    let foundNewNode: boolean;
+    let childrenToRemove = doNotInclude;
+
+    // Filters out all unwanted nodes
+    do {
+      foundNewNode = false;
+      nodes = nodes.filter((n) => !doNotInclude.includes(n.id)); // Unwanted nodes
+      // Find nodes where all children were filtered
+      const toRemoveNext: number[] = [];
+      nodes.forEach((n) => {
+        if (n.children && n.children.every((id) => doNotInclude.includes(id))) {
+          toRemoveNext.push(n.id);
+          foundNewNode = true;
+        }
+      });
+      if (foundNewNode) {
+        doNotInclude = toRemoveNext; // Reduces array length
+        childrenToRemove = childrenToRemove.concat(toRemoveNext);
+      }
+    } while (foundNewNode);
+
+    // Removes filtered out children in all left nodes with children
+    nodes = _.cloneDeep(nodes); // Clone objects to not change original objects
+    nodes = nodes.map((n) => {
+      if (!n.children) {
+        return n;
+      }
+      n.children = n.children.filter((id) => !childrenToRemove.includes(id));
+      return n;
+    });
+
+    return nodes;
+  }
+
+  static getFailureDomains(nodes: CrushNode[]): { [failureDomain: string]: CrushNode[] } {
+    const domains = {};
+    nodes.forEach((node) => {
+      const type = node.type;
+      if (!domains[type]) {
+        domains[type] = [];
+      }
+      domains[type].push(node);
+    });
+    return domains;
+  }
+
   initCrushNodeSelection(
     nodes: CrushNode[],
     rootControl: AbstractControl,
@@ -27,8 +123,9 @@ export class CrushNodeSelectionClass {
     deviceControl: AbstractControl
   ) {
     this.nodes = nodes;
+    this.idTree = CrushNodeSelectionClass.createIdTreeFromNodes(nodes);
     nodes.forEach((node) => {
-      this.easyNodes[node.id] = node;
+      this.idTree[node.id] = node;
     });
     this.buckets = _.sortBy(
       nodes.filter((n) => n.children),
@@ -56,14 +153,8 @@ export class CrushNodeSelectionClass {
   }
 
   private onRootChange() {
-    const nodes = this.getSubNodes(this.controls.root.value);
-    const domains = {};
-    nodes.forEach((node) => {
-      if (!domains[node.type]) {
-        domains[node.type] = [];
-      }
-      domains[node.type].push(node);
-    });
+    const nodes = CrushNodeSelectionClass.getSubNodes(this.controls.root.value, this.idTree);
+    const domains = CrushNodeSelectionClass.getFailureDomains(nodes);
     Object.keys(domains).forEach((type) => {
       if (domains[type].length <= 1) {
         delete domains[type];
@@ -74,18 +165,6 @@ export class CrushNodeSelectionClass {
     this.updateFailureDomain();
   }
 
-  private getSubNodes(node: CrushNode): CrushNode[] {
-    let subNodes = [node]; // Includes parent node
-    if (!node.children) {
-      return subNodes;
-    }
-    node.children.forEach((id) => {
-      const childNode = this.easyNodes[id];
-      subNodes = subNodes.concat(this.getSubNodes(childNode));
-    });
-    return subNodes;
-  }
-
   private updateFailureDomain() {
     let failureDomain = this.getIncludedCustomValue(
       this.controls.failure,
@@ -119,7 +198,9 @@ export class CrushNodeSelectionClass {
 
   private updateDevices(failureDomain: string = this.controls.failure.value) {
     const subNodes = _.flatten(
-      this.failureDomains[failureDomain].map((node) => this.getSubNodes(node))
+      this.failureDomains[failureDomain].map((node) =>
+        CrushNodeSelectionClass.getSubNodes(node, this.idTree)
+      )
     );
     this.allDevices = subNodes.filter((n) => n.device_class).map((n) => n.device_class);
     this.devices = _.uniq(this.allDevices).sort();