]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: support multiple DriveGroups when creating OSDs 32678/head
authorKiefer Chang <kiefer.chang@suse.com>
Thu, 16 Jan 2020 09:44:27 +0000 (17:44 +0800)
committerKiefer Chang <kiefer.chang@suse.com>
Fri, 6 Mar 2020 03:15:18 +0000 (11:15 +0800)
The `create_osds` call in orchestrator uses multiple named DriveGroups as
the parameter. Adapt the change in Dashboard.

Some minor polishes:
- Use task manager to wrap the operation.
- The submit button in Preview modal is changed from `Add` to `Create`.
- POST `/api/osd` to create OSDs:
  - Bare OSDs for OSD service container
    {
        "method": "bare",
        "data": {
            "uuid": "xxxx",
            "svc_id": 5
        }
    }

  - OSDs with devices (DriveGroups)

    {
        "method": "drive_groups",
        "data": {
            < drive group spec here>
        }
    }
- `/orchestrator/osd` endpoint is removed.

Fixes: https://tracker.ceph.com/issues/43615
Signed-off-by: Kiefer Chang <kiefer.chang@suse.com>
17 files changed:
qa/tasks/mgr/dashboard/test_orchestrator.py
qa/tasks/mgr/dashboard/test_osd.py
src/pybind/mgr/dashboard/controllers/orchestrator.py
src/pybind/mgr/dashboard/controllers/osd.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-creation-preview-modal/osd-creation-preview-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/drive-groups.interface.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/services/orchestrator.py
src/pybind/mgr/dashboard/tests/test_orchestrator.py
src/pybind/mgr/dashboard/tests/test_osd.py

index 0f0a22431a9087cae9764af5f268129608a1f3b4..9f4204379ac9afd9518a34134f4f9410683efc6d 100644 (file)
@@ -132,56 +132,3 @@ class OrchestratorControllerTest(DashboardTestCase):
         self.assertStatus(200)
         self.assertEqual(len(data), 1)
         self._validate_inventory(node, data[0])
-
-        """
-    def test_service_list(self):
-        # get all services
-        data = self._get(self.URL_SERVICE)
-        self.assertStatus(200)
-
-        sorting_key = lambda svc: '%(nodename)s.%(service_type)s.%(service_instance)s' % svc
-        test_services = sorted(self.test_data_services, key=sorting_key)
-        resp_services = sorted(data, key=sorting_key)
-        self.assertEqual(len(test_services), len(resp_services))
-        for test, resp in zip(test_services, resp_services):
-            self._validate_service(test, resp)
-
-        # get service by hostname
-        nodename = self.test_data_services[-1]['nodename']
-        test_services = sorted(filter(lambda svc: svc['nodename'] == nodename, test_services),
-                          key=sorting_key)
-        data = self._get('{}?hostname={}'.format(self.URL_SERVICE, nodename))
-        resp_services = sorted(data, key=sorting_key)
-        for test, resp in zip(test_services, resp_services):
-            self._validate_service(test, resp)
-        """
-
-    def test_create_osds(self):
-        data = {
-            'drive_group': {
-                'host_pattern': '*',
-                'data_devices': {
-                    'vendor': 'abc',
-                    'model': 'cba',
-                    'rotational': True,
-                    'size': '4 TB'
-                },
-                'wal_devices': {
-                    'vendor': 'def',
-                    'model': 'fed',
-                    'rotational': False,
-                    'size': '1 TB'
-                },
-                'db_devices': {
-                    'vendor': 'ghi',
-                    'model': 'ihg',
-                    'rotational': False,
-                    'size': '512 GB'
-                },
-                'wal_slots': 5,
-                'db_slots': 5,
-                'encrypted': True
-            }
-        }
-        self._post(self.URL_OSD, data)
-        self.assertStatus(201)
index 111c440ab45428a08dfa36da9fb66e80ca7641fc..0c17be4f6a104cbf9da19bb6aa126d2e21c368de 100644 (file)
@@ -11,6 +11,13 @@ class OsdTest(DashboardTestCase):
 
     AUTH_ROLES = ['cluster-manager']
 
+    @classmethod
+    def setUpClass(cls):
+        super(OsdTest, cls).setUpClass()
+        cls._load_module('test_orchestrator')
+        cmd = ['orch', 'set', 'backend', 'test_orchestrator']
+        cls.mgr_cluster.mon_manager.raw_cluster_cmd(*cmd)
+
     def tearDown(self):
         self._post('/api/osd/0/mark_in')
 
@@ -82,9 +89,13 @@ class OsdTest(DashboardTestCase):
 
     def test_create_lost_destroy_remove(self):
         # Create
-        self._post('/api/osd', {
-            'uuid': 'f860ca2e-757d-48ce-b74a-87052cad563f',
-            'svc_id': 5
+        self._task_post('/api/osd', {
+            'method': 'bare',
+            'data': {
+                'uuid': 'f860ca2e-757d-48ce-b74a-87052cad563f',
+                'svc_id': 5
+            },
+            'tracking_id': 'bare-5'
         })
         self.assertStatus(201)
         # Lost
@@ -97,6 +108,40 @@ class OsdTest(DashboardTestCase):
         self._post('/api/osd/5/purge')
         self.assertStatus(200)
 
+    def test_create_with_drive_group(self):
+        data = {
+            'method': 'drive_groups',
+            'data': {
+                'test': {
+                    'host_pattern': '*',
+                    'data_devices': {
+                        'vendor': 'abc',
+                        'model': 'cba',
+                        'rotational': True,
+                        'size': '4 TB'
+                    },
+                    'wal_devices': {
+                        'vendor': 'def',
+                        'model': 'fed',
+                        'rotational': False,
+                        'size': '1 TB'
+                    },
+                    'db_devices': {
+                        'vendor': 'ghi',
+                        'model': 'ihg',
+                        'rotational': False,
+                        'size': '512 GB'
+                    },
+                    'wal_slots': 5,
+                    'db_slots': 5,
+                    'encrypted': True
+                }
+            },
+            'tracking_id': 'test'
+        }
+        self._post('/api/osd', data)
+        self.assertStatus(201)
+
     def test_safe_to_destroy(self):
         osd_dump = json.loads(self._ceph_cmd(['osd', 'dump', '-f', 'json']))
         max_id = max(map(lambda e: e['osd'], osd_dump['osds']))
index a1f088ba2c26105088840ead8cb33a29432ce87e..c9172b83942809e70220865c080fee473ee802bf 100644 (file)
@@ -4,11 +4,6 @@ import os.path
 
 import time
 
-try:
-    from ceph.deployment.drive_group import DriveGroupSpec, DriveGroupValidationError
-except ImportError:
-    pass
-
 from . import ApiController, Endpoint, ReadPermission, UpdatePermission
 from . import RESTController, Task
 from .. import mgr
@@ -121,15 +116,3 @@ class OrchestratorInventory(RESTController):
                 else:
                     device['osd_ids'] = []
         return inventory_hosts
-
-
-@ApiController('/orchestrator/osd', Scope.OSD)
-class OrchestratorOsd(RESTController):
-
-    @raise_if_no_orchestrator
-    def create(self, drive_group):
-        orch = OrchClient.instance()
-        try:
-            orch.osds.create(DriveGroupSpec.from_json(drive_group))
-        except (ValueError, TypeError, DriveGroupValidationError) as e:
-            raise DashboardException(e, component='osd')
index 373f66329adcfe024e7dc404234a09aeab1ccd96..466130d392ae2dce4f54311e12f0c12905b80f09 100644 (file)
@@ -3,13 +3,18 @@ from __future__ import absolute_import
 import json
 import logging
 
+from ceph.deployment.drive_group import DriveGroupSpecs, DriveGroupValidationError
 from mgr_util import get_most_recent_rate
 
-from . import ApiController, RESTController, Endpoint, ReadPermission, UpdatePermission
+from . import ApiController, RESTController, Endpoint, Task
+from . import CreatePermission, ReadPermission, UpdatePermission
+from .orchestrator import raise_if_no_orchestrator
 from .. import mgr
+from ..exceptions import DashboardException
 from ..security import Scope
 from ..services.ceph_service import CephService, SendCommandError
-from ..services.exception import handle_send_command_error
+from ..services.exception import handle_send_command_error, handle_orchestrator_error
+from ..services.orchestrator import OrchClient
 from ..tools import str_to_bool
 try:
     from typing import Dict, List, Any, Union  # noqa: F401 pylint: disable=unused-import
@@ -20,6 +25,10 @@ except ImportError:
 logger = logging.getLogger('controllers.osd')
 
 
+def osd_task(name, metadata, wait_for=2.0):
+    return Task("osd/{}".format(name), metadata, wait_for)
+
+
 @ApiController('/osd', Scope.OSD)
 class Osd(RESTController):
     def list(self):
@@ -170,20 +179,47 @@ class Osd(RESTController):
             id=int(svc_id),
             yes_i_really_mean_it=True)
 
-    def create(self, uuid=None, svc_id=None):
-        """
-        :param uuid: Will be set automatically if the OSD starts up.
-        :param id: The ID is only used if a valid uuid is given.
-        :return:
+    def _create_bare(self, data):
+        """Create a OSD container that has no associated device.
+
+        :param data: contain attributes to create a bare OSD.
+        :    `uuid`: will be set automatically if the OSD starts up
+        :    `svc_id`: the ID is only used if a valid uuid is given.
         """
+        try:
+            uuid = data['uuid']
+            svc_id = int(data['svc_id'])
+        except (KeyError, ValueError) as e:
+            raise DashboardException(e, component='osd', http_status_code=400)
+
         result = CephService.send_command(
-            'mon', 'osd create', id=int(svc_id), uuid=uuid)
+            'mon', 'osd create', id=svc_id, uuid=uuid)
         return {
             'result': result,
-            'svc_id': int(svc_id),
+            'svc_id': svc_id,
             'uuid': uuid,
         }
 
+    @raise_if_no_orchestrator
+    @handle_orchestrator_error('osd')
+    def _create_with_drive_groups(self, drive_groups):
+        """Create OSDs with DriveGroups."""
+        orch = OrchClient.instance()
+        try:
+            orch.osds.create(DriveGroupSpecs(drive_groups).drive_groups)
+        except (ValueError, TypeError, DriveGroupValidationError) as e:
+            raise DashboardException(e, component='osd')
+
+    @CreatePermission
+    @osd_task('create', {'tracking_id': '{tracking_id}'})
+    def create(self, method, data, tracking_id):  # pylint: disable=W0622
+        if method == 'bare':
+            return self._create_bare(data)
+        if method == 'drive_groups':
+            return self._create_with_drive_groups(data)
+        raise DashboardException(
+            component='osd', http_status_code=400, msg='Unknown method: {}'.format(method))
+
     @RESTController.Resource('POST')
     def purge(self, svc_id):
         """
index fd0acad491a5602d4d876eddee9bc38f5795076b..1e4b6d989ea5ede88195c04611659f8fe0c025c8 100644 (file)
@@ -7,8 +7,8 @@
           [formGroup]="formGroup"
           novalidate>
       <div class="modal-body">
-        <h3>Drive Group</h3>
-        <pre>{{ driveGroup.spec | json}}</pre>
+        <h3 i18n>DriveGroups</h3>
+        <pre>{{ driveGroups | json}}</pre>
       </div>
       <div class="modal-footer">
         <cd-submit-button (submitAction)="onSubmit()"
index 36fe3b0e57c5b032b1ac18b585294e672c93f166..a428903f2ec685e71fd4c979b3d586c9ef9a24c6 100644 (file)
@@ -4,10 +4,10 @@ import { ReactiveFormsModule } from '@angular/forms';
 import { RouterTestingModule } from '@angular/router/testing';
 
 import { BsModalRef } from 'ngx-bootstrap/modal';
+import { ToastrModule } from 'ngx-toastr';
 
 import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
 import { SharedModule } from '../../../../shared/shared.module';
-import { DriveGroup } from '../osd-form/drive-group.model';
 import { OsdCreationPreviewModalComponent } from './osd-creation-preview-modal.component';
 
 describe('OsdCreationPreviewModalComponent', () => {
@@ -15,7 +15,13 @@ describe('OsdCreationPreviewModalComponent', () => {
   let fixture: ComponentFixture<OsdCreationPreviewModalComponent>;
 
   configureTestBed({
-    imports: [HttpClientTestingModule, ReactiveFormsModule, SharedModule, RouterTestingModule],
+    imports: [
+      HttpClientTestingModule,
+      ReactiveFormsModule,
+      SharedModule,
+      RouterTestingModule,
+      ToastrModule.forRoot()
+    ],
     providers: [BsModalRef, i18nProviders],
     declarations: [OsdCreationPreviewModalComponent]
   });
@@ -23,7 +29,6 @@ describe('OsdCreationPreviewModalComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(OsdCreationPreviewModalComponent);
     component = fixture.componentInstance;
-    component.driveGroup = new DriveGroup();
     fixture.detectChanges();
   });
 
index 703ce037a138f51814c5fe984165a38393f715a5..ae35da6d6b5406d54010d895ab891fe09b76028a 100644 (file)
@@ -1,12 +1,15 @@
 import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
 
+import * as _ from 'lodash';
 import { BsModalRef } from 'ngx-bootstrap/modal';
 
-import { OrchestratorService } from '../../../../shared/api/orchestrator.service';
-import { ActionLabelsI18n } from '../../../../shared/constants/app.constants';
+import { OsdService } from '../../../../shared/api/osd.service';
+import { ActionLabelsI18n, URLVerbs } from '../../../../shared/constants/app.constants';
 import { CdFormBuilder } from '../../../../shared/forms/cd-form-builder';
 import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
-import { DriveGroup } from '../osd-form/drive-group.model';
+import { FinishedTask } from '../../../../shared/models/finished-task';
+import { TaskWrapperService } from '../../../../shared/services/task-wrapper.service';
+import { DriveGroups } from '../osd-form/drive-groups.interface';
 
 @Component({
   selector: 'cd-osd-creation-preview-modal',
@@ -15,7 +18,7 @@ import { DriveGroup } from '../osd-form/drive-group.model';
 })
 export class OsdCreationPreviewModalComponent implements OnInit {
   @Input()
-  driveGroup: DriveGroup;
+  driveGroups: DriveGroups = {};
 
   @Output()
   submitAction = new EventEmitter();
@@ -27,9 +30,10 @@ export class OsdCreationPreviewModalComponent implements OnInit {
     public bsModalRef: BsModalRef,
     public actionLabels: ActionLabelsI18n,
     private formBuilder: CdFormBuilder,
-    private orchService: OrchestratorService
+    private osdService: OsdService,
+    private taskWrapper: TaskWrapperService
   ) {
-    this.action = actionLabels.ADD;
+    this.action = actionLabels.CREATE;
     this.createForm();
   }
 
@@ -40,15 +44,22 @@ export class OsdCreationPreviewModalComponent implements OnInit {
   }
 
   onSubmit() {
-    this.orchService.osdCreate(this.driveGroup.spec).subscribe(
-      undefined,
-      () => {
-        this.formGroup.setErrors({ cdSubmitButton: true });
-      },
-      () => {
-        this.submitAction.emit();
-        this.bsModalRef.hide();
-      }
-    );
+    this.taskWrapper
+      .wrapTaskAroundCall({
+        task: new FinishedTask('osd/' + URLVerbs.CREATE, {
+          tracking_id: _.join(_.keys(this.driveGroups), ', ')
+        }),
+        call: this.osdService.create(this.driveGroups)
+      })
+      .subscribe(
+        undefined,
+        () => {
+          this.formGroup.setErrors({ cdSubmitButton: true });
+        },
+        () => {
+          this.submitAction.emit();
+          this.bsModalRef.hide();
+        }
+      );
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/drive-groups.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/drive-groups.interface.ts
new file mode 100644 (file)
index 0000000..bccb7c9
--- /dev/null
@@ -0,0 +1,3 @@
+export interface DriveGroups {
+  [key: string]: object;
+}
index 15fed89766e976ac2f8ee10e04684808b04c985a..56187719bc845fd00f92bb2e55decc451244040e 100644 (file)
@@ -4,7 +4,7 @@ import { Router } from '@angular/router';
 
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import * as _ from 'lodash';
-import { BsModalService, ModalOptions } from 'ngx-bootstrap/modal';
+import { BsModalService } from 'ngx-bootstrap/modal';
 
 import { OrchestratorService } from '../../../../shared/api/orchestrator.service';
 import { SubmitButtonComponent } from '../../../../shared/components/submit-button/submit-button.component';
@@ -13,6 +13,7 @@ import { Icons } from '../../../../shared/enum/icons.enum';
 import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
 import { CdTableColumn } from '../../../../shared/models/cd-table-column';
 import { CephReleaseNamePipe } from '../../../../shared/pipes/ceph-release-name.pipe';
+import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
 import { SummaryService } from '../../../../shared/services/summary.service';
 import { InventoryDevice } from '../../inventory/inventory-devices/inventory-device.model';
 import { OsdCreationPreviewModalComponent } from '../osd-creation-preview-modal/osd-creation-preview-modal.component';
@@ -20,6 +21,7 @@ import { DevicesSelectionChangeEvent } from '../osd-devices-selection-groups/dev
 import { DevicesSelectionClearEvent } from '../osd-devices-selection-groups/devices-selection-clear-event.interface';
 import { OsdDevicesSelectionGroupsComponent } from '../osd-devices-selection-groups/osd-devices-selection-groups.component';
 import { DriveGroup } from './drive-group.model';
+import { DriveGroups } from './drive-groups.interface';
 import { OsdFeature } from './osd-feature.interface';
 
 @Component({
@@ -67,6 +69,7 @@ export class OsdFormComponent implements OnInit {
 
   constructor(
     public actionLabels: ActionLabelsI18n,
+    private authStorageService: AuthStorageService,
     private i18n: I18n,
     private orchService: OrchestratorService,
     private router: Router,
@@ -223,12 +226,14 @@ export class OsdFormComponent implements OnInit {
   }
 
   submit() {
-    const options: ModalOptions = {
-      initialState: {
-        driveGroup: this.driveGroup
-      }
+    // use user name and timestamp for drive group name
+    const user = this.authStorageService.getUsername();
+    const driveGroups: DriveGroups = {
+      [`dashboard-${user}-${_.now()}`]: this.driveGroup.spec
     };
-    const modalRef = this.bsModalService.show(OsdCreationPreviewModalComponent, options);
+    const modalRef = this.bsModalService.show(OsdCreationPreviewModalComponent, {
+      initialState: { driveGroups: driveGroups }
+    });
     modalRef.content.submitAction.subscribe(() => {
       this.router.navigate(['/osd']);
     });
index b68aefcfba212706044bf2cf434222a86d1c60c5..e8ca64cd39773a2dd3803aac5867f81dbdde3ce8 100644 (file)
@@ -45,16 +45,4 @@ describe('OrchestratorService', () => {
     const req = httpTesting.expectOne(`${apiPath}/inventory?hostname=${host}`);
     expect(req.request.method).toBe('GET');
   });
-
-  it('should call osdCreate', () => {
-    const data = {
-      drive_group: {
-        host_pattern: '*'
-      }
-    };
-    service.osdCreate(data['drive_group']).subscribe();
-    const req = httpTesting.expectOne(`${apiPath}/osd`);
-    expect(req.request.method).toBe('POST');
-    expect(req.request.body).toEqual(data);
-  });
 });
index 8b966448cbee4d0c9648c945fd18a9349d5cee2f..6fbd44d2fa5d5e675b4f45e081f837ecaa151d01 100644 (file)
@@ -48,11 +48,4 @@ export class OrchestratorService {
       })
     );
   }
-
-  osdCreate(driveGroup: {}) {
-    const request = {
-      drive_group: driveGroup
-    };
-    return this.http.post(`${this.url}/osd`, request, { observe: 'response' });
-  }
 }
index cc92721c09d7f4a3f24fccc0b28d1d82495873df..3f1abd4ace4f749842995145b2862d139be3cc2e 100644 (file)
@@ -26,6 +26,31 @@ describe('OsdService', () => {
     expect(service).toBeTruthy();
   });
 
+  it('should call create', () => {
+    const post_data = {
+      method: 'drive_groups',
+      data: {
+        all_hdd: {
+          host_pattern: '*',
+          data_devices: {
+            rotational: true
+          }
+        },
+        host1_ssd: {
+          host_pattern: 'host1',
+          data_devices: {
+            rotational: false
+          }
+        }
+      },
+      tracking_id: 'all_hdd, host1_ssd'
+    };
+    service.create(post_data.data).subscribe();
+    const req = httpTesting.expectOne('api/osd');
+    expect(req.request.method).toBe('POST');
+    expect(req.request.body).toEqual(post_data);
+  });
+
   it('should call getList', () => {
     service.getList().subscribe();
     const req = httpTesting.expectOne('api/osd');
index 2a03ea66c655ef528e819dd876acdb3bcb8d21b8..02d99fa1f3062d892b4bf4ad6b8fdd216e7c3b70 100644 (file)
@@ -4,6 +4,8 @@ import { Injectable } from '@angular/core';
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import { map } from 'rxjs/operators';
 
+import * as _ from 'lodash';
+import { DriveGroups } from '../../ceph/cluster/osd/osd-form/drive-groups.interface';
 import { CdDevice } from '../models/devices';
 import { SmartDataResponseV1 } from '../models/smart';
 import { DeviceService } from '../services/device.service';
@@ -62,6 +64,15 @@ export class OsdService {
 
   constructor(private http: HttpClient, private i18n: I18n, private deviceService: DeviceService) {}
 
+  create(driveGroups: DriveGroups) {
+    const request = {
+      method: 'drive_groups',
+      data: driveGroups,
+      tracking_id: _.join(_.keys(driveGroups), ', ')
+    };
+    return this.http.post(this.path, request, { observe: 'response' });
+  }
+
   getList() {
     return this.http.get(`${this.path}`);
   }
index 51d1ff06dfec148d27036c24c0a31c7d8e3d49b4..2faf6134283992555567c21c6a14e1e605970c0f 100644 (file)
@@ -166,6 +166,12 @@ export class TaskMessageService {
     'host/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
       this.host(metadata)
     ),
+    // OSD tasks
+    'osd/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+      this.i18n(`OSDs (DriveGroups: {{tracking_id}})`, {
+        tracking_id: metadata.tracking_id
+      })
+    ),
     // Pool tasks
     'pool/create': this.newTaskMessage(
       this.commonOperations.create,
index 1d66a9e76830fdef59af650f50d869997f45eeac..b863e20a88196ac8f2a120b4f904b8bf4f9ffa79 100644 (file)
@@ -105,8 +105,8 @@ class ServiceManager(ResourceManager):
 
 class OsdManager(ResourceManager):
     @wait_api_result
-    def create(self, drive_group):
-        return self.api.create_osds([drive_group])
+    def create(self, drive_groups):
+        return self.api.create_osds(drive_groups)
 
 
 class OrchClient(object):
index ee6ea44fa60ee818dbd4862a4fd3442614c33df4..714d59c08565fb98fdd3f7bbd940003067c53893 100644 (file)
@@ -11,23 +11,19 @@ from .. import mgr
 from ..controllers.orchestrator import get_device_osd_map
 from ..controllers.orchestrator import Orchestrator
 from ..controllers.orchestrator import OrchestratorInventory
-from ..controllers.orchestrator import OrchestratorOsd
 
 
 class OrchestratorControllerTest(ControllerTestCase):
     URL_STATUS = '/api/orchestrator/status'
     URL_INVENTORY = '/api/orchestrator/inventory'
-    URL_OSD = '/api/orchestrator/osd'
 
     @classmethod
     def setup_server(cls):
         # pylint: disable=protected-access
         Orchestrator._cp_config['tools.authenticate.on'] = False
         OrchestratorInventory._cp_config['tools.authenticate.on'] = False
-        OrchestratorOsd._cp_config['tools.authenticate.on'] = False
         cls.setup_controllers([Orchestrator,
-                               OrchestratorInventory,
-                               OrchestratorOsd])
+                               OrchestratorInventory])
 
     @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')
     def test_status_get(self, instance):
@@ -120,28 +116,6 @@ class OrchestratorControllerTest(ControllerTestCase):
         self._get(self.URL_INVENTORY)
         self.assertStatus(503)
 
-    @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')
-    def test_osd_create(self, instance):
-        # with orchestrator service
-        fake_client = mock.Mock()
-        fake_client.available.return_value = False
-        instance.return_value = fake_client
-        self._post(self.URL_OSD, {})
-        self.assertStatus(503)
-
-        # without orchestrator service
-        fake_client.available.return_value = True
-        # incorrect drive group
-        self._post(self.URL_OSD, {'drive_group': {}})
-        self.assertStatus(400)
-
-        # correct drive group
-        dg = {
-            'host_pattern': '*'
-        }
-        self._post(self.URL_OSD, {'drive_group': dg})
-        self.assertStatus(201)
-
 
 class TestOrchestrator(unittest.TestCase):
     def test_get_device_osd_map(self):
index 7ef351aaceaeaaaae338dee3eda23b3cff9d7ad9..0cc4ca8d8927708d382573e86df936933d995b49 100644 (file)
@@ -5,12 +5,14 @@ import uuid
 from contextlib import contextmanager
 
 try:
-    from mock import patch
+    import mock
 except ImportError:
-    from unittest.mock import patch
+    from unittest import mock
+from ceph.deployment.drive_group import DeviceSelection, DriveGroupSpec
 
 from . import ControllerTestCase
 from ..controllers.osd import Osd
+from ..tools import NotificationQueue, TaskManager
 from .. import mgr
 from .helper import update_dict
 
@@ -205,6 +207,12 @@ class OsdTest(ControllerTestCase):
     def setup_server(cls):
         Osd._cp_config['tools.authenticate.on'] = False  # pylint: disable=protected-access
         cls.setup_controllers([Osd])
+        NotificationQueue.start_queue()
+        TaskManager.init()
+
+    @classmethod
+    def tearDownClass(cls):
+        NotificationQueue.stop()
 
     @contextmanager
     def _mock_osd_list(self, osd_stat_ids, osdmap_tree_node_ids, osdmap_ids):
@@ -221,10 +229,10 @@ class OsdTest(ControllerTestCase):
                 return {path: OsdHelper.gen_mgr_get_counter()}
             raise NotImplementedError()
 
-        with patch.object(Osd, 'get_osd_map', return_value=OsdHelper.gen_osdmap(osdmap_ids)):
-            with patch.object(mgr, 'get', side_effect=mgr_get_replacement):
-                with patch.object(mgr, 'get_counter', side_effect=mgr_get_counter_replacement):
-                    with patch.object(mgr, 'get_latest', return_value=1146609664):
+        with mock.patch.object(Osd, 'get_osd_map', return_value=OsdHelper.gen_osdmap(osdmap_ids)):
+            with mock.patch.object(mgr, 'get', side_effect=mgr_get_replacement):
+                with mock.patch.object(mgr, 'get_counter', side_effect=mgr_get_counter_replacement):
+                    with mock.patch.object(mgr, 'get_latest', return_value=1146609664):
                         yield
 
     def test_osd_list_aggregation(self):
@@ -241,3 +249,68 @@ class OsdTest(ControllerTestCase):
             self._get('/api/osd')
             self.assertEqual(len(self.json_body()), 2, 'It should display two OSDs without failure')
             self.assertStatus(200)
+
+    @mock.patch('dashboard.controllers.osd.CephService')
+    def test_osd_create_bare(self, ceph_service):
+        ceph_service.send_command.return_value = '5'
+        data = {
+            'method': 'bare',
+            'data': {
+                'uuid': 'f860ca2e-757d-48ce-b74a-87052cad563f',
+                'svc_id': 5
+            },
+            'tracking_id': 'bare-5'
+        }
+        self._task_post('/api/osd', data)
+        self.assertStatus(201)
+        ceph_service.send_command.assert_called()
+
+    @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')
+    def test_osd_create_with_drive_groups(self, instance):
+        # without orchestrator service
+        fake_client = mock.Mock()
+        instance.return_value = fake_client
+
+        # Valid DriveGroups
+        data = {
+            'method': 'drive_groups',
+            'data': {
+                'all_hdd': {
+                    'host_pattern': '*',
+                    'data_devices': {
+                        'rotational': True
+                    }
+                },
+                'b_ssd': {
+                    'host_pattern': 'b',
+                    'data_devices': {
+                        'rotational': False
+                    }
+                }
+            },
+            'tracking_id': 'all_hdd, b_ssd'
+        }
+
+        # Without orchestrator service
+        fake_client.available.return_value = False
+        self._task_post('/api/osd', data)
+        self.assertStatus(503)
+
+        # With orchestrator service
+        fake_client.available.return_value = True
+        self._task_post('/api/osd', data)
+        self.assertStatus(201)
+        fake_client.osds.create.assert_called_with(
+            [DriveGroupSpec(host_pattern='*',
+                            name='all_hdd',
+                            data_devices=DeviceSelection(rotational=True)),
+             DriveGroupSpec(host_pattern='b',
+                            name='b_ssd',
+                            data_devices=DeviceSelection(rotational=False))])
+
+        # Invalid DriveGroups
+        data['data']['b'] = {
+            'host_pattern1': 'aa'
+        }
+        self._task_post('/api/osd', data)
+        self.assertStatus(400)