# -*- coding: utf-8 -*-
from __future__ import absolute_import
+import time
+
import cherrypy
-from ceph.deployment.drive_group import DriveGroupSpec, DriveGroupValidationError
-from . import ApiController, Endpoint, ReadPermission
+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
from ..exceptions import DashboardException
from ..security import Scope
+from ..services.exception import handle_orchestrator_error
from ..services.orchestrator import OrchClient
-from ..tools import wraps
+from ..tools import TaskManager, wraps
def get_device_osd_map():
def status(self):
return OrchClient.instance().status()
+ @Endpoint(method='POST')
+ @UpdatePermission
+ @raise_if_no_orchestrator
+ @handle_orchestrator_error('osd')
+ @orchestrator_task('identify_device', ['{hostname}', '{device}'])
+ def identify_device(self, hostname, device, duration):
+ # type: (str, str, int) -> None
+ """
+ Identify a device by switching on the device light for N seconds.
+ :param hostname: The hostname of the device to process.
+ :param device: The device identifier to process, e.g. ``ABC1234DEF567-1R1234_ABC8DE0Q``.
+ :param duration: The duration in seconds how long the LED should flash.
+ """
+ orch = OrchClient.instance()
+ TaskManager.current_task().set_progress(0)
+ orch.blink_device_light(hostname, device, 'ident', True)
+ for i in range(int(duration)):
+ percentage = int(round(i / float(duration) * 100))
+ TaskManager.current_task().set_progress(percentage)
+ time.sleep(1)
+ orch.blink_device_light(hostname, device, 'ident', False)
+ TaskManager.current_task().set_progress(100)
+
@ApiController('/orchestrator/inventory', Scope.HOSTS)
class OrchestratorInventory(RESTController):
import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { TabsModule } from 'ngx-bootstrap/tabs';
+import { ToastrModule } from 'ngx-toastr';
import { of } from 'rxjs';
import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
CephModule,
CoreModule,
CephSharedModule,
- SharedModule
+ SharedModule,
+ ToastrModule.forRoot()
],
declarations: [],
providers: [i18nProviders]
[selectionType]="selectionType"
columnMode="flex"
[autoReload]="false"
- [searchField]="false">
+ [searchField]="false"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
<div class="table-filters form-inline"
*ngIf="filters.length !== 0">
<div class="form-group filter tc_filter"
+import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { getterForProp } from '@swimlane/ngx-datatable/release/utils';
import * as _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
import {
configureTestBed,
];
configureTestBed({
- imports: [FormsModule, SharedModule],
+ imports: [FormsModule, HttpClientTestingModule, SharedModule, ToastrModule.forRoot()],
providers: [i18nProviders],
declarations: [InventoryDevicesComponent]
});
import { getterForProp } from '@swimlane/ngx-datatable/release/utils';
import * as _ from 'lodash';
+import { BsModalService } from 'ngx-bootstrap/modal';
+import { OrchestratorService } from '../../../../shared/api/orchestrator.service';
+import { FormModalComponent } from '../../../../shared/components/form-modal/form-modal.component';
import { CellTemplate } from '../../../../shared/enum/cell-template.enum';
import { Icons } from '../../../../shared/enum/icons.enum';
+import { NotificationType } from '../../../../shared/enum/notification-type.enum';
+import { CdTableAction } from '../../../../shared/models/cd-table-action';
import { CdTableColumn } from '../../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
+import { Permission } from '../../../../shared/models/permissions';
import { DimlessBinaryPipe } from '../../../../shared/pipes/dimless-binary.pipe';
+import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
+import { NotificationService } from '../../../../shared/services/notification.service';
import { InventoryDeviceFilter } from './inventory-device-filter.interface';
import { InventoryDeviceFiltersChangeEvent } from './inventory-device-filters-change-event.interface';
import { InventoryDevice } from './inventory-device.model';
icons = Icons;
columns: Array<CdTableColumn> = [];
filters: InventoryDeviceFilter[] = [];
+ selection: CdTableSelection = new CdTableSelection();
+ permission: Permission;
+ tableActions: CdTableAction[];
- constructor(private dimlessBinary: DimlessBinaryPipe, private i18n: I18n) {}
+ constructor(
+ private authStorageService: AuthStorageService,
+ private dimlessBinary: DimlessBinaryPipe,
+ private i18n: I18n,
+ private modalService: BsModalService,
+ private notificationService: NotificationService,
+ private orchService: OrchestratorService
+ ) {}
ngOnInit() {
+ this.permission = this.authStorageService.getPermissions().osd;
+ this.tableActions = [
+ {
+ permission: 'update',
+ icon: Icons.show,
+ click: () => this.identifyDevice(),
+ name: this.i18n('Identify'),
+ disable: () => !this.selection.hasSingleSelection,
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
+ visible: () => _.isString(this.selectionType)
+ }
+ ];
const columns = [
{
name: this.i18n('Hostname'),
filterOutDevices: this.filterOutDevices
});
}
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ identifyDevice() {
+ const selected = this.selection.first();
+ const hostname = selected.hostname;
+ const device = selected.path || selected.device_id;
+ this.modalService.show(FormModalComponent, {
+ initialState: {
+ titleText: this.i18n(`Identify device {{device}}`, { device }),
+ message: this.i18n('Please enter the duration how long to blink the LED.'),
+ fields: [
+ {
+ type: 'select',
+ name: 'duration',
+ value: 300,
+ required: true,
+ options: [
+ { text: this.i18n('1 minute'), value: 60 },
+ { text: this.i18n('2 minutes'), value: 120 },
+ { text: this.i18n('5 minutes'), value: 300 },
+ { text: this.i18n('10 minutes'), value: 600 },
+ { text: this.i18n('15 minutes'), value: 900 }
+ ]
+ }
+ ],
+ submitButtonText: this.i18n('Execute'),
+ onSubmit: (values) => {
+ this.orchService.identifyDevice(hostname, device, values.duration).subscribe(() => {
+ this.notificationService.show(
+ NotificationType.success,
+ this.i18n(`Identifying '{{device}}' started on host '{{hostname}}'`, {
+ hostname,
+ device
+ })
+ );
+ });
+ }
+ }
+ });
+ }
}
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
+import { ToastrModule } from 'ngx-toastr';
import { of } from 'rxjs';
import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
let orchService: OrchestratorService;
configureTestBed({
- imports: [FormsModule, SharedModule, HttpClientTestingModule, RouterTestingModule],
+ imports: [
+ FormsModule,
+ SharedModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ],
providers: [i18nProviders],
declarations: [InventoryComponent, InventoryDevicesComponent]
});
+import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
+import { ToastrModule } from 'ngx-toastr';
+
import {
configureTestBed,
FixtureHelper,
};
configureTestBed({
- imports: [FormsModule, SharedModule],
+ imports: [FormsModule, HttpClientTestingModule, SharedModule, ToastrModule.forRoot()],
providers: [i18nProviders],
declarations: [OsdDevicesSelectionGroupsComponent, InventoryDevicesComponent]
});
+import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, 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';
};
configureTestBed({
- imports: [FormsModule, SharedModule, ReactiveFormsModule, RouterTestingModule],
+ imports: [
+ FormsModule,
+ HttpClientTestingModule,
+ SharedModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ ToastrModule.forRoot()
+ ],
providers: [BsModalRef, i18nProviders],
declarations: [OsdDevicesSelectionModalComponent, InventoryDevicesComponent]
});
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
+import { ToastrModule } from 'ngx-toastr';
import { BehaviorSubject, of } from 'rxjs';
import {
FormsModule,
SharedModule,
RouterTestingModule,
- ReactiveFormsModule
+ ReactiveFormsModule,
+ ToastrModule.forRoot()
],
providers: [i18nProviders],
declarations: [OsdFormComponent, OsdDevicesSelectionGroupsComponent, InventoryDevicesComponent]
describe('OrchestratorService', () => {
let service: OrchestratorService;
let httpTesting: HttpTestingController;
+ const apiPath = 'api/orchestrator';
configureTestBed({
providers: [OrchestratorService, i18nProviders],
it('should call status', () => {
service.status().subscribe();
- const req = httpTesting.expectOne(service.statusURL);
+ const req = httpTesting.expectOne(`${apiPath}/status`);
expect(req.request.method).toBe('GET');
});
it('should call inventoryList', () => {
service.inventoryList().subscribe();
- const req = httpTesting.expectOne(service.inventoryURL);
+ const req = httpTesting.expectOne(`${apiPath}/inventory`);
expect(req.request.method).toBe('GET');
});
it('should call inventoryList with a host', () => {
const host = 'host0';
service.inventoryList(host).subscribe();
- const req = httpTesting.expectOne(`${service.inventoryURL}?hostname=${host}`);
+ const req = httpTesting.expectOne(`${apiPath}/inventory?hostname=${host}`);
expect(req.request.method).toBe('GET');
});
it('should call serviceList', () => {
service.serviceList().subscribe();
- const req = httpTesting.expectOne(service.serviceURL);
+ const req = httpTesting.expectOne(`${apiPath}/service`);
expect(req.request.method).toBe('GET');
});
it('should call serviceList with a host', () => {
const host = 'host0';
service.serviceList(host).subscribe();
- const req = httpTesting.expectOne(`${service.serviceURL}?hostname=${host}`);
+ const req = httpTesting.expectOne(`${apiPath}/service?hostname=${host}`);
expect(req.request.method).toBe('GET');
});
}
};
service.osdCreate(data['drive_group']).subscribe();
- const req = httpTesting.expectOne(service.osdURL);
+ const req = httpTesting.expectOne(`${apiPath}/osd`);
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(data);
});
providedIn: ApiModule
})
export class OrchestratorService {
- statusURL = 'api/orchestrator/status';
- inventoryURL = 'api/orchestrator/inventory';
- serviceURL = 'api/orchestrator/service';
- osdURL = 'api/orchestrator/osd';
+ private url = 'api/orchestrator';
constructor(private http: HttpClient) {}
status() {
- return this.http.get(this.statusURL);
+ return this.http.get(`${this.url}/status`);
+ }
+
+ identifyDevice(hostname: string, device: string, duration: number) {
+ return this.http.post(`${this.url}/identify_device`, {
+ hostname,
+ device,
+ duration
+ });
}
inventoryList(hostname?: string): Observable<InventoryNode[]> {
const options = hostname ? { params: new HttpParams().set('hostname', hostname) } : {};
- return this.http.get<InventoryNode[]>(this.inventoryURL, options);
+ return this.http.get<InventoryNode[]>(`${this.url}/inventory`, options);
}
inventoryDeviceList(hostname?: string): Observable<InventoryDevice[]> {
serviceList(hostname?: string) {
const options = hostname ? { params: new HttpParams().set('hostname', hostname) } : {};
- return this.http.get(this.serviceURL, options);
+ return this.http.get(`${this.url}/service`, options);
}
osdCreate(driveGroup: {}) {
const request = {
drive_group: driveGroup
};
- return this.http.post(this.osdURL, request, { observe: 'response' });
+ return this.http.post(`${this.url}/osd`, request, { observe: 'response' });
}
}
i18n>This field is required.</span>
</div>
</ng-template>
+ <ng-template [ngSwitchCase]="'select'">
+ <label *ngIf="field.label"
+ class="col-form-label col-sm-3"
+ [for]="field.name">
+ {{ field.label }}
+ </label>
+ <div [ngClass]="{'col-sm-9': field.label, 'col-sm-12': !field.label}">
+ <select class="form-control custom-select"
+ [id]="field.name"
+ [formControlName]="field.name">
+ <option *ngIf="field.placeholder"
+ [ngValue]="null">
+ {{ field.placeholder }}
+ </option>
+ <option *ngFor="let option of field.options"
+ [value]="option.value">
+ {{ option.text }}
+ </option>
+ </select>
+ <span *ngIf="formGroup.hasError('required', field.name)"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ </div>
+ </ng-template>
</ng-container>
</div>
</ng-container>
import { CdFormBuilder } from '../../forms/cd-form-builder';
interface CdFormFieldConfig {
- type: 'textInput';
+ type: 'inputText' | 'select';
name: string;
label?: string;
value?: any;
required?: boolean;
+ // --- select ---
+ placeholder?: string;
+ options?: Array<{
+ text: string;
+ value: any;
+ }>;
}
@Component({
this.commonOperations.update,
this.grafana.update_dashboards,
() => ({})
+ ),
+ // Orchestrator tasks
+ 'orchestrator/identify_device': this.newTaskMessage(
+ new TaskMessageOperation(
+ this.i18n('Identifying'),
+ this.i18n('identify'),
+ this.i18n('Identified')
+ ),
+ (metadata) => this.i18n(`device '{{device}}' on host '{{hostname}}'`, metadata)
)
};
import logging
-from orchestrator import InventoryFilter
+from orchestrator import InventoryFilter, DeviceLightLoc, Completion
from orchestrator import OrchestratorClientMixin, raise_if_exception, OrchestratorError
from .. import mgr
from ..tools import wraps
def status(self):
return self.api.status()
+
+ @wait_api_result
+ def blink_device_light(self, hostname, device, ident_fault, on):
+ # type: (str, str, str, bool) -> Completion
+ return self.api.blink_device_light(ident_fault, on, [DeviceLightLoc(hostname, device)])