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)
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')
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
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']))
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
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')
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
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):
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):
"""
[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()"
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', () => {
let fixture: ComponentFixture<OsdCreationPreviewModalComponent>;
configureTestBed({
- imports: [HttpClientTestingModule, ReactiveFormsModule, SharedModule, RouterTestingModule],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ SharedModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ],
providers: [BsModalRef, i18nProviders],
declarations: [OsdCreationPreviewModalComponent]
});
beforeEach(() => {
fixture = TestBed.createComponent(OsdCreationPreviewModalComponent);
component = fixture.componentInstance;
- component.driveGroup = new DriveGroup();
fixture.detectChanges();
});
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',
})
export class OsdCreationPreviewModalComponent implements OnInit {
@Input()
- driveGroup: DriveGroup;
+ driveGroups: DriveGroups = {};
@Output()
submitAction = new EventEmitter();
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();
}
}
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();
+ }
+ );
}
}
--- /dev/null
+export interface DriveGroups {
+ [key: string]: object;
+}
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';
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';
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({
constructor(
public actionLabels: ActionLabelsI18n,
+ private authStorageService: AuthStorageService,
private i18n: I18n,
private orchService: OrchestratorService,
private router: Router,
}
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']);
});
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);
- });
});
})
);
}
-
- osdCreate(driveGroup: {}) {
- const request = {
- drive_group: driveGroup
- };
- return this.http.post(`${this.url}/osd`, request, { observe: 'response' });
- }
}
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');
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';
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}`);
}
'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,
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):
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):
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):
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
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):
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):
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)