- \(MDS_UP_LESS_THAN_MAX\)
- \(OSD_DOWN\)
- \(OSD_HOST_DOWN\)
+ - \(POOL_APP_NOT_ENABLED\)
- pauserd,pausewr flag\(s\) set
- Monitor daemon marked osd\.[[:digit:]]+ down, but it is still running
- rgw: [client.0]
# pylint: disable=too-many-arguments
@classmethod
- def edit_image(cls, pool, image, name=None, size=None, features=None):
- return cls._task_put('/api/block/image/{}/{}'.format(pool, image),
- {'name': name, 'size': size, 'features': features})
+ def edit_image(cls, pool, image, name=None, size=None, features=None, **kwargs):
+ kwargs.update({'name': name, 'size': size, 'features': features})
+ return cls._task_put('/api/block/image/{}/{}'.format(pool, image), kwargs)
@classmethod
def flatten_image(cls, pool, image):
'--yes-i-really-really-mean-it'])
@classmethod
- def create_image_in_trash(cls, pool, name, delay=0, **kwargs):
+ def create_image_in_trash(cls, pool, name, delay=0):
cls.create_image(pool, name, 10240)
img = cls._get('/api/block/image/{}/{}'.format(pool, name))
'timestamp': JLeaf(str, none=True),
'disk_usage': JLeaf(int, none=True),
'total_disk_usage': JLeaf(int, none=True),
+ 'configuration': JList(JObj(sub_elems={
+ 'name': JLeaf(str),
+ 'source': JLeaf(int),
+ 'value': JLeaf(str),
+ })),
})
self.assertSchema(img, schema)
self.remove_image('rbd', rbd_name)
+ def test_create_with_configuration(self):
+ pool = 'rbd'
+ image_name = 'image_with_config'
+ size = 10240
+ configuration = {
+ 'rbd_qos_bps_limit': 10240,
+ 'rbd_qos_bps_burst': 10240 * 2,
+ }
+ expected = [{
+ 'name': 'rbd_qos_bps_limit',
+ 'source': 2,
+ 'value': str(10240),
+ }, {
+ 'name': 'rbd_qos_bps_burst',
+ 'source': 2,
+ 'value': str(10240 * 2),
+ }]
+
+ self.create_image(pool, image_name, size, configuration=configuration)
+ self.assertStatus(201)
+ img = self._get('/api/block/image/rbd/{}'.format(image_name))
+ self.assertStatus(200)
+ for conf in expected:
+ self.assertIn(conf, img['configuration'])
+
+ self.remove_image(pool, image_name)
+
def test_create_rbd_in_data_pool(self):
if not self.bluestore_support:
self.skipTest('requires bluestore cluster')
self.remove_image('rbd', 'edit_img')
self.assertStatus(204)
+ def test_image_change_config(self):
+ pool = 'rbd'
+ image = 'image_with_config'
+ initial_conf = {
+ 'rbd_qos_bps_limit': 10240,
+ 'rbd_qos_write_iops_limit': None
+ }
+ initial_expect = [{
+ 'name': 'rbd_qos_bps_limit',
+ 'source': 2,
+ 'value': '10240',
+ }, {
+ 'name': 'rbd_qos_write_iops_limit',
+ 'source': 0,
+ 'value': '0',
+ }]
+ new_conf = {
+ 'rbd_qos_bps_limit': 0,
+ 'rbd_qos_bps_burst': 20480,
+ 'rbd_qos_write_iops_limit': None
+ }
+ new_expect = [{
+ 'name': 'rbd_qos_bps_limit',
+ 'source': 2,
+ 'value': '0',
+ }, {
+ 'name': 'rbd_qos_bps_burst',
+ 'source': 2,
+ 'value': '20480',
+ }, {
+ 'name': 'rbd_qos_write_iops_limit',
+ 'source': 0,
+ 'value': '0',
+ }]
+
+ self.create_image(pool, image, 2**30, configuration=initial_conf)
+ self.assertStatus(201)
+ img = self._get('/api/block/image/{}/{}'.format(pool, image))
+ self.assertStatus(200)
+ for conf in initial_expect:
+ self.assertIn(conf, img['configuration'])
+
+ self.edit_image(pool, image, configuration=new_conf)
+ img = self._get('/api/block/image/{}/{}'.format(pool, image))
+ self.assertStatus(200)
+ for conf in new_expect:
+ self.assertIn(conf, img['configuration'])
+
+ self.remove_image(pool, image)
+ self.assertStatus(204)
+
def test_update_snapshot(self):
self.create_snapshot('rbd', 'img1', 'snap5')
self.assertStatus(201)
@wraps(func)
def wrapper(*args, **kwargs):
arg_map = self._gen_arg_map(func, args, kwargs)
- md = {}
+ metadata = {}
for k, v in self.metadata.items():
if isinstance(v, str) and v and v[0] == '{' and v[-1] == '}':
param = v[1:-1]
try:
pos = int(param)
- md[k] = arg_map[pos]
+ metadata[k] = arg_map[pos]
except ValueError:
if param.find('.') == -1:
- md[k] = arg_map[param]
+ metadata[k] = arg_map[param]
else:
path = param.split('.')
- md[k] = arg_map[path[0]]
+ metadata[k] = arg_map[path[0]]
for i in range(1, len(path)):
- md[k] = md[k][path[i]]
+ metadata[k] = metadata[k][path[i]]
else:
- md[k] = v
- task = TaskManager.run(self.name, md, func, args, kwargs,
+ metadata[k] = v
+ task = TaskManager.run(self.name, metadata, func, args, kwargs,
exception_handler=self.exception_handler)
try:
status, value = task.wait(self.wait_for)
raise ex
if status == TaskManager.VALUE_EXECUTING:
cherrypy.response.status = 202
- return {'name': self.name, 'metadata': md}
+ return {'name': self.name, 'metadata': metadata}
return value
return wrapper
from .. import mgr
from ..security import Scope
from ..services.ceph_service import CephService
+from ..services.rbd import RbdConfiguration
from ..services.exception import handle_send_command_error
from ..tools import str_to_bool
def get(self, pool_name, attrs=None, stats=False):
# type: (str, str, bool) -> dict
- return self._get(pool_name, attrs, stats)
+ pool = self._get(pool_name, attrs, stats)
+ pool['configuration'] = RbdConfiguration(pool_name).list()
+ return pool
@pool_task('delete', ['{pool_name}'])
@handle_send_command_error('pool')
yes_i_really_really_mean_it=True)
@pool_task('edit', ['{pool_name}'])
- def set(self, pool_name, flags=None, application_metadata=None, **kwargs):
+ def set(self, pool_name, flags=None, application_metadata=None, configuration=None, **kwargs):
self._set_pool_values(pool_name, application_metadata, flags, True, kwargs)
+ RbdConfiguration(pool_name).set_configuration(configuration)
@pool_task('create', {'pool_name': '{pool}'})
@handle_send_command_error('pool')
def create(self, pool, pg_num, pool_type, erasure_code_profile=None, flags=None,
- application_metadata=None, rule_name=None, **kwargs):
+ application_metadata=None, rule_name=None, configuration=None, **kwargs):
ecp = erasure_code_profile if erasure_code_profile else None
CephService.send_command('mon', 'osd pool create', pool=pool, pg_num=int(pg_num),
pgp_num=int(pg_num), pool_type=pool_type, erasure_code_profile=ecp,
rule=rule_name)
-
self._set_pool_values(pool, application_metadata, flags, False, kwargs)
+ RbdConfiguration(pool).set_configuration(configuration)
def _set_pool_values(self, pool, application_metadata, flags, update_existing, kwargs):
update_name = False
reset_arg(arg, '0')
reset_arg('compression_algorithm', 'unset')
+ @RESTController.Resource()
+ @ReadPermission
+ def configuration(self, pool_name):
+ return RbdConfiguration(pool_name).list()
+
@Endpoint()
@ReadPermission
- def _info(self):
+ def _info(self, pool_name=''):
+ # type: (str) -> dict
"""Used by the create-pool dialog"""
+
def rules(pool_type):
return [r
for r in mgr.get('osd_map_crush')['rules']
for o in mgr.get('config_options')['options']
if o['name'] == conf_name][0]
- return {
+ result = {
"pool_names": [p['pool_name'] for p in self._pool_list()],
"crush_rules_replicated": rules(1),
"crush_rules_erasure": rules(3),
"compression_algorithms": compression_enum('bluestore_compression_algorithm'),
"compression_modes": compression_enum('bluestore_compression_mode'),
}
+
+ if pool_name:
+ result['pool_options'] = RbdConfiguration(pool_name).list()
+
+ return result
import rbd
from . import ApiController, RESTController, Task, UpdatePermission, \
- DeletePermission, CreatePermission
+ DeletePermission, CreatePermission, ReadPermission
from .. import mgr
from ..security import Scope
from ..services.ceph_service import CephService
+from ..services.rbd import RbdConfiguration
from ..tools import ViewCache, str_to_bool
from ..services.exception import handle_rados_error, handle_rbd_error, \
serialize_dashboard_exception
RESOURCE_ID = "pool_name/image_name"
# set of image features that can be enable on existing images
- ALLOW_ENABLE_FEATURES = set(["exclusive-lock", "object-map", "fast-diff",
- "journaling"])
+ ALLOW_ENABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "journaling"}
# set of image features that can be disabled on existing images
- ALLOW_DISABLE_FEATURES = set(["exclusive-lock", "object-map", "fast-diff",
- "deep-flatten", "journaling"])
+ ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
+ "journaling"}
@classmethod
def _rbd_disk_usage(cls, image, snaps, whole_object=True):
return total_used_size, snap_map
- def _rbd_image(self, ioctx, pool_name, image_name):
+ @classmethod
+ def _rbd_image(cls, ioctx, pool_name, image_name):
with rbd.Image(ioctx, image_name) as img:
stat = img.stat()
stat['name'] = image_name
for s in stat['snapshots']]
snaps.sort(key=lambda s: s[0])
snaps += [(snaps[-1][0]+1 if snaps else 0, stat['size'], None)]
- total_prov_bytes, snaps_prov_bytes = self._rbd_disk_usage(
+ total_prov_bytes, snaps_prov_bytes = cls._rbd_disk_usage(
img, snaps, True)
stat['total_disk_usage'] = total_prov_bytes
for snap, prov_bytes in snaps_prov_bytes.items():
stat['total_disk_usage'] = None
stat['disk_usage'] = None
+ stat['configuration'] = RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).list()
+
return stat
+ @classmethod
@ViewCache()
- def _rbd_pool_list(self, pool_name):
+ def _rbd_pool_list(cls, pool_name):
rbd_inst = rbd.RBD()
with mgr.rados.open_ioctx(pool_name) as ioctx:
names = rbd_inst.list(ioctx)
result = []
for name in names:
try:
- stat = self._rbd_image(ioctx, pool_name, name)
+ stat = cls._rbd_image(ioctx, pool_name, name)
except rbd.ImageNotFound:
# may have been removed in the meanwhile
continue
for pool in pools:
# pylint: disable=unbalanced-tuple-unpacking
status, value = self._rbd_pool_list(pool)
+ for i, image in enumerate(value):
+ value[i]['configuration'] = RbdConfiguration(pool, image['name']).list()
result.append({'status': status, 'value': value, 'pool_name': pool})
return result
@RbdTask('create',
{'pool_name': '{pool_name}', 'image_name': '{name}'}, 2.0)
def create(self, name, pool_name, size, obj_size=None, features=None,
- stripe_unit=None, stripe_count=None, data_pool=None):
+ stripe_unit=None, stripe_count=None, data_pool=None, configuration=None):
size = int(size)
rbd_inst.create(ioctx, name, size, order=l_order, old_format=False,
features=feature_bitmask, stripe_unit=stripe_unit,
stripe_count=stripe_count, data_pool=data_pool)
+ RbdConfiguration(pool_ioctx=ioctx, image_name=name).set_configuration(configuration)
- return _rbd_call(pool_name, _create)
+ _rbd_call(pool_name, _create)
@RbdTask('delete', ['{pool_name}', '{image_name}'], 2.0)
def delete(self, pool_name, image_name):
return _rbd_call(pool_name, rbd_inst.remove, image_name)
@RbdTask('edit', ['{pool_name}', '{image_name}', '{name}'], 4.0)
- def set(self, pool_name, image_name, name=None, size=None, features=None):
+ def set(self, pool_name, image_name, name=None, size=None, features=None, configuration=None):
def _edit(ioctx, image):
rbd_inst = rbd.RBD()
# check rename image
f_bitmask = _format_features([feature])
image.update_features(f_bitmask, True)
+ RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration(
+ configuration)
+
return _rbd_image_call(pool_name, image_name, _edit)
@RbdTask('copy',
@RESTController.Resource('POST')
def copy(self, pool_name, image_name, dest_pool_name, dest_image_name,
snapshot_name=None, obj_size=None, features=None, stripe_unit=None,
- stripe_count=None, data_pool=None):
+ stripe_count=None, data_pool=None, configuration=None):
def _src_copy(s_ioctx, s_img):
def _copy(d_ioctx):
s_img.copy(d_ioctx, dest_image_name, feature_bitmask, l_order,
stripe_unit, stripe_count, data_pool)
+ RbdConfiguration(pool_ioctx=d_ioctx, image_name=dest_image_name).set_configuration(
+ configuration)
return _rbd_call(dest_pool_name, _copy)
rbd_inst = rbd.RBD()
return _rbd_call(pool_name, rbd_inst.trash_move, image_name, delay)
+ @RESTController.Resource()
+ @ReadPermission
+ def configuration(self, pool_name, image_name):
+ return RbdConfiguration(pool_name, image_name).list()
+
@ApiController('/block/image/{pool_name}/{image_name}/snap', Scope.RBD_IMAGE)
class RbdSnapshot(RESTController):
'child_image_name': '{child_image_name}'}, 2.0)
@RESTController.Resource('POST')
def clone(self, pool_name, image_name, snapshot_name, child_pool_name,
- child_image_name, obj_size=None, features=None,
- stripe_unit=None, stripe_count=None, data_pool=None):
+ child_image_name, obj_size=None, features=None, stripe_unit=None, stripe_count=None,
+ data_pool=None, configuration=None):
+ """
+ Clones a snapshot to an image
+ """
def _parent_clone(p_ioctx):
def _clone(ioctx):
child_image_name, feature_bitmask, l_order,
stripe_unit, stripe_count, data_pool)
+ RbdConfiguration(pool_ioctx=ioctx, image_name=child_image_name).set_configuration(
+ configuration)
+
return _rbd_call(child_pool_name, _clone)
- return _rbd_call(pool_name, _parent_clone)
+ _rbd_call(pool_name, _parent_clone)
@ApiController('/block/image/trash', Scope.RBD_IMAGE)
import { IscsiTargetListComponent } from './iscsi-target-list/iscsi-target-list.component';
import { IscsiComponent } from './iscsi/iscsi.component';
import { MirroringModule } from './mirroring/mirroring.module';
+import { RbdConfigurationFormComponent } from './rbd-configuration-form/rbd-configuration-form.component';
+import { RbdConfigurationListComponent } from './rbd-configuration-list/rbd-configuration-list.component';
import { RbdDetailsComponent } from './rbd-details/rbd-details.component';
import { RbdFormComponent } from './rbd-form/rbd-form.component';
import { RbdImagesComponent } from './rbd-images/rbd-images.component';
IscsiTargetFormComponent,
IscsiTargetImageSettingsModalComponent,
IscsiTargetIqnSettingsModalComponent,
- IscsiTargetDiscoveryModalComponent
- ]
+ IscsiTargetDiscoveryModalComponent,
+ RbdConfigurationListComponent,
+ RbdConfigurationFormComponent
+ ],
+ exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
})
export class BlockModule {}
--- /dev/null
+<fieldset #cfgFormGroup [formGroup]="form.get('configuration')">
+ <legend i18n>RBD Configuration</legend>
+
+ <div *ngFor="let section of rbdConfigurationService.sections">
+ <h3 class="page-header">
+ <span
+ (click)="toggleSectionVisibility(section.class)"
+ class="collapsible">{{ section.heading }} <i [ngClass]="{'fa-plus-circle': !sectionVisibility[section.class], 'fa-minus-circle': sectionVisibility[section.class]}" class="fa" aria-hidden="true"></i></span>
+ </h3>
+ <div class="{{ section.class }}" [hidden]="!sectionVisibility[section.class]">
+ <div
+ class="form-group"
+ *ngFor="let option of section.options"
+ [ngClass]="{'has-error': form.showError('configuration.' + option.name, cfgFormGroup)}">
+ <label
+ class="control-label col-sm-3"
+ [for]="option.name">{{ option.displayName }}<cd-helper>{{ option.description }}</cd-helper></label>
+
+ <div class="col-sm-9 {{ section.heading }}">
+ <div class="input-group">
+ <ng-container [ngSwitch]="option.type">
+ <ng-container *ngSwitchCase="configurationType.milliseconds">
+ <input
+ [id]="option.name"
+ [name]="option.name"
+ [formControlName]="option.name"
+ type="text"
+ class="form-control"
+ [ngDataReady]="ngDataReady"
+ cdMilliseconds>
+ </ng-container>
+ <ng-container *ngSwitchCase="configurationType.bps">
+ <input
+ [id]="option.name"
+ [name]="option.name"
+ [formControlName]="option.name"
+ type="text"
+ class="form-control"
+ defaultUnit="b"
+ [ngDataReady]="ngDataReady"
+ cdDimlessBinaryPerSecond>
+ </ng-container>
+ <ng-container *ngSwitchCase="configurationType.iops">
+ <input
+ [id]="option.name"
+ [name]="option.name"
+ [formControlName]="option.name"
+ type="text"
+ class="form-control"
+ [ngDataReady]="ngDataReady"
+ cdIops>
+ </ng-container>
+ </ng-container>
+ <span class="input-group-btn">
+ <button
+ class="btn btn-default"
+ type="button"
+ data-toggle="button"
+ [ngClass]="{'active': isDisabled(option.name)}"
+ tooltip="Remove the local configuration value. The parent configuration value will be inherited and used instead."
+ containerClass="tooltip-wide"
+ [delay]="1000"
+ i18n-tooltip
+ (click)="reset(option.name)">
+ <i class="fa fa-eraser"
+ aria-hidden="true"></i>
+ </button>
+ </span>
+ </div>
+ <span
+ i18n
+ class="help-block"
+ *ngIf="form.showError('configuration.' + option.name, cfgFormGroup, 'min')">The mininum value is 0</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+</fieldset>
--- /dev/null
+.collapsible {
+ cursor: pointer;
+ user-select: none;
+}
--- /dev/null
+import { EventEmitter } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+
+import { ComponentLoaderFactory } from 'ngx-bootstrap/component-loader';
+import { PositioningService } from 'ngx-bootstrap/positioning';
+import { TooltipConfig, TooltipModule } from 'ngx-bootstrap/tooltip';
+
+import { configureTestBed, FormHelper, i18nProviders } from '../../../../testing/unit-test-helper';
+import { DirectivesModule } from '../../../shared/directives/directives.module';
+import { CdFormGroup } from '../../../shared/forms/cd-form-group';
+import { RbdConfigurationSourceField } from '../../../shared/models/configuration';
+import { DimlessBinaryPerSecondPipe } from '../../../shared/pipes/dimless-binary-per-second.pipe';
+import { FormatterService } from '../../../shared/services/formatter.service';
+import { RbdConfigurationService } from '../../../shared/services/rbd-configuration.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { RbdConfigurationFormComponent } from './rbd-configuration-form.component';
+
+describe('RbdConfigurationFormComponent', () => {
+ let component: RbdConfigurationFormComponent;
+ let fixture: ComponentFixture<RbdConfigurationFormComponent>;
+ let sections: any[];
+ let fh: FormHelper;
+
+ configureTestBed({
+ imports: [ReactiveFormsModule, TooltipModule, DirectivesModule, SharedModule],
+ declarations: [RbdConfigurationFormComponent],
+ providers: [
+ ComponentLoaderFactory,
+ PositioningService,
+ TooltipConfig,
+ RbdConfigurationService,
+ FormatterService,
+ DimlessBinaryPerSecondPipe,
+ i18nProviders
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdConfigurationFormComponent);
+ component = fixture.componentInstance;
+ component.form = new CdFormGroup({}, null);
+ fh = new FormHelper(component.form);
+ fixture.detectChanges();
+ sections = TestBed.get(RbdConfigurationService).sections;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should create all form fields mentioned in RbdConfiguration::OPTIONS', () => {
+ /* Test form creation on a TypeScript level */
+ const actual = Object.keys((component.form.get('configuration') as CdFormGroup).controls);
+ const expected = sections
+ .map((section) => section.options)
+ .reduce((a, b) => a.concat(b))
+ .map((option) => option.name);
+ expect(actual).toEqual(expected);
+
+ /* Test form creation on a template level */
+ const controlDebugElements = fixture.debugElement.queryAll(By.css('input.form-control'));
+ expect(controlDebugElements.length).toBe(expected.length);
+ controlDebugElements.forEach((element) => expect(element.nativeElement).toBeTruthy());
+ });
+
+ it('should only contain values of changed controls if submitted', () => {
+ let values = {};
+ component.changes.subscribe((getDirtyValues: Function) => {
+ values = getDirtyValues();
+ });
+ fh.setValue('configuration.rbd_qos_bps_limit', 0, true);
+ fixture.detectChanges();
+
+ expect(values).toEqual({ rbd_qos_bps_limit: 0 });
+ });
+
+ describe('test loading of initial data for editing', () => {
+ beforeEach(() => {
+ component.initializeData = new EventEmitter<any>();
+ fixture.detectChanges();
+ component.ngOnInit();
+ });
+
+ it('should return dirty values without any units', () => {
+ let dirtyValues = {};
+ component.changes.subscribe((getDirtyValues) => {
+ dirtyValues = getDirtyValues();
+ });
+
+ fh.setValue('configuration.rbd_qos_bps_limit', 55, true);
+ fh.setValue('configuration.rbd_qos_iops_limit', 22, true);
+
+ expect(dirtyValues['rbd_qos_bps_limit']).toBe(55);
+ expect(dirtyValues['rbd_qos_iops_limit']).toBe(22);
+ });
+
+ it('should load initial data into forms', () => {
+ component.initializeData.emit({
+ initialData: [
+ {
+ name: 'rbd_qos_bps_limit',
+ value: 55,
+ source: 1
+ }
+ ],
+ sourceType: RbdConfigurationSourceField.pool
+ });
+
+ expect(component.form.getValue('configuration.rbd_qos_bps_limit')).toEqual('55 B/s');
+ });
+
+ it('should not load initial data if the source is not the pool itself', () => {
+ component.initializeData.emit({
+ initialData: [
+ {
+ name: 'rbd_qos_bps_limit',
+ value: 55,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ value: 22,
+ source: RbdConfigurationSourceField.global
+ }
+ ],
+ sourceType: RbdConfigurationSourceField.pool
+ });
+
+ expect(component.form.getValue('configuration.rbd_qos_iops_limit')).toEqual('0 IOPS');
+ expect(component.form.getValue('configuration.rbd_qos_bps_limit')).toEqual('0 B/s');
+ });
+
+ it('should not load initial data if the source is not the image itself', () => {
+ component.initializeData.emit({
+ initialData: [
+ {
+ name: 'rbd_qos_bps_limit',
+ value: 55,
+ source: RbdConfigurationSourceField.pool
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ value: 22,
+ source: RbdConfigurationSourceField.global
+ }
+ ],
+ sourceType: RbdConfigurationSourceField.image
+ });
+
+ expect(component.form.getValue('configuration.rbd_qos_iops_limit')).toEqual('0 IOPS');
+ expect(component.form.getValue('configuration.rbd_qos_bps_limit')).toEqual('0 B/s');
+ });
+
+ it('should always have formatted results', () => {
+ component.initializeData.emit({
+ initialData: [
+ {
+ name: 'rbd_qos_bps_limit',
+ value: 55,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ value: 22,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_read_bps_limit',
+ value: null, // incorrect type
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_read_bps_limit',
+ value: undefined, // incorrect type
+ source: RbdConfigurationSourceField.image
+ }
+ ],
+ sourceType: RbdConfigurationSourceField.image
+ });
+
+ expect(component.form.getValue('configuration.rbd_qos_iops_limit')).toEqual('22 IOPS');
+ expect(component.form.getValue('configuration.rbd_qos_bps_limit')).toEqual('55 B/s');
+ expect(component.form.getValue('configuration.rbd_qos_read_bps_limit')).toEqual('0 B/s');
+ expect(component.form.getValue('configuration.rbd_qos_read_bps_limit')).toEqual('0 B/s');
+ });
+ });
+
+ it('should reset the corresponding form field correctly', () => {
+ const fieldName = 'rbd_qos_bps_limit';
+ const getValue = () => component.form.get(`configuration.${fieldName}`).value;
+
+ // Initialization
+ fh.setValue(`configuration.${fieldName}`, 418, true);
+ expect(getValue()).toBe(418);
+
+ // Reset
+ component.reset(fieldName);
+ expect(getValue()).toBe(null);
+
+ // Restore
+ component.reset(fieldName);
+ expect(getValue()).toBe(418);
+
+ // Reset
+ component.reset(fieldName);
+ expect(getValue()).toBe(null);
+
+ // Restore
+ component.reset(fieldName);
+ expect(getValue()).toBe(418);
+ });
+
+ describe('should verify that getDirtyValues() returns correctly', () => {
+ let data;
+
+ beforeEach(() => {
+ component.initializeData = new EventEmitter<any>();
+ fixture.detectChanges();
+ component.ngOnInit();
+ data = {
+ initialData: [
+ {
+ name: 'rbd_qos_bps_limit',
+ value: 0,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ value: 0,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_read_bps_limit',
+ value: 0,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_read_iops_limit',
+ value: 0,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_read_iops_burst',
+ value: 0,
+ source: RbdConfigurationSourceField.image
+ },
+ {
+ name: 'rbd_qos_write_bps_burst',
+ value: undefined,
+ source: RbdConfigurationSourceField.global
+ },
+ {
+ name: 'rbd_qos_write_iops_burst',
+ value: null,
+ source: RbdConfigurationSourceField.global
+ }
+ ],
+ sourceType: RbdConfigurationSourceField.image
+ };
+ component.initializeData.emit(data);
+ });
+
+ it('should return an empty object', () => {
+ expect(component.getDirtyValues()).toEqual({});
+ expect(component.getDirtyValues(true, RbdConfigurationSourceField.image)).toEqual({});
+ });
+
+ it('should return dirty values', () => {
+ component.form.get('configuration.rbd_qos_write_bps_burst').markAsDirty();
+ expect(component.getDirtyValues()).toEqual({ rbd_qos_write_bps_burst: 0 });
+
+ component.form.get('configuration.rbd_qos_write_iops_burst').markAsDirty();
+ expect(component.getDirtyValues()).toEqual({
+ rbd_qos_write_iops_burst: 0,
+ rbd_qos_write_bps_burst: 0
+ });
+ });
+
+ it('should also return all local values if they do not contain their initial values', () => {
+ // Change value for all options
+ data.initialData = data.initialData.map((o) => {
+ o.value = 22;
+ return o;
+ });
+
+ // Mark some dirty
+ ['rbd_qos_read_iops_limit', 'rbd_qos_write_bps_burst'].forEach((option) => {
+ component.form.get(`configuration.${option}`).markAsDirty();
+ });
+
+ expect(component.getDirtyValues(true, RbdConfigurationSourceField.image)).toEqual({
+ rbd_qos_read_iops_limit: 0,
+ rbd_qos_write_bps_burst: 0
+ });
+ });
+
+ it('should throw an error if used incorrectly', () => {
+ expect(() => component.getDirtyValues(true)).toThrowError(
+ /^ProgrammingError: If local values shall be included/
+ );
+ });
+ });
+});
--- /dev/null
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+
+import { CdFormGroup } from '../../../shared/forms/cd-form-group';
+import {
+ RbdConfigurationEntry,
+ RbdConfigurationSourceField,
+ RbdConfigurationType
+} from '../../../shared/models/configuration';
+import { FormatterService } from '../../../shared/services/formatter.service';
+import { RbdConfigurationService } from '../../../shared/services/rbd-configuration.service';
+
+@Component({
+ selector: 'cd-rbd-configuration-form',
+ templateUrl: './rbd-configuration-form.component.html',
+ styleUrls: ['./rbd-configuration-form.component.scss']
+})
+export class RbdConfigurationFormComponent implements OnInit {
+ @Input()
+ form: CdFormGroup;
+ @Input()
+ initializeData: EventEmitter<{
+ initialData: RbdConfigurationEntry[];
+ sourceType: RbdConfigurationSourceField;
+ }>;
+ @Output()
+ changes = new EventEmitter<any>();
+ ngDataReady = new EventEmitter<any>();
+ initialData: RbdConfigurationEntry[];
+ configurationType = RbdConfigurationType;
+ sectionVisibility: { [key: string]: boolean } = {};
+
+ constructor(
+ public formatterService: FormatterService,
+ public rbdConfigurationService: RbdConfigurationService
+ ) {}
+
+ ngOnInit() {
+ const configFormGroup = this.createConfigurationFormGroup();
+ this.form.addControl('configuration', configFormGroup);
+
+ // Listen to changes and emit the values to the parent component
+ configFormGroup.valueChanges.subscribe(() => {
+ this.changes.emit(this.getDirtyValues.bind(this));
+ });
+
+ if (this.initializeData) {
+ this.initializeData.subscribe((data) => {
+ this.initialData = data.initialData;
+ const dataType = data.sourceType;
+
+ this.rbdConfigurationService.getWritableOptionFields().forEach((option) => {
+ const optionData = data.initialData.filter((entry) => entry.name === option.name).pop();
+ if (optionData && optionData['source'] === dataType) {
+ this.form.get(`configuration.${option.name}`).setValue(optionData['value']);
+ }
+ });
+ this.ngDataReady.emit();
+ });
+ }
+
+ this.rbdConfigurationService
+ .getWritableSections()
+ .forEach((section) => (this.sectionVisibility[section.class] = false));
+ }
+
+ getDirtyValues(includeLocalValues = false, localFieldType?: RbdConfigurationSourceField) {
+ if (includeLocalValues && !localFieldType) {
+ const msg =
+ 'ProgrammingError: If local values shall be included, a proper localFieldType argument has to be provided, too';
+ throw new Error(msg);
+ }
+ const result = {};
+
+ this.rbdConfigurationService.getWritableOptionFields().forEach((config) => {
+ const control = this.form.get('configuration').get(config.name);
+ const dirty = control.dirty;
+
+ if (this.initialData && this.initialData[config.name] === control.value) {
+ return; // Skip controls with initial data loaded
+ }
+
+ if (dirty || (includeLocalValues && control['source'] === localFieldType)) {
+ if (control.value === null) {
+ result[config.name] = control.value;
+ } else if (config.type === RbdConfigurationType.bps) {
+ result[config.name] = this.formatterService.toBytes(control.value);
+ } else if (config.type === RbdConfigurationType.milliseconds) {
+ result[config.name] = this.formatterService.toMilliseconds(control.value);
+ } else if (config.type === RbdConfigurationType.iops) {
+ result[config.name] = this.formatterService.toIops(control.value);
+ } else {
+ result[config.name] = control.value;
+ }
+ }
+ });
+
+ return result;
+ }
+
+ /**
+ * Dynamically create form controls.
+ */
+ private createConfigurationFormGroup() {
+ const configFormGroup = new CdFormGroup({});
+
+ this.rbdConfigurationService.getWritableOptionFields().forEach((c) => {
+ let control: FormControl;
+ if (
+ c.type === RbdConfigurationType.milliseconds ||
+ c.type === RbdConfigurationType.iops ||
+ c.type === RbdConfigurationType.bps
+ ) {
+ control = new FormControl(0, Validators.min(0));
+ } else {
+ throw new Error(
+ `Type ${c.type} is unknown, you may need to add it to RbdConfiguration class`
+ );
+ }
+ configFormGroup.addControl(c.name, control);
+ });
+
+ return configFormGroup;
+ }
+
+ /**
+ * Reset the value. The inherited value will be used instead.
+ */
+ reset(optionName: string) {
+ const formControl = this.form.get('configuration').get(optionName);
+ if (formControl.disabled) {
+ formControl.setValue(formControl['previousValue'] || 0);
+ formControl.enable();
+ if (!formControl['previousValue']) {
+ formControl.markAsPristine();
+ }
+ } else {
+ formControl['previousValue'] = formControl.value;
+ formControl.setValue(null);
+ formControl.markAsDirty();
+ formControl.disable();
+ }
+ }
+
+ isDisabled(optionName: string) {
+ return this.form.get('configuration').get(optionName).disabled;
+ }
+
+ toggleSectionVisibility(className) {
+ this.sectionVisibility[className] = !this.sectionVisibility[className];
+ }
+}
--- /dev/null
+<cd-table [data]="data"
+ [columns]="poolConfigurationColumns"
+ identifier="name">
+</cd-table>
+
+<ng-template #configurationSourceTpl
+ let-row="row"
+ let-value="value">
+ <div [ngSwitch]="+value">
+ <span *ngSwitchCase="sourceField.global" i18n>Global</span>
+ <strong *ngSwitchCase="sourceField.image" i18n>Image</strong>
+ <strong *ngSwitchCase="sourceField.pool" i18n>Pool</strong>
+ </div>
+</ng-template>
+
+<ng-template #configurationValueTpl
+ let-row="row"
+ let-value="value">
+ <div [ngSwitch]="row.type">
+ <span *ngSwitchCase="typeField.bps">{{ value | dimlessBinaryPerSecond }}</span>
+ <span *ngSwitchCase="typeField.milliseconds">{{ value | milliseconds }}</span>
+ <span *ngSwitchCase="typeField.iops">{{ value | iops }}</span>
+ <span *ngSwitchDefault>{{ value }}</span>
+ </div>
+</ng-template>
--- /dev/null
+import { SimpleChange } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { ChartsModule } from 'ng2-charts';
+import { AlertModule } from 'ngx-bootstrap/alert';
+
+import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { ErrorPanelComponent } from '../../../shared/components/error-panel/error-panel.component';
+import { SparklineComponent } from '../../../shared/components/sparkline/sparkline.component';
+import { TableComponent } from '../../../shared/datatable/table/table.component';
+import { RbdConfigurationEntry } from '../../../shared/models/configuration';
+import { PipesModule } from '../../../shared/pipes/pipes.module';
+import { FormatterService } from '../../../shared/services/formatter.service';
+import { RbdConfigurationService } from '../../../shared/services/rbd-configuration.service';
+import { RbdConfigurationListComponent } from './rbd-configuration-list.component';
+
+describe('RbdConfigurationListComponent', () => {
+ let component: RbdConfigurationListComponent;
+ let fixture: ComponentFixture<RbdConfigurationListComponent>;
+
+ configureTestBed({
+ imports: [
+ FormsModule,
+ NgxDatatableModule,
+ RouterTestingModule,
+ AlertModule,
+ ChartsModule,
+ PipesModule
+ ],
+ declarations: [
+ RbdConfigurationListComponent,
+ TableComponent,
+ ErrorPanelComponent,
+ SparklineComponent
+ ],
+ providers: [FormatterService, RbdConfigurationService, i18nProviders]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdConfigurationListComponent);
+ component = fixture.componentInstance;
+ component.data = [];
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('filters options out which are not defined in RbdConfigurationService', () => {
+ const fakeOption = { name: 'foo', source: 0, value: '50' } as RbdConfigurationEntry;
+ const realOption = {
+ name: 'rbd_qos_read_iops_burst',
+ source: 0,
+ value: '50'
+ } as RbdConfigurationEntry;
+
+ component.data = [fakeOption, realOption];
+ component.ngOnChanges({ name: new SimpleChange(null, null, null) });
+
+ expect(component.data.length).toBe(1);
+ expect(component.data.pop()).toBe(realOption);
+ });
+});
--- /dev/null
+import {
+ Component,
+ Input,
+ OnChanges,
+ OnInit,
+ SimpleChanges,
+ TemplateRef,
+ ViewChild
+} from '@angular/core';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import {
+ RbdConfigurationEntry,
+ RbdConfigurationSourceField,
+ RbdConfigurationType
+} from '../../../shared/models/configuration';
+import { FormatterService } from '../../../shared/services/formatter.service';
+import { RbdConfigurationService } from '../../../shared/services/rbd-configuration.service';
+
+@Component({
+ selector: 'cd-rbd-configuration-table',
+ templateUrl: './rbd-configuration-list.component.html',
+ styleUrls: ['./rbd-configuration-list.component.scss']
+})
+export class RbdConfigurationListComponent implements OnInit, OnChanges {
+ @Input()
+ data: RbdConfigurationEntry[];
+ poolConfigurationColumns: CdTableColumn[];
+ @ViewChild('configurationSourceTpl')
+ configurationSourceTpl: TemplateRef<any>;
+ @ViewChild('configurationValueTpl')
+ configurationValueTpl: TemplateRef<any>;
+
+ readonly sourceField = RbdConfigurationSourceField;
+ readonly typeField = RbdConfigurationType;
+
+ constructor(
+ public formatterService: FormatterService,
+ private rbdConfigurationService: RbdConfigurationService,
+ private i18n: I18n
+ ) {}
+
+ ngOnInit() {
+ this.poolConfigurationColumns = [
+ { prop: 'displayName', name: this.i18n('Name') },
+ { prop: 'description', name: this.i18n('Description') },
+ { prop: 'name', name: this.i18n('Key') },
+ { prop: 'source', name: this.i18n('Source'), cellTemplate: this.configurationSourceTpl },
+ { prop: 'value', name: this.i18n('Value'), cellTemplate: this.configurationValueTpl }
+ ];
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (!this.data) {
+ return;
+ }
+ // Filter settings out which are not listed in RbdConfigurationService
+ this.data = this.data.filter((row) =>
+ this.rbdConfigurationService
+ .getOptionFields()
+ .map((o) => o.name)
+ .includes(row.name)
+ );
+ }
+}
[poolName]="selectedItem.pool_name"
[rbdName]="selectedItem.name"></cd-rbd-snapshot-list>
</tab>
+ <tab i18n-heading
+ heading="Configuration">
+ <cd-rbd-configuration-table [data]="selectedItem['configuration']"></cd-rbd-configuration-table>
+ </tab>
</tabset>
+
+<ng-template
+ #poolConfigurationSourceTpl
+ let-row="row"
+ let-value="value">
+ <ng-container *ngIf="+value; else global">
+ <strong i18n i18n-tooltip tooltip="This setting overrides the global value">Image</strong>
+ </ng-container>
+ <ng-template #global><span i18n i18n-tooltip tooltip="This is the global value. No value for this option has been set for this image.">Global</span></ng-template>
+</ng-template>
+
import { configureTestBed } from '../../../../testing/unit-test-helper';
import { SharedModule } from '../../../shared/shared.module';
+import { RbdConfigurationListComponent } from '../rbd-configuration-list/rbd-configuration-list.component';
import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component';
import { RbdDetailsComponent } from './rbd-details.component';
let fixture: ComponentFixture<RbdDetailsComponent>;
configureTestBed({
- declarations: [RbdDetailsComponent, RbdSnapshotListComponent],
+ declarations: [RbdDetailsComponent, RbdSnapshotListComponent, RbdConfigurationListComponent],
imports: [SharedModule, TabsModule.forRoot(), TooltipModule.forRoot(), RouterTestingModule]
});
-import { Component, Input, OnChanges } from '@angular/core';
+import { Component, Input, OnChanges, TemplateRef, ViewChild } from '@angular/core';
import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { RbdFormModel } from '../rbd-form/rbd-form.model';
@Component({
selector: 'cd-rbd-details',
export class RbdDetailsComponent implements OnChanges {
@Input()
selection: CdTableSelection;
- selectedItem: any;
+ selectedItem: RbdFormModel;
+ @Input()
+ images: any;
+ @ViewChild('poolConfigurationSourceTpl')
+ poolConfigurationSourceTpl: TemplateRef<any>;
constructor() {}
+import { RbdConfigurationEntry } from '../../../shared/models/configuration';
+
export class RbdFormCloneRequestModel {
child_pool_name: string;
child_image_name: string;
stripe_unit: number;
stripe_count: number;
data_pool: string;
+ configuration?: RbdConfigurationEntry[];
}
+import { RbdConfigurationEntry } from '../../../shared/models/configuration';
+
export class RbdFormCopyRequestModel {
dest_pool_name: string;
dest_image_name: string;
stripe_unit: number;
stripe_count: number;
data_pool: string;
+ configuration: RbdConfigurationEntry[];
}
+import { RbdConfigurationEntry } from '../../../shared/models/configuration';
+
export class RbdFormEditRequestModel {
name: string;
size: number;
features: Array<string> = [];
+ configuration: RbdConfigurationEntry[];
}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
- <span i18n>{mode, select, editing {Edit} cloning {Clone} copying {Copy} other {Add}}</span> RBD
+ <span
+ i18n>{mode, select, editing {Edit} cloning {Clone} copying {Copy} other {Add}}</span>
+ RBD
</h3>
</div>
<div class="panel-body">
i18n>Advanced...</a>
</div>
</div>
- <div *ngIf="advancedEnabled">
+ <div [hidden]="!advancedEnabled">
<h2 i18n
class="page-header">Advanced</h2>
- <!-- Object Size -->
- <div class="form-group"
- [ngClass]="{'has-error': rbdForm.showError('obj_size', formDir)}">
- <label i18n
- class="control-label col-sm-3"
- for="size">Object size</label>
- <div class="col-sm-9">
- <select id="obj_size"
- name="obj_size"
- class="form-control"
- formControlName="obj_size">
- <option *ngFor="let objectSize of objectSizes"
- [value]="objectSize">{{ objectSize }}</option>
- </select>
+ <div class="section">
+ <h3 class="page-header" i18n>Striping</h3>
+
+ <!-- Object Size -->
+ <div class="form-group"
+ [ngClass]="{'has-error': rbdForm.showError('obj_size', formDir)}">
+ <label i18n
+ class="control-label col-sm-3"
+ for="size">Object size</label>
+ <div class="col-sm-9">
+ <select id="obj_size"
+ name="obj_size"
+ class="form-control"
+ formControlName="obj_size">
+ <option *ngFor="let objectSize of objectSizes"
+ [value]="objectSize">{{ objectSize }}</option>
+ </select>
+ </div>
</div>
- </div>
- <!-- Strippe Unit -->
- <div class="form-group"
- [ngClass]="{'has-error': rbdForm.showError('stripingUnit', formDir)}">
- <label class="control-label col-sm-3"
- for="stripingUnit">
- <span i18n>Stripe unit</span>
- <span class="required"
- *ngIf="rbdForm.getValue('stripingCount')">
+ <!-- Stripe Unit -->
+ <div class="form-group"
+ [ngClass]="{'has-error': rbdForm.showError('stripingUnit', formDir)}">
+ <label class="control-label col-sm-3"
+ for="stripingUnit">
+ <span i18n>Stripe unit</span>
+ <span class="required"
+ *ngIf="rbdForm.getValue('stripingCount')">
</span>
- </label>
- <div class="col-sm-9">
- <select id="stripingUnit"
- name="stripingUnit"
- class="form-control"
- formControlName="stripingUnit">
- <option i18n
- [ngValue]="null">-- Select stripe unit --</option>
- <option *ngFor="let objectSize of objectSizes"
- [value]="objectSize">{{ objectSize }}</option>
- </select>
- <span class="help-block"
- *ngIf="rbdForm.showError('stripingUnit', formDir, 'required')"
- i18n>This field is required because stripe count is defined!</span>
- <span class="help-block"
- *ngIf="rbdForm.showError('stripingUnit', formDir, 'invalidStripingUnit')"
- i18n>Stripe unit is greater than object size.</span>
+ </label>
+ <div class="col-sm-9">
+ <select id="stripingUnit"
+ name="stripingUnit"
+ class="form-control"
+ formControlName="stripingUnit">
+ <option i18n
+ [ngValue]="null">-- Select stripe unit --</option>
+ <option *ngFor="let objectSize of objectSizes"
+ [value]="objectSize">{{ objectSize }}</option>
+ </select>
+ <span class="help-block"
+ *ngIf="rbdForm.showError('stripingUnit', formDir, 'required')"
+ i18n>This field is required because stripe count is defined!</span>
+ <span class="help-block"
+ *ngIf="rbdForm.showError('stripingUnit', formDir, 'invalidStripingUnit')"
+ i18n>Stripe unit is greater than object size.</span>
+ </div>
</div>
- </div>
- <!-- Strippe Count -->
- <div class="form-group"
- [ngClass]="{'has-error': rbdForm.showError('stripingCount', formDir)}">
- <label class="control-label col-sm-3"
- for="stripingCount">
- <span i18n>Stripe count</span>
- <span class="required"
- *ngIf="rbdForm.getValue('stripingUnit')">
+ <!-- Stripe Count -->
+ <div class="form-group"
+ [ngClass]="{'has-error': rbdForm.showError('stripingCount', formDir)}">
+ <label class="control-label col-sm-3"
+ for="stripingCount">
+ <span i18n>Stripe count</span>
+ <span class="required"
+ *ngIf="rbdForm.getValue('stripingUnit')">
</span>
- </label>
- <div class="col-sm-9">
- <input id="stripingCount"
- name="stripingCount"
- formControlName="stripingCount"
- class="form-control"
- type="number">
- <span class="help-block"
- *ngIf="rbdForm.showError('stripingCount', formDir, 'required')"
- i18n>This field is required because stripe unit is defined!</span>
- <span class="help-block"
- *ngIf="rbdForm.showError('stripingCount', formDir, 'min')"
- i18n>Stripe count must be greater than 0.</span>
+ </label>
+ <div class="col-sm-9">
+ <input id="stripingCount"
+ name="stripingCount"
+ formControlName="stripingCount"
+ class="form-control"
+ type="number">
+ <span class="help-block"
+ *ngIf="rbdForm.showError('stripingCount', formDir, 'required')"
+ i18n>This field is required because stripe unit is defined!</span>
+ <span class="help-block"
+ *ngIf="rbdForm.showError('stripingCount', formDir, 'min')"
+ i18n>Stripe count must be greater than 0.</span>
+ </div>
</div>
</div>
+ <div class="section">
+ <cd-rbd-configuration-form [form]="rbdForm"
+ [initializeData]="initializeConfigData"
+ (changes)="getDirtyConfigurationValues = $event"></cd-rbd-configuration-form>
+ </div>
+
</div>
</div>
<cd-submit-button [form]="formDir"
type="button"
(submitAction)="submit()">
- <span i18n>{mode, select, editing {Update} cloning {Clone} copying {Copy} other {Create}}</span> RBD
+ <span i18n>{mode, select, editing {Update} cloning {Clone} copying {Copy} other {Create}} RBD</span>
</cd-submit-button>
<button type="button"
class="btn btn-sm btn-default"
import { ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
+import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { ToastModule } from 'ng2-toastr';
+import { By } from '@angular/platform-browser';
import { ActivatedRouteStub } from '../../../../testing/activated-route-stub';
import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
import { RbdService } from '../../../shared/api/rbd.service';
import { SharedModule } from '../../../shared/shared.module';
+import { RbdConfigurationFormComponent } from '../rbd-configuration-form/rbd-configuration-form.component';
import { RbdFormMode } from './rbd-form-mode.enum';
import { RbdFormComponent } from './rbd-form.component';
ReactiveFormsModule,
RouterTestingModule,
ToastModule.forRoot(),
- SharedModule
+ SharedModule,
+ TooltipModule
],
- declarations: [RbdFormComponent],
+ declarations: [RbdFormComponent, RbdConfigurationFormComponent],
providers: [
{
provide: ActivatedRoute,
expect(component.snapName).toBe('baz/baz');
});
});
+
+ describe('test image configuration component', () => {
+ it('is visible', () => {
+ fixture.detectChanges();
+ expect(
+ fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
+ .hidden
+ ).toBe(false);
+ });
+ });
});
-import { Component, OnInit } from '@angular/core';
+import { Component, EventEmitter, OnInit } from '@angular/core';
import { FormControl, ValidatorFn, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { PoolService } from '../../../shared/api/pool.service';
import { RbdService } from '../../../shared/api/rbd.service';
import { CdFormGroup } from '../../../shared/forms/cd-form-group';
+import {
+ RbdConfigurationEntry,
+ RbdConfigurationSourceField
+} from '../../../shared/models/configuration';
import { FinishedTask } from '../../../shared/models/finished-task';
import { Permission } from '../../../shared/models/permissions';
import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
objectMapFormControl: FormControl;
journalingFormControl: FormControl;
fastDiffFormControl: FormControl;
+ getDirtyConfigurationValues: (
+ includeLocalField?: boolean,
+ localField?: RbdConfigurationSourceField
+ ) => RbdConfigurationEntry[];
pools: Array<string> = null;
allPools: Array<string> = null;
allDataPools: Array<string> = null;
features: any;
featuresList = [];
+ initializeConfigData = new EventEmitter<{
+ initialData: RbdConfigurationEntry[];
+ sourceType: RbdConfigurationSourceField;
+ }>();
pool: string;
.get('stripingUnit')
.setValue(this.dimlessBinaryPipe.transform(response.stripe_unit));
this.rbdForm.get('stripingCount').setValue(response.stripe_count);
+
+ /* Configuration */
+ this.initializeConfigData.emit({
+ initialData: this.response.configuration,
+ sourceType: RbdConfigurationSourceField.image
+ });
}
createRequest() {
request.features.push(feature.key);
}
});
+
+ /* Striping */
request.stripe_unit = this.formatter.toBytes(this.rbdForm.getValue('stripingUnit'));
request.stripe_count = this.rbdForm.getValue('stripingCount');
request.data_pool = this.rbdForm.getValue('dataPool');
+
+ /* Configuration */
+ request.configuration = this.getDirtyConfigurationValues();
+
return request;
}
request.features.push(feature.key);
}
});
+
+ request.configuration = this.getDirtyConfigurationValues();
+
return request;
}
request.features.push(feature.key);
}
});
+
+ /* Striping */
request.stripe_unit = this.formatter.toBytes(this.rbdForm.getValue('stripingUnit'));
request.stripe_count = this.rbdForm.getValue('stripingCount');
request.data_pool = this.rbdForm.getValue('dataPool');
+
+ /* Configuration */
+ request.configuration = this.getDirtyConfigurationValues(
+ true,
+ RbdConfigurationSourceField.image
+ );
+
return request;
}
request.features.push(feature.key);
}
});
+
+ /* Striping */
request.stripe_unit = this.formatter.toBytes(this.rbdForm.getValue('stripingUnit'));
request.stripe_count = this.rbdForm.getValue('stripingCount');
request.data_pool = this.rbdForm.getValue('dataPool');
+
+ /* Configuration */
+ request.configuration = this.getDirtyConfigurationValues(
+ true,
+ RbdConfigurationSourceField.image
+ );
+
return request;
}
copyAction(): Observable<any> {
const request = this.copyRequest();
+
return this.taskWrapper.wrapTaskAroundCall({
task: new FinishedTask('rbd/copy', {
src_pool_name: this.response.pool_name,
submit() {
let action: Observable<any>;
+
if (this.mode === this.rbdFormMode.editing) {
action = this.editAction();
} else if (this.mode === this.rbdFormMode.cloning) {
} else {
action = this.createAction();
}
+
action.subscribe(
undefined,
() => this.rbdForm.setErrors({ cdSubmitButton: true }),
+import { RbdConfigurationEntry } from '../../../shared/models/configuration';
+
export class RbdFormModel {
name: string;
pool_name: string;
data_pool: string;
size: number;
+
+ /* Striping */
obj_size: number;
stripe_unit: number;
stripe_count: number;
+
+ /* Configuration */
+ configuration: RbdConfigurationEntry[];
}
import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
import { TaskListService } from '../../../shared/services/task-list.service';
import { SharedModule } from '../../../shared/shared.module';
+import { RbdConfigurationListComponent } from '../rbd-configuration-list/rbd-configuration-list.component';
import { RbdDetailsComponent } from '../rbd-details/rbd-details.component';
import { RbdListComponent } from '../rbd-list/rbd-list.component';
import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component';
RbdImagesComponent,
RbdListComponent,
RbdSnapshotListComponent,
- RbdTrashListComponent
+ RbdTrashListComponent,
+ RbdConfigurationListComponent
],
imports: [
HttpClientTestingModule,
import { SummaryService } from '../../../shared/services/summary.service';
import { TaskListService } from '../../../shared/services/task-list.service';
import { SharedModule } from '../../../shared/shared.module';
+import { RbdConfigurationListComponent } from '../rbd-configuration-list/rbd-configuration-list.component';
import { RbdDetailsComponent } from '../rbd-details/rbd-details.component';
import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component';
import { RbdListComponent } from './rbd-list.component';
RouterTestingModule,
HttpClientTestingModule
],
- declarations: [RbdListComponent, RbdDetailsComponent, RbdSnapshotListComponent],
+ declarations: [
+ RbdListComponent,
+ RbdDetailsComponent,
+ RbdSnapshotListComponent,
+ RbdConfigurationListComponent
+ ],
providers: [TaskListService, i18nProviders]
});
<tabset #tabsetChild
- cdTableDetail
+ cdTableDetail
*ngIf="selection.hasSingleSelection">
<tab i18n-heading
heading="Details">
grafanaStyle="one">
</cd-grafana>
</tab>
+ <tab *ngIf="selection.first().type === 'replicated'"
+ i18n-heading
+ heading="Configuration">
+ <cd-rbd-configuration-table [data]="selectedPoolConfiguration"></cd-rbd-configuration-table>
+ </tab>
<tab i18n-heading
*ngIf="selection.first()['tiers'].length > 0"
heading="Cache Tiers Details">
-import { Component, Input, ViewChild } from '@angular/core';
+import { Component, Input, OnChanges, ViewChild } from '@angular/core';
import { I18n } from '@ngx-translate/i18n-polyfill';
import { TabsetComponent } from 'ngx-bootstrap/tabs';
+import { PoolService } from '../../../shared/api/pool.service';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { RbdConfigurationEntry } from '../../../shared/models/configuration';
import { Permissions } from '../../../shared/models/permissions';
@Component({
templateUrl: './pool-details.component.html',
styleUrls: ['./pool-details.component.scss']
})
-export class PoolDetailsComponent {
+export class PoolDetailsComponent implements OnChanges {
cacheTierColumns: Array<CdTableColumn> = [];
@Input()
cacheTiers: any[];
@ViewChild(TabsetComponent)
tabsetChild: TabsetComponent;
+ selectedPoolConfiguration: RbdConfigurationEntry[];
- constructor(private i18n: I18n) {
+ constructor(private i18n: I18n, private poolService: PoolService) {
this.cacheTierColumns = [
{
prop: 'pool_name',
}
];
}
+
+ ngOnChanges() {
+ if (this.selection.hasSingleSelection) {
+ this.poolService.getConfiguration(this.selection.first().pool_name).subscribe((poolConf) => {
+ this.selectedPoolConfiguration = poolConf;
+ });
+ }
+ }
}
</div>
</div>
- <!-- Applications -->
- <div class="form-group">
- <label i18n
- class="col-sm-3 control-label"
- for="applications">Applications</label>
- <div class="col-sm-9">
- <span class="form-control no-border full-height">
- <cd-select-badges id="applications"
- [customBadges]="true"
- [customBadgeValidators]="data.applications.validators"
- [messages]="data.applications.messages"
- [data]="data.applications.selected"
- [options]="data.applications.available"
- [selectionLimit]="4">
- </cd-select-badges>
- </span>
- </div>
+ </div>
+ <!-- Applications -->
+ <div class="form-group">
+ <label i18n
+ class="col-sm-3 control-label"
+ for="applications">Applications</label>
+ <div class="col-sm-9">
+ <span class="form-control no-border full-height">
+ <cd-select-badges id="applications"
+ [customBadges]="true"
+ [customBadgeValidators]="data.applications.validators"
+ [messages]="data.applications.messages"
+ [data]="data.applications.selected"
+ [options]="data.applications.available"
+ [selectionLimit]="4">
+ </cd-select-badges>
+ </span>
</div>
+ </div>
<!-- Compression -->
<div *ngIf="info.is_all_bluestore"
</div>
</div>
+ <!-- Pool configuration -->
+ <div [hidden]="form.get('poolType').value !== 'replicated' || data.applications.selected.indexOf('rbd') === -1">
+ <cd-rbd-configuration-form [form]="form"
+ [initializeData]="initializeConfigData"
+ (changes)="currentConfigurationValues = $event()">
+ </cd-rbd-configuration-form>
</div>
</div>
+ </div>
- <div class="panel-footer">
- <div class="button-group text-right">
- <cd-submit-button [form]="formDir"
- type="button"
- (submitAction)="submit()">
- <span i18n>{editing, select, 1 {Edit} other {Create}} pool</span>
- </cd-submit-button>
- <button i18n
- type="button"
- class="btn btn-sm btn-default"
- routerLink="/pool">Back</button>
- </div>
+ <div class="panel-footer">
+ <div class="button-group text-right">
+ <cd-submit-button [form]="formDir"
+ type="button"
+ (submitAction)="submit()">
+ <span i18n>{editing, select, 1 {Edit} other {Create}} pool</span>
+ </cd-submit-button>
+ <button i18n
+ type="button"
+ class="btn btn-sm btn-default"
+ routerLink="/pool">Back</button>
</div>
</div>
</form>
return rule;
};
- const testSubmit = (pool: any, taskName: string, poolServiceMethod: 'create' | 'update') => {
+ const expectValidSubmit = (
+ pool: any,
+ taskName: string,
+ poolServiceMethod: 'create' | 'update'
+ ) => {
spyOn(poolService, poolServiceMethod).and.stub();
const taskWrapper = TestBed.get(TaskWrapperService);
spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
});
};
const testCreate = (pool) => {
- testSubmit(pool, 'pool/create', 'create');
+ expectValidSubmit(pool, 'pool/create', 'create');
};
beforeEach(() => {
flags: ['ec_overwrites']
});
});
+
+ it('with rbd qos settings', () => {
+ setMultipleValues({
+ name: 'replicatedRbdQos',
+ poolType: 'replicated',
+ size: 2,
+ pgNum: 32
+ });
+ component.currentConfigurationValues = {
+ rbd_qos_bps_limit: 55
+ };
+ testCreate({
+ pool: 'replicatedRbdQos',
+ pool_type: 'replicated',
+ size: 2,
+ pg_num: 32,
+ configuration: {
+ rbd_qos_bps_limit: 55
+ }
+ });
+ });
});
describe('replicated coded pool', () => {
pool.options.compression_required_ratio = 0.8;
pool.flags_names = 'someFlag1,someFlag2';
pool.application_metadata = ['rbd', 'rgw'];
+
createCrushRule({ name: 'someRule' });
spyOn(poolService, 'get').and.callFake(() => of(pool));
});
it(`always provides the application metadata array with submit even if it's empty`, () => {
expect(form.get('mode').dirty).toBe(false);
component.data.applications.selected = [];
- testSubmit(
+ expectValidSubmit(
{
application_metadata: [],
pool: 'somePoolName'
formHelper.setValue('minBlobSize', '').markAsDirty();
formHelper.setValue('maxBlobSize', '').markAsDirty();
formHelper.setValue('ratio', '').markAsDirty();
- testSubmit(
+ expectValidSubmit(
{
application_metadata: ['rbd', 'rgw'],
compression_max_blob_size: 0,
it(`will unset mode not used anymore`, () => {
formHelper.setValue('mode', 'none').markAsDirty();
- testSubmit(
+ expectValidSubmit(
{
application_metadata: ['rbd', 'rgw'],
compression_mode: 'unset',
});
});
});
+
+ describe('test pool configuration component', () => {
+ it('is visible for replicated pools with rbd application', () => {
+ const poolType = component.form.get('poolType');
+ poolType.markAsDirty();
+ poolType.setValue('replicated');
+ component.data.applications.selected = ['rbd'];
+ fixture.detectChanges();
+ expect(
+ fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
+ .hidden
+ ).toBe(false);
+ });
+
+ it('is invisible for erasure coded pools', () => {
+ const poolType = component.form.get('poolType');
+ poolType.markAsDirty();
+ poolType.setValue('erasure');
+ fixture.detectChanges();
+ expect(
+ fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
+ .hidden
+ ).toBe(true);
+ });
+ });
});
-import { Component, OnInit } from '@angular/core';
+import { Component, EventEmitter, OnInit } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { CdFormGroup } from '../../../shared/forms/cd-form-group';
import { CdValidators } from '../../../shared/forms/cd-validators';
+import {
+ RbdConfigurationEntry,
+ RbdConfigurationSourceField
+} from '../../../shared/models/configuration';
import { CrushRule } from '../../../shared/models/crush-rule';
import { CrushStep } from '../../../shared/models/crush-step';
import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
current = {
rules: []
};
+ initializeConfigData = new EventEmitter<{
+ initialData: RbdConfigurationEntry[];
+ sourceType: RbdConfigurationSourceField;
+ }>();
+ currentConfigurationValues: { [configKey: string]: any } = {};
constructor(
private dimlessBinaryPipe: DimlessBinaryPipe,
}
private initEditFormData(pool: Pool) {
+ this.initializeConfigData.emit({
+ initialData: pool.configuration,
+ sourceType: RbdConfigurationSourceField.pool
+ });
+
const dataMap = {
name: pool.pool_name,
poolType: pool.type,
]);
}
}
+
const apps = this.data.applications.selected;
if (apps.length > 0 || this.editing) {
pool['application_metadata'] = apps;
}
+
+ // Only collect configuration data for replicated pools, as QoS cannot be configured on EC
+ // pools. EC data pools inherit their settings from the corresponding replicated metadata pool.
+ if (
+ this.form.get('poolType').value === 'replicated' &&
+ !_.isEmpty(this.currentConfigurationValues)
+ ) {
+ pool['configuration'] = this.currentConfigurationValues;
+ }
+
this.triggerApiTask(pool);
}
import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
import { PoolService } from '../../../shared/api/pool.service';
-import { DeletionModalComponent } from '../../../shared/components/deletion-modal/deletion-modal.component';
+import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { ExecutingTask } from '../../../shared/models/executing-task';
import { SummaryService } from '../../../shared/services/summary.service';
import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
import { SharedModule } from '../../../shared/shared.module';
+import { RbdConfigurationListComponent } from '../../block/rbd-configuration-list/rbd-configuration-list.component';
import { PgCategoryService } from '../../shared/pg-category.service';
import { Pool } from '../pool';
import { PoolDetailsComponent } from '../pool-details/pool-details.component';
};
configureTestBed({
- declarations: [PoolListComponent, PoolDetailsComponent],
+ declarations: [PoolListComponent, PoolDetailsComponent, RbdConfigurationListComponent],
imports: [
SharedModule,
ToastModule.forRoot(),
const callDeletion = () => {
component.deletePoolModal();
- const deletion: DeletionModalComponent = component.modalRef.content;
+ const deletion: CriticalConfirmationModalComponent = component.modalRef.content;
deletion.submitActionObservable();
};
@ViewChild('poolUsageTpl')
poolUsageTpl: TemplateRef<any>;
+ @ViewChild('poolConfigurationSourceTpl')
+ poolConfigurationSourceTpl: TemplateRef<any>;
+
pools: Pool[] = [];
columns: CdTableColumn[];
selection = new CdTableSelection();
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { PopoverModule } from 'ngx-bootstrap/popover';
import { TabsModule } from 'ngx-bootstrap/tabs';
+import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { ServicesModule } from '../../shared/services/services.module';
import { SharedModule } from '../../shared/shared.module';
+import { BlockModule } from '../block/block.module';
import { CephSharedModule } from '../shared/ceph-shared.module';
import { ErasureCodeProfileFormComponent } from './erasure-code-profile-form/erasure-code-profile-form.component';
import { PoolDetailsComponent } from './pool-details/pool-details.component';
RouterModule,
ReactiveFormsModule,
BsDropdownModule,
- ServicesModule
+ ServicesModule,
+ TooltipModule.forRoot(),
+ BlockModule
],
exports: [PoolListComponent, PoolFormComponent],
declarations: [
wr?: PoolStat;
};
cdIsBinary?: boolean;
+ configuration: { source: number; name: string; value: string }[];
constructor(name) {
this.pool_name = name;
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
-import { configureTestBed } from '../../../testing/unit-test-helper';
+import { configureTestBed, i18nProviders } from '../../../testing/unit-test-helper';
+import { RbdConfigurationSourceField } from '../models/configuration';
+import { RbdConfigurationService } from '../services/rbd-configuration.service';
import { PoolService } from './pool.service';
describe('PoolService', () => {
const apiPath = 'api/pool';
configureTestBed({
- providers: [PoolService],
+ providers: [PoolService, RbdConfigurationService, i18nProviders],
imports: [HttpClientTestingModule]
});
tick();
expect(result).toEqual(['foo', 'bar']);
}));
+
+ it('should test injection of data from getConfiguration()', fakeAsync(() => {
+ const pool = 'foo';
+ let value;
+ service.getConfiguration(pool).subscribe((next) => (value = next));
+ const req = httpTesting.expectOne(`${apiPath}/${pool}/configuration`);
+ expect(req.request.method).toBe('GET');
+ req.flush([
+ {
+ name: 'rbd_qos_bps_limit',
+ value: '60',
+ source: RbdConfigurationSourceField.global
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ value: '0',
+ source: RbdConfigurationSourceField.global
+ }
+ ]);
+ tick();
+ expect(value).toEqual([
+ {
+ description: 'The desired limit of IO bytes per second.',
+ displayName: 'BPS Limit',
+ name: 'rbd_qos_bps_limit',
+ source: RbdConfigurationSourceField.global,
+ type: 0,
+ value: '60'
+ },
+ {
+ description: 'The desired limit of IO operations per second.',
+ displayName: 'IOPS Limit',
+ name: 'rbd_qos_iops_limit',
+ source: RbdConfigurationSourceField.global,
+ type: 1,
+ value: '0'
+ }
+ ]);
+ }));
});
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
import { cdEncode } from '../decorators/cd-encode';
-import { PoolFormInfo } from '../models/pool-form-info';
+import { RbdConfigurationEntry } from '../models/configuration';
+import { RbdConfigurationService } from '../services/rbd-configuration.service';
import { ApiModule } from './api.module';
@cdEncode
export class PoolService {
apiPath = 'api/pool';
- constructor(private http: HttpClient) {}
+ constructor(private http: HttpClient, private rbdConfigurationService: RbdConfigurationService) {}
create(pool) {
return this.http.post(this.apiPath, pool, { observe: 'response' });
return this.http.get(`${this.apiPath}?stats=true`);
}
- getInfo(): Observable<PoolFormInfo> {
- return this.http.get<PoolFormInfo>(`${this.apiPath}/_info`);
+ getConfiguration(poolName: string): Observable<RbdConfigurationEntry[]> {
+ return this.http.get<RbdConfigurationEntry[]>(`${this.apiPath}/${poolName}/configuration`).pipe(
+ // Add static data maintained in RbdConfigurationService
+ map((values) =>
+ values.map((entry) =>
+ Object.assign(entry, this.rbdConfigurationService.getOptionByName(entry.name))
+ )
+ )
+ );
+ }
+
+ getInfo(pool_name?: string) {
+ return this.http.get(`${this.apiPath}/_info` + (pool_name ? `?pool_name=${pool_name}` : ''));
}
list(attrs = []) {
--- /dev/null
+import { RbdConfigurationEntry } from '../models/configuration';
+
+export interface RbdPool {
+ pool_name: string;
+ status: number;
+ value: RbdImage[];
+}
+
+export interface RbdImage {
+ disk_usage: number;
+ stripe_unit: number;
+ name: string;
+ parent: any;
+ pool_name: string;
+ num_objs: number;
+ block_name_prefix: string;
+ snapshots: any[];
+ obj_size: number;
+ data_pool: string;
+ total_disk_usage: number;
+ features: number;
+ configuration: RbdConfigurationEntry[];
+ timestamp: string;
+ id: string;
+ features_name: string[];
+ stripe_count: number;
+ order: number;
+ size: number;
+}
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
-import { configureTestBed } from '../../../testing/unit-test-helper';
+import { configureTestBed, i18nProviders } from '../../../testing/unit-test-helper';
+import { RbdConfigurationService } from '../services/rbd-configuration.service';
import { RbdService } from './rbd.service';
describe('RbdService', () => {
let httpTesting: HttpTestingController;
configureTestBed({
- providers: [RbdService],
+ providers: [RbdService, RbdConfigurationService, i18nProviders],
imports: [HttpClientTestingModule]
});
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import { map } from 'rxjs/operators';
+
import { cdEncode, cdEncodeNot } from '../decorators/cd-encode';
+import { RbdConfigurationService } from '../services/rbd-configuration.service';
import { ApiModule } from './api.module';
+import { RbdPool } from './rbd.model';
@cdEncode
@Injectable({
providedIn: ApiModule
})
export class RbdService {
- constructor(private http: HttpClient) {}
+ constructor(private http: HttpClient, private rbdConfigurationService: RbdConfigurationService) {}
create(rbd) {
return this.http.post('api/block/image', rbd, { observe: 'response' });
}
list() {
- return this.http.get('api/block/image');
+ return this.http.get<RbdPool[]>('api/block/image').pipe(
+ map((pools) =>
+ pools.map((pool) => {
+ pool.value.map((image) => {
+ if (!image.configuration) {
+ return image;
+ }
+ image.configuration.map((option) =>
+ Object.assign(option, this.rbdConfigurationService.getOptionByName(option.name))
+ );
+ return image;
+ });
+ return pool;
+ })
+ )
+ );
}
copy(poolName, rbdName, rbd) {
import { Component, ElementRef, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
import { Input } from '@angular/core';
-import { ChartTooltip } from '../../../shared/models/chart-tooltip';
+import { ChartTooltip } from '../../models/chart-tooltip';
import { DimlessBinaryPipe } from '../../pipes/dimless-binary.pipe';
@Component({
--- /dev/null
+import { DimlessBinaryPerSecondDirective } from './dimless-binary-per-second.directive';
+
+export class MockElementRef {
+ nativeElement: {};
+}
+
+describe('DimlessBinaryPerSecondDirective', () => {
+ it('should create an instance', () => {
+ const directive = new DimlessBinaryPerSecondDirective(new MockElementRef(), null, null, null);
+ expect(directive).toBeTruthy();
+ });
+});
--- /dev/null
+import {
+ Directive,
+ ElementRef,
+ EventEmitter,
+ HostListener,
+ Input,
+ OnInit,
+ Output
+} from '@angular/core';
+import { NgControl } from '@angular/forms';
+
+import * as _ from 'lodash';
+
+import { DimlessBinaryPerSecondPipe } from '../pipes/dimless-binary-per-second.pipe';
+import { FormatterService } from '../services/formatter.service';
+
+@Directive({
+ selector: '[cdDimlessBinaryPerSecond]'
+})
+export class DimlessBinaryPerSecondDirective implements OnInit {
+ @Output()
+ ngModelChange: EventEmitter<any> = new EventEmitter();
+
+ /**
+ * Event emitter for letting this directive know that the data has (asynchronously) been loaded
+ * and the value needs to be adapted by this directive.
+ */
+ @Input()
+ ngDataReady: EventEmitter<any>;
+
+ /**
+ * Minimum size in bytes.
+ * If user enter a value lower than <minBytes>,
+ * the model will automatically be update to <minBytes>.
+ *
+ * If <roundPower> is used, this value should be a power of <roundPower>.
+ *
+ * Example:
+ * Given minBytes=4096 (4KiB), if user type 1KiB, then model will be updated to 4KiB
+ */
+ @Input()
+ minBytes: number;
+
+ /**
+ * Maximum size in bytes.
+ * If user enter a value greater than <maxBytes>,
+ * the model will automatically be update to <maxBytes>.
+ *
+ * If <roundPower> is used, this value should be a power of <roundPower>.
+ *
+ * Example:
+ * Given maxBytes=3145728 (3MiB), if user type 4MiB, then model will be updated to 3MiB
+ */
+ @Input()
+ maxBytes: number;
+
+ /**
+ * Value will be rounded up the nearest power of <roundPower>
+ *
+ * Example:
+ * Given roundPower=2, if user type 7KiB, then model will be updated to 8KiB
+ * Given roundPower=2, if user type 5KiB, then model will be updated to 4KiB
+ */
+ @Input()
+ roundPower: number;
+
+ /**
+ * Default unit that should be used when user do not type a unit.
+ * By default, "MiB" will be used.
+ *
+ * Example:
+ * Given defaultUnit=null, if user type 7, then model will be updated to 7MiB
+ * Given defaultUnit=k, if user type 7, then model will be updated to 7KiB
+ */
+ @Input()
+ defaultUnit: string;
+
+ private el: HTMLInputElement;
+
+ constructor(
+ private elementRef: ElementRef,
+ private control: NgControl,
+ private dimlessBinaryPerSecondPipe: DimlessBinaryPerSecondPipe,
+ private formatter: FormatterService
+ ) {
+ this.el = this.elementRef.nativeElement;
+ }
+
+ ngOnInit() {
+ this.setValue(this.el.value);
+ if (this.ngDataReady) {
+ this.ngDataReady.subscribe(() => this.setValue(this.el.value));
+ }
+ }
+
+ setValue(value) {
+ if (/^[\d.]+$/.test(value)) {
+ value += this.defaultUnit || 'm';
+ }
+ const size = this.formatter.toBytes(value, 0);
+ const roundedSize = this.round(size);
+ this.el.value = this.dimlessBinaryPerSecondPipe.transform(roundedSize);
+ if (size !== null) {
+ this.ngModelChange.emit(this.el.value);
+ this.control.control.setValue(this.el.value);
+ } else {
+ this.ngModelChange.emit(null);
+ this.control.control.setValue(null);
+ }
+ }
+
+ round(size) {
+ if (size !== null && size !== 0) {
+ if (!_.isUndefined(this.minBytes) && size < this.minBytes) {
+ return this.minBytes;
+ }
+ if (!_.isUndefined(this.maxBytes) && size > this.maxBytes) {
+ return this.maxBytes;
+ }
+ if (!_.isUndefined(this.roundPower)) {
+ const power = Math.round(Math.log(size) / Math.log(this.roundPower));
+ return Math.pow(this.roundPower, power);
+ }
+ }
+ return size;
+ }
+
+ @HostListener('blur', ['$event.target.value'])
+ onBlur(value) {
+ this.setValue(value);
+ }
+}
import { AutofocusDirective } from './autofocus.directive';
import { Copy2ClipboardButtonDirective } from './copy2clipboard-button.directive';
+import { DimlessBinaryPerSecondDirective } from './dimless-binary-per-second.directive';
import { DimlessBinaryDirective } from './dimless-binary.directive';
+import { IopsDirective } from './iops.directive';
+import { MillisecondsDirective } from './milliseconds.directive';
import { PasswordButtonDirective } from './password-button.directive';
@NgModule({
AutofocusDirective,
Copy2ClipboardButtonDirective,
DimlessBinaryDirective,
- PasswordButtonDirective
+ DimlessBinaryPerSecondDirective,
+ PasswordButtonDirective,
+ MillisecondsDirective,
+ IopsDirective
],
exports: [
AutofocusDirective,
Copy2ClipboardButtonDirective,
DimlessBinaryDirective,
- PasswordButtonDirective
+ DimlessBinaryPerSecondDirective,
+ PasswordButtonDirective,
+ MillisecondsDirective,
+ IopsDirective
],
providers: []
})
--- /dev/null
+import { IopsDirective } from './iops.directive';
+
+describe('IopsDirective', () => {
+ it('should create an instance', () => {
+ const directive = new IopsDirective(null, null);
+ expect(directive).toBeTruthy();
+ });
+});
--- /dev/null
+import { Directive, EventEmitter, HostListener, Input, OnInit } from '@angular/core';
+import { NgControl } from '@angular/forms';
+
+import { FormatterService } from '../services/formatter.service';
+
+@Directive({
+ selector: '[cdIops]'
+})
+export class IopsDirective implements OnInit {
+ @Input()
+ ngDataReady: EventEmitter<any>;
+
+ constructor(private formatter: FormatterService, private ngControl: NgControl) {}
+
+ setValue(value: string): void {
+ const iops = this.formatter.toIops(value);
+ this.ngControl.control.setValue(`${iops} IOPS`);
+ }
+
+ ngOnInit(): void {
+ this.setValue(this.ngControl.value);
+ if (this.ngDataReady) {
+ this.ngDataReady.subscribe(() => this.setValue(this.ngControl.value));
+ }
+ }
+
+ @HostListener('blur', ['$event.target.value'])
+ onUpdate(value) {
+ this.setValue(value);
+ }
+}
--- /dev/null
+import { MillisecondsDirective } from './milliseconds.directive';
+
+describe('MillisecondsDirective', () => {
+ it('should create an instance', () => {
+ const directive = new MillisecondsDirective(null, null);
+ expect(directive).toBeTruthy();
+ });
+});
--- /dev/null
+import { Directive, EventEmitter, HostListener, Input, OnInit } from '@angular/core';
+import { NgControl } from '@angular/forms';
+
+import { FormatterService } from '../services/formatter.service';
+
+@Directive({
+ selector: '[cdMilliseconds]'
+})
+export class MillisecondsDirective implements OnInit {
+ @Input()
+ ngDataReady: EventEmitter<any>;
+
+ constructor(private control: NgControl, private formatter: FormatterService) {}
+
+ setValue(value: string): void {
+ const ms = this.formatter.toMilliseconds(value);
+ this.control.control.setValue(`${ms} ms`);
+ }
+
+ ngOnInit(): void {
+ this.setValue(this.control.value);
+ if (this.ngDataReady) {
+ this.ngDataReady.subscribe(() => this.setValue(this.control.value));
+ }
+ }
+
+ @HostListener('blur', ['$event.target.value'])
+ onUpdate(value) {
+ this.setValue(value);
+ }
+}
--- /dev/null
+export enum RbdConfigurationSourceField {
+ global = 0,
+ pool = 1,
+ image = 2
+}
+
+export enum RbdConfigurationType {
+ bps,
+ iops,
+ milliseconds
+}
+
+/**
+ * This configuration can also be set on a pool level.
+ */
+export interface RbdConfigurationEntry {
+ name: string;
+ source: RbdConfigurationSourceField;
+ value: any;
+ type?: RbdConfigurationType; // Non-external field.
+ description?: string; // Non-external field.
+ displayName?: string; // Non-external field. Nice name for the UI which is added in the UI.
+}
+
+/**
+ * This object contains additional information injected into the elements retrieved by the service.
+ */
+export interface RbdConfigurationExtraField {
+ name: string;
+ displayName: string;
+ description: string;
+ type: RbdConfigurationType;
+ readOnly?: boolean;
+}
+
+/**
+ * Represents a set of data to be used for editing or creating configuration options
+ */
+export interface RbdConfigurationSection {
+ heading: string;
+ class: string;
+ options: RbdConfigurationExtraField[];
+}
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+import { FormatterService } from '../services/formatter.service';
+
+@Pipe({
+ name: 'dimlessBinaryPerSecond'
+})
+export class DimlessBinaryPerSecondPipe implements PipeTransform {
+ constructor(private formatter: FormatterService) {}
+
+ transform(value: any, args?: any): any {
+ return this.formatter.format_number(value, 1024, [
+ 'B/s',
+ 'kB/s',
+ 'MB/s',
+ 'GB/s',
+ 'TB/s',
+ 'PB/s',
+ 'EB/s',
+ 'ZB/s',
+ 'YB/s'
+ ]);
+ }
+}
--- /dev/null
+import { IopsPipe } from './iops.pipe';
+
+describe('IopsPipe', () => {
+ it('create an instance', () => {
+ const pipe = new IopsPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'iops'
+})
+export class IopsPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ return `${value} IOPS`;
+ }
+}
--- /dev/null
+import { MillisecondsPipe } from './milliseconds.pipe';
+
+describe('MillisecondsPipe', () => {
+ it('create an instance', () => {
+ const pipe = new MillisecondsPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'milliseconds'
+})
+export class MillisecondsPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ return `${value} ms`;
+ }
+}
import { CdDatePipe } from './cd-date.pipe';
import { CephReleaseNamePipe } from './ceph-release-name.pipe';
import { CephShortVersionPipe } from './ceph-short-version.pipe';
+import { DimlessBinaryPerSecondPipe } from './dimless-binary-per-second.pipe';
import { DimlessBinaryPipe } from './dimless-binary.pipe';
import { DimlessPipe } from './dimless.pipe';
import { EncodeUriPipe } from './encode-uri.pipe';
import { FilterPipe } from './filter.pipe';
import { HealthColorPipe } from './health-color.pipe';
+import { IopsPipe } from './iops.pipe';
import { ListPipe } from './list.pipe';
import { LogPriorityPipe } from './log-priority.pipe';
+import { MillisecondsPipe } from './milliseconds.pipe';
import { OrdinalPipe } from './ordinal.pipe';
import { RelativeDatePipe } from './relative-date.pipe';
import { RoundPipe } from './round.pipe';
imports: [CommonModule],
declarations: [
DimlessBinaryPipe,
+ DimlessBinaryPerSecondPipe,
HealthColorPipe,
DimlessPipe,
CephShortVersionPipe,
EmptyPipe,
EncodeUriPipe,
RoundPipe,
- OrdinalPipe
+ OrdinalPipe,
+ MillisecondsPipe,
+ IopsPipe
],
exports: [
DimlessBinaryPipe,
+ DimlessBinaryPerSecondPipe,
HealthColorPipe,
DimlessPipe,
CephShortVersionPipe,
EmptyPipe,
EncodeUriPipe,
RoundPipe,
- OrdinalPipe
+ OrdinalPipe,
+ MillisecondsPipe,
+ IopsPipe
],
providers: [
DatePipe,
CephShortVersionPipe,
CephReleaseNamePipe,
DimlessBinaryPipe,
+ DimlessBinaryPerSecondPipe,
DimlessPipe,
RelativeDatePipe,
ListPipe,
CdDatePipe,
EmptyPipe,
EncodeUriPipe,
- OrdinalPipe
+ OrdinalPipe,
+ IopsPipe,
+ MillisecondsPipe
]
})
export class PipesModule {}
/**
* Convert the given value into bytes.
* @param {string} value The value to be converted, e.g. 1024B, 10M, 300KiB or 1ZB.
- * @returns Returns the given value in bytes without any appended unit or null in case
- * of an error.
+ * @param error_value The value returned in case the regular expression did not match. Defaults to
+ * null.
+ * @returns Returns the given value in bytes without any unit appended or the defined error value
+ * in case xof an error.
*/
- toBytes(value: string): number | null {
+ toBytes(value: string, error_value = null): number | null {
const base = 1024;
const units = ['b', 'k', 'm', 'g', 't', 'p', 'e', 'z', 'y'];
- const m = RegExp('^(\\d+(.\\d+)?) ?([' + units.join('') + '](b|ib)?)?$', 'i').exec(value);
+ const m = RegExp('^(\\d+(.\\d+)?) ?([' + units.join('') + ']?(b|ib|B/s)?)?$', 'i').exec(value);
if (m === null) {
- return null;
+ return error_value;
}
let bytes = parseFloat(m[1]);
if (_.isString(m[3])) {
}
return Math.round(bytes);
}
+
+ /**
+ * Converts `x ms` to `x` (currently) or `0` if the conversion fails
+ */
+ toMilliseconds(value: string): number {
+ const pattern = /^\s*(\d+)\s*(ms)?\s*$/i;
+ const testResult = pattern.exec(value);
+
+ if (testResult !== null) {
+ return +testResult[1];
+ }
+
+ return 0;
+ }
+
+ /**
+ * Converts `x IOPS` to `x` (currently) or `0` if the conversion fails
+ */
+ toIops(value: string): number {
+ const pattern = /^\s*(\d+)\s*(IOPS)?\s*$/i;
+ const testResult = pattern.exec(value);
+
+ if (testResult !== null) {
+ return +testResult[1];
+ }
+
+ return 0;
+ }
}
--- /dev/null
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed, i18nProviders } from '../../../testing/unit-test-helper';
+import { RbdConfigurationType } from '../models/configuration';
+import { RbdConfigurationService } from './rbd-configuration.service';
+
+describe('RbdConfigurationService', () => {
+ let service: RbdConfigurationService;
+
+ configureTestBed({
+ providers: [RbdConfigurationService, i18nProviders]
+ });
+
+ beforeEach(() => {
+ service = TestBed.get(RbdConfigurationService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should filter config options', () => {
+ const result = service.getOptionByName('rbd_qos_write_iops_burst');
+ expect(result).toEqual({
+ name: 'rbd_qos_write_iops_burst',
+ displayName: 'Write IOPS Burst',
+ description: 'The desired burst limit of write operations.',
+ type: RbdConfigurationType.iops
+ });
+ });
+
+ it('should return the display name', () => {
+ const displayName = service.getDisplayName('rbd_qos_write_iops_burst');
+ expect(displayName).toBe('Write IOPS Burst');
+ });
+
+ it('should return the description', () => {
+ const description = service.getDescription('rbd_qos_write_iops_burst');
+ expect(description).toBe('The desired burst limit of write operations.');
+ });
+
+ it('should have a class for each section', () => {
+ service.sections.forEach((section) => expect(section.class).toBeTruthy());
+ });
+});
--- /dev/null
+import { Injectable } from '@angular/core';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+
+import {
+ RbdConfigurationExtraField,
+ RbdConfigurationSection,
+ RbdConfigurationType
+} from '../models/configuration';
+import { ServicesModule } from './services.module';
+
+/**
+ * Define here which options should be made available under which section heading.
+ * The display name and description needs to be added manually as long as Ceph does not provide
+ * this information.
+ */
+@Injectable({
+ providedIn: ServicesModule
+})
+export class RbdConfigurationService {
+ readonly sections: RbdConfigurationSection[];
+
+ constructor(private i18n: I18n) {
+ this.sections = [
+ {
+ heading: this.i18n('Quality of Service'),
+ class: 'quality-of-service',
+ options: [
+ {
+ name: 'rbd_qos_bps_limit',
+ displayName: this.i18n('BPS Limit'),
+ description: this.i18n('The desired limit of IO bytes per second.'),
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_iops_limit',
+ displayName: this.i18n('IOPS Limit'),
+ description: this.i18n('The desired limit of IO operations per second.'),
+ type: RbdConfigurationType.iops
+ },
+ {
+ name: 'rbd_qos_read_bps_limit',
+ displayName: this.i18n('Read BPS Limit'),
+ description: this.i18n('The desired limit of read bytes per second.'),
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_read_iops_limit',
+ displayName: this.i18n('Read IOPS Limit'),
+ description: this.i18n('The desired limit of read operations per second.'),
+ type: RbdConfigurationType.iops
+ },
+ {
+ name: 'rbd_qos_write_bps_limit',
+ displayName: this.i18n('Write BPS Limit'),
+ description: this.i18n('The desired limit of write bytes per second.'),
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_write_iops_limit',
+ displayName: this.i18n('Write IOPS Limit'),
+ description: this.i18n('The desired limit of write operations per second.'),
+ type: RbdConfigurationType.iops
+ },
+ {
+ name: 'rbd_qos_bps_burst',
+ displayName: this.i18n('BPS Burst'),
+ description: this.i18n('The desired burst limit of IO bytes.'),
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_iops_burst',
+ displayName: this.i18n('IOPS Burst'),
+ description: this.i18n('The desired burst limit of IO operations.'),
+ type: RbdConfigurationType.iops
+ },
+ {
+ name: 'rbd_qos_read_bps_burst',
+ displayName: this.i18n('Read BPS Burst'),
+ description: this.i18n('The desired burst limit of read bytes.'),
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_read_iops_burst',
+ displayName: this.i18n('Read IOPS Burst'),
+ description: this.i18n('The desired burst limit of read operations.'),
+ type: RbdConfigurationType.iops
+ },
+ {
+ name: 'rbd_qos_write_bps_burst',
+ displayName: this.i18n('Write BPS Burst'),
+ description: this.i18n('The desired burst limit of write bytes.'),
+ type: RbdConfigurationType.bps
+ },
+ {
+ name: 'rbd_qos_write_iops_burst',
+ displayName: this.i18n('Write IOPS Burst'),
+ description: this.i18n('The desired burst limit of write operations.'),
+ type: RbdConfigurationType.iops
+ }
+ ] as RbdConfigurationExtraField[]
+ }
+ ];
+ }
+
+ private static getOptionsFromSections(sections: RbdConfigurationSection[]) {
+ return sections.map((section) => section.options).reduce((a, b) => a.concat(b));
+ }
+
+ private filterConfigOptionsByName(configName: string) {
+ return RbdConfigurationService.getOptionsFromSections(this.sections).filter(
+ (option) => option.name === configName
+ );
+ }
+
+ private getOptionValueByName(configName: string, fieldName: string, defaultValue = '') {
+ const configOptions = this.filterConfigOptionsByName(configName);
+ return configOptions.length === 1 ? configOptions.pop()[fieldName] : defaultValue;
+ }
+
+ getWritableSections() {
+ return this.sections.map((section) => {
+ section.options = section.options.filter((o) => !o.readOnly);
+ return section;
+ });
+ }
+
+ getOptionFields() {
+ return RbdConfigurationService.getOptionsFromSections(this.sections);
+ }
+
+ getWritableOptionFields() {
+ return RbdConfigurationService.getOptionsFromSections(this.getWritableSections());
+ }
+
+ getOptionByName(optionName: string): RbdConfigurationExtraField {
+ return this.filterConfigOptionsByName(optionName).pop();
+ }
+
+ getDisplayName(configName: string): string {
+ return this.getOptionValueByName(configName, 'displayName');
+ }
+
+ getDescription(configName: string): string {
+ return this.getOptionValueByName(configName, 'description');
+ }
+}
<context context-type="sourcefile">app/core/navigation/navigation/navigation.component.html</context>
<context context-type="linenumber">78</context>
</context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
+ <context context-type="linenumber">125</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/pool/pool-details/pool-details.component.html</context>
+ <context context-type="linenumber">22</context>
+ </context-group>
</trans-unit><trans-unit id="8c95898abff46bfac3ed6eb2afef74597e60b15c" datatype="html">
<source>CRUSH map</source>
<context-group purpose="location">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">303</context>
+ <context context-type="linenumber">315</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/cluster/configuration/configuration-form/configuration-form.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">443</context>
+ <context context-type="linenumber">451</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">49</context>
+ <context context-type="linenumber">51</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">91</context>
+ <context context-type="linenumber">93</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">151</context>
+ <context context-type="linenumber">153</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">175</context>
+ <context context-type="linenumber">177</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">36</context>
+ <context context-type="linenumber">38</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/cluster/configuration/configuration-form/configuration-form.component.html</context>
<source>{VAR_SELECT, select, editing {Edit} cloning {Clone} copying {Copy} other {Add} }</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">10</context>
+ <context context-type="linenumber">11</context>
</context-group>
</trans-unit><trans-unit id="52dd89f49fc440660cbbb3665b88d80f5baa7437" datatype="html">
<source>{VAR_SELECT, select, cloning {Clone from} copying {Copy from} other {Parent} }</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">20</context>
+ <context context-type="linenumber">22</context>
</context-group>
</trans-unit><trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
<source>Loading...</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">79</context>
+ <context context-type="linenumber">81</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">139</context>
+ <context context-type="linenumber">141</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">341</context>
+ <context context-type="linenumber">342</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html</context>
<source>-- No rbd pools available --</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">82</context>
+ <context context-type="linenumber">84</context>
</context-group>
</trans-unit><trans-unit id="ef83ec9c304a89d45650e580dcdc2978c37b3a83" datatype="html">
<source>-- Select a pool --</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">85</context>
+ <context context-type="linenumber">87</context>
</context-group>
</trans-unit><trans-unit id="49449943d8cbf59d8c401c8bd2e76f92e207cc5f" datatype="html">
<source>Use a dedicated data pool</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">105</context>
+ <context context-type="linenumber">107</context>
</context-group>
</trans-unit><trans-unit id="870aee0dd31a9643bf62007beb8f1ae1deb34d42" datatype="html">
<source>Data pool</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">116</context>
+ <context context-type="linenumber">118</context>
</context-group>
</trans-unit><trans-unit id="3792ca829d9b9f687e1f5d7733d30e9bb0bfec47" datatype="html">
<source>Dedicated pool that stores the object-data of the RBD.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">120</context>
+ <context context-type="linenumber">122</context>
</context-group>
</trans-unit><trans-unit id="151c80ea931037cd92e854442927f8a0f6ae7795" datatype="html">
<source>-- No data pools available --</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">142</context>
+ <context context-type="linenumber">144</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
<source>Size</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">160</context>
+ <context context-type="linenumber">162</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
<source>e.g., 10GiB</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">170</context>
+ <context context-type="linenumber">172</context>
</context-group>
</trans-unit><trans-unit id="0a88bbee20570aaf9615332fb27020627044874d" datatype="html">
<source>You have to increase the size.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">178</context>
+ <context context-type="linenumber">180</context>
</context-group>
</trans-unit><trans-unit id="6cdb1fea93d77c07950c0c76c6e0ad79ebbef084" datatype="html">
<source>Features</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">188</context>
+ <context context-type="linenumber">190</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
<source>Advanced...</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">211</context>
+ <context context-type="linenumber">213</context>
</context-group>
</trans-unit><trans-unit id="bc2e854e111ecf2bd7db170da5e3c2ed08181d88" datatype="html">
<source>Advanced</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">217</context>
+ <context context-type="linenumber">219</context>
+ </context-group>
+ </trans-unit><trans-unit id="3562a3778695a5f9c0445660e35301f0a39aaf73" datatype="html">
+ <source>Striping</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
+ <context context-type="linenumber">222</context>
</context-group>
</trans-unit><trans-unit id="ceac8e132384322ec778ba760875a6c6897d3e42" datatype="html">
<source>Object size</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">224</context>
+ <context context-type="linenumber">229</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
<source>Stripe unit</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">241</context>
+ <context context-type="linenumber">246</context>
</context-group>
</trans-unit><trans-unit id="84471be1049006edecbcaef1a32ae0893c229c50" datatype="html">
<source>-- Select stripe unit --</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">252</context>
+ <context context-type="linenumber">257</context>
</context-group>
</trans-unit><trans-unit id="8d32c5c54c8581c774a7f467fbd4e329b15a74fa" datatype="html">
<source>This field is required because stripe count is defined!</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">258</context>
+ <context context-type="linenumber">263</context>
</context-group>
</trans-unit><trans-unit id="6bbf9040be7c5491d4a03f2185708f43a6582a3b" datatype="html">
<source>Stripe unit is greater than object size.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">261</context>
+ <context context-type="linenumber">266</context>
</context-group>
</trans-unit><trans-unit id="a682f49f9b761591661276d7c6f550e641a130a4" datatype="html">
<source>Stripe count</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">270</context>
+ <context context-type="linenumber">275</context>
</context-group>
</trans-unit><trans-unit id="baa74031990c5370008ba622d0a250f0929097f4" datatype="html">
<source>This field is required because stripe unit is defined!</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">283</context>
+ <context context-type="linenumber">288</context>
</context-group>
</trans-unit><trans-unit id="cd2ada6d5ecbd5cbf89eae0a1f5326efedac0dbc" datatype="html">
<source>Stripe count must be greater than 0.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">286</context>
+ <context context-type="linenumber">291</context>
+ </context-group>
+ </trans-unit><trans-unit id="21afa21d069c7fdfc742e6fbbe7e6c0f6ff4e580" datatype="html">
+ <source><x id="ICU" equiv-text="{mode, select, editing {...} cloning {...} copying {...} other {...}}"/> RBD</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
+ <context context-type="linenumber">310</context>
</context-group>
</trans-unit><trans-unit id="96d97d99d8c30942ace7d29cad9dcfb5d32315a1" datatype="html">
<source>{VAR_SELECT, select, editing {Update} cloning {Clone} copying {Copy} other {Create} }</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">298</context>
+ <context context-type="linenumber">310</context>
</context-group>
</trans-unit><trans-unit id="aba82bfd8e177d35b76cad7cd43941f8e5e5acac" datatype="html">
<source>Trash</source>
<source>Applications</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">290</context>
+ <context context-type="linenumber">291</context>
</context-group>
</trans-unit><trans-unit id="2208d63d5940ce656006a220102b1eb2b5e553da" datatype="html">
<source>Compression</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">308</context>
+ <context context-type="linenumber">309</context>
</context-group>
</trans-unit><trans-unit id="37e10df2d9c0c25ef04ac112c9c9a7723e8efae0" datatype="html">
<source>Mode</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">314</context>
+ <context context-type="linenumber">315</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html</context>
<source>Algorithm</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">333</context>
+ <context context-type="linenumber">334</context>
</context-group>
</trans-unit><trans-unit id="4903231d42089325a28892c0fde1aed46b733ae6" datatype="html">
<source>-- No erasure compression algorithm available --</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">344</context>
+ <context context-type="linenumber">345</context>
</context-group>
</trans-unit><trans-unit id="5d68ddb254275f8f44221e9ad6d8ceeb59ca46a6" datatype="html">
<source>Minimum blob size</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">358</context>
+ <context context-type="linenumber">359</context>
</context-group>
</trans-unit><trans-unit id="fb2f176df80647137cbb02bbeb29e5dec707a400" datatype="html">
<source>e.g., 128KiB</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">367</context>
+ <context context-type="linenumber">368</context>
</context-group>
</trans-unit><trans-unit id="1b7f6e53a4521c6eb3ced4c007fdd4cf80bb7707" datatype="html">
<source>Value should be greater than 0</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">372</context>
+ <context context-type="linenumber">373</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">397</context>
+ <context context-type="linenumber">398</context>
</context-group>
</trans-unit><trans-unit id="54be22af9625a595ee5d70917ee1fb116be7c4be" datatype="html">
<source>Value should be greater than the maximum blob size</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">375</context>
+ <context context-type="linenumber">376</context>
</context-group>
</trans-unit><trans-unit id="151efb127a9a4dd25259a0b2055442318a141f5b" datatype="html">
<source>Maximum blob size</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">384</context>
+ <context context-type="linenumber">385</context>
</context-group>
</trans-unit><trans-unit id="0c656f0e346bbadf46eb1a5d20d0307a3bd20ba8" datatype="html">
<source>e.g., 512KiB</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">392</context>
+ <context context-type="linenumber">393</context>
</context-group>
</trans-unit><trans-unit id="0a65a24eee8a026f3b1113fe9e157e9a0dd69486" datatype="html">
<source>Value should be greater than the minimum blob size</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">400</context>
+ <context context-type="linenumber">401</context>
</context-group>
</trans-unit><trans-unit id="261ba09c4a59de83f48f52a23fd328da37e61ff4" datatype="html">
<source>Ratio</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">409</context>
+ <context context-type="linenumber">410</context>
</context-group>
</trans-unit><trans-unit id="c1430457a9c3c66366e51d76bf10396014c576be" datatype="html">
<source>Compression ratio</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">420</context>
+ <context context-type="linenumber">421</context>
</context-group>
</trans-unit><trans-unit id="ae5ce6de352cde949998fb10754459c3a4eb183b" datatype="html">
<source>Value should be between 0.0 and 1.0</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">423</context>
+ <context context-type="linenumber">424</context>
</context-group>
</trans-unit><trans-unit id="c6d3131ce5386154504dc91b5ad08855d85881d3" datatype="html">
<source><x id="ICU" equiv-text="{editing, select, 1 {...} other {...}}"/> pool</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">438</context>
+ <context context-type="linenumber">446</context>
</context-group>
</trans-unit><trans-unit id="e2b15aacc2f17a9c3276895eeeaadd342ab26555" datatype="html">
<source>{VAR_SELECT, select, 1 {Edit} other {Create} }</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">438</context>
+ <context context-type="linenumber">446</context>
</context-group>
</trans-unit><trans-unit id="1d8a7c8aea58294a3c57c23af0468ddf0ba0c9c7" datatype="html">
<source>Pools List</source>
<context context-type="sourcefile">app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html</context>
<context context-type="linenumber">12</context>
</context-group>
+ </trans-unit><trans-unit id="5decb3917d46a9ac6e5813699801becb7c3c1455" datatype="html">
+ <source>Global</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.html</context>
+ <context context-type="linenumber">10</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
+ <context context-type="linenumber">137</context>
+ </context-group>
+ </trans-unit><trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
+ <source>Image</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.html</context>
+ <context context-type="linenumber">11</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
+ <context context-type="linenumber">135</context>
+ </context-group>
+ </trans-unit><trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
+ <source>Pool</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.html</context>
+ <context context-type="linenumber">12</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
+ <context context-type="linenumber">18</context>
+ </context-group>
+ </trans-unit><trans-unit id="ff92fbdec9fdd5054493eeda0d7ee8b450f83e72" datatype="html">
+ <source>RBD Configuration</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.html</context>
+ <context context-type="linenumber">2</context>
+ </context-group>
+ </trans-unit><trans-unit id="b62d9efc8eb3b589904f6cb96a0406bbda55673a" datatype="html">
+ <source>Remove the local configuration value. The parent configuration value will be inherited and used instead.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.html</context>
+ <context context-type="linenumber">60</context>
+ </context-group>
+ </trans-unit><trans-unit id="963488a1010d46067b238c4ae917fab3907c0a3a" datatype="html">
+ <source>The mininum value is 0</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.html</context>
+ <context context-type="linenumber">73</context>
+ </context-group>
</trans-unit><trans-unit id="91853167141c37b58868f3b0421383dd72fa8a01" datatype="html">
<source>Attributes (OSD map)</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
- </trans-unit><trans-unit id="e70fcca5a99575cffef3ff8cbd5e69f06ffd0f1c" datatype="html">
- <source>Pool</source>
- <context-group purpose="location">
- <context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
- <context context-type="linenumber">18</context>
- </context-group>
</trans-unit><trans-unit id="03cc5b14b0a20d075e9009ff021f4f1660ba348a" datatype="html">
<source>Data Pool</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
<context context-type="linenumber">119</context>
</context-group>
+ </trans-unit><trans-unit id="58e58f1a8786da9031a05e6770c5dafce82badf5" datatype="html">
+ <source>This setting overrides the global value</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
+ <context context-type="linenumber">135</context>
+ </context-group>
+ </trans-unit><trans-unit id="36b46714164964c6258b08ed0a25f57d8a950f92" datatype="html">
+ <source>This is the global value. No value for this option has been set for this image.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
+ <context context-type="linenumber">137</context>
+ </context-group>
</trans-unit><trans-unit id="3f67f30568e9ae47507d46e28e1e82a7dca772e2" datatype="html">
<source><x id="ICU" equiv-text="{ editing, select, true {...} other {...}}"/> RBD Snapshot</source>
<context-group purpose="location">
<source>Cache Tiers Details</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-details/pool-details.component.html</context>
- <context context-type="linenumber">22</context>
+ <context context-type="linenumber">27</context>
</context-group>
</trans-unit><trans-unit id="0c1e17956453ad772dbe82d6946f62748c692f3e" datatype="html">
<source>Ranks</source>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
- <trans-unit id="a5f9ba9bb9faa8284bcadb1cdbc6aaf969e9c4bb" datatype="html">
- <source>Image</source>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/ceph/block/iscsi/iscsi.component.ts</context>
- <context context-type="linenumber">1</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/ceph/block/mirroring/image-list/image-list.component.ts</context>
- <context context-type="linenumber">1</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/ceph/block/mirroring/image-list/image-list.component.ts</context>
- <context context-type="linenumber">1</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/ceph/block/mirroring/image-list/image-list.component.ts</context>
- <context context-type="linenumber">1</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.ts</context>
- <context context-type="linenumber">1</context>
- </context-group>
- </trans-unit>
<trans-unit id="2cd86fc9c2dc1b4398514266a08e507140fe5ba8" datatype="html">
<source>Active/Optimized</source>
<context-group purpose="location">
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
+ <trans-unit id="a6493c6ca346cd052da40423eda9c132de2b2002" datatype="html">
+ <source>Key</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
+ <source>Value</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
<trans-unit id="4130b1a836f2000cfe9a11fb62493dc42d444de7" datatype="html">
<source>Deep flatten</source>
<context-group purpose="location">
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
- <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
- <source>Value</source>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts</context>
- <context context-type="linenumber">1</context>
- </context-group>
- </trans-unit>
<trans-unit id="60fb10c145f9c7ede2b9ddf9b2b0b0f6191d0ebd" datatype="html">
<source>Cache Mode</source>
<context-group purpose="location">
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
+ <trans-unit id="8fd98fb2a0f24ba72cde0787dd3f02cfc17de469" datatype="html">
+ <source>Quality of Service</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="2c53f3ce2c6fff9025a7b41cc334521725da6e7b" datatype="html">
+ <source>BPS Limit</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="e897624a392419386b035bcd8529337e262fc41d" datatype="html">
+ <source>The desired limit of IO bytes per second.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="dc56f77e4507da7e86a3accd0a2a7099a838d539" datatype="html">
+ <source>IOPS Limit</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="141c3f6eadbb35b0e17b2f847e4cac23240a6ea9" datatype="html">
+ <source>The desired limit of IO operations per second.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="3b3b101f16c38806e984b157e0cf22cd21451122" datatype="html">
+ <source>Read BPS Limit</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="87835fabb6a645873f3829a450ca1d58f97754f2" datatype="html">
+ <source>The desired limit of read bytes per second.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="28b485e700df46d70b96666099f714e3ae9e5c2e" datatype="html">
+ <source>Read IOPS Limit</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="5907bd1a5cf3e20e740bca676cb1538372e1f9ff" datatype="html">
+ <source>The desired limit of read operations per second.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="07317121f0b194987d4dee8b322f521ac17bf124" datatype="html">
+ <source>Write BPS Limit</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="4022c78bd9132ca2ec67927f8484c437d1d9b6fd" datatype="html">
+ <source>The desired limit of write bytes per second.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="52c22c19f98ff3a09bd811292954a889b93d28b7" datatype="html">
+ <source>Write IOPS Limit</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="0ed62e3fd8654b3191fc7fa959ad3f10f1ea773a" datatype="html">
+ <source>The desired limit of write operations per second.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="d90c99d77864f21f932ee1369019f400c4ad0e2d" datatype="html">
+ <source>BPS Burst</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="29c32192f4d185139aaaf6d3a766a3411dfeabbe" datatype="html">
+ <source>The desired burst limit of IO bytes.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="b988b90489e899941e196c3dd0ecc22e37540224" datatype="html">
+ <source>IOPS Burst</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="02c5de8ff11870f1b3896f2b45637ce0dba1ed4b" datatype="html">
+ <source>The desired burst limit of IO operations.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="7a160538129b37cf595f1651f28b5fac62fbc0f8" datatype="html">
+ <source>Read BPS Burst</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="b573a5df16a53b9947288461bc24e6a0eb4341d6" datatype="html">
+ <source>The desired burst limit of read bytes.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="1d5b8c2a0c653651cc4ca4b8fed9456f449220d1" datatype="html">
+ <source>Read IOPS Burst</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="20b199f9d4a8d08ffefec66040e27639ec221512" datatype="html">
+ <source>The desired burst limit of read operations.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="824525ae57992aee629ddffcd500a8b77087ddec" datatype="html">
+ <source>Write BPS Burst</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="a90aac4050a5d79765da5e1186909f6d4d7f981d" datatype="html">
+ <source>The desired burst limit of write bytes.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="35a8c6e6c249c7d16e26aa7c4ef97c06db405070" datatype="html">
+ <source>Write IOPS Burst</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="b356abd41d7e9eea4d20c03d2e6892b6d435ecbd" datatype="html">
+ <source>The desired burst limit of write operations.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/services/rbd-configuration.service.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
<trans-unit id="24c4d50fe8567de381a20a1745f1b6d37eacaa90" datatype="html">
<source>Failed to <x id="INTERPOLATION" equiv-text="{{failure}}"/> <x id="INTERPOLATION_1" equiv-text="{{metadata}}"/></source>
<context-group purpose="location">
.block-ui-wrapper {
background: $color-transparent-black !important;
}
+
+h3.page-header {
+ margin-left: 1em;
+ margin-top: 1em;
+ border-color: #f0f0f0;
+}
+
+.tooltip-wide .tooltip-inner {
+ width: 400px;
+}
+
+.tooltip-inner {
+ background-color: white;
+ border: 1px solid grey;
+ color: #333;
+ font-size: 1.1em;
+}
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import rbd
+from .. import mgr
+
+
+class RbdConfiguration(object):
+ _rbd = rbd.RBD()
+
+ def __init__(self, pool_name='', image_name='', pool_ioctx=None, image_ioctx=None):
+ # type: (str, str, object, object) -> None
+ assert bool(pool_name) != bool(pool_ioctx) # xor
+ self._pool_name = pool_name
+ self._image_name = image_name
+ self._pool_ioctx = pool_ioctx
+ self._image_ioctx = image_ioctx
+
+ @staticmethod
+ def _ensure_prefix(option):
+ # type: (str) -> str
+ return option if option.startswith('conf_') else 'conf_' + option
+
+ def list(self):
+ # type: () -> [dict]
+ def _list(ioctx):
+ if self._image_name: # image config
+ with rbd.Image(ioctx, self._image_name) as image:
+ result = image.config_list()
+ else: # pool config
+ result = self._rbd.config_list(ioctx)
+ return list(result)
+
+ if self._pool_name:
+ ioctx = mgr.rados.open_ioctx(self._pool_name)
+ else:
+ ioctx = self._pool_ioctx
+
+ return _list(ioctx)
+
+ def get(self, option_name):
+ # type: (str) -> str
+ option_name = self._ensure_prefix(option_name)
+ with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
+ if self._image_name:
+ with rbd.Image(pool_ioctx, self._image_name) as image:
+ return image.metadata_get(option_name)
+ return self._rbd.pool_metadata_get(pool_ioctx, option_name)
+
+ def set(self, option_name, option_value):
+ # type: (str, str) -> None
+
+ option_value = str(option_value)
+ option_name = self._ensure_prefix(option_name)
+
+ pool_ioctx = self._pool_ioctx
+ if self._pool_name: # open ioctx
+ pool_ioctx = mgr.rados.open_ioctx(self._pool_name)
+ pool_ioctx.__enter__()
+
+ image_ioctx = self._image_ioctx
+ if self._image_name:
+ image_ioctx = rbd.Image(pool_ioctx, self._image_name)
+ image_ioctx.__enter__()
+
+ if image_ioctx:
+ image_ioctx.metadata_set(option_name, option_value)
+ else:
+ self._rbd.pool_metadata_set(pool_ioctx, option_name, option_value)
+
+ if self._image_name: # Name provided, so we opened it and now have to close it
+ image_ioctx.__exit__(None, None, None)
+ if self._pool_name:
+ pool_ioctx.__exit__(None, None, None)
+
+ def remove(self, option_name):
+ """
+ Removes an option by name. Will not raise an error, if the option hasn't been found.
+ :type option_name str
+ """
+ def _remove(ioctx):
+ try:
+ if self._image_name:
+ with rbd.Image(ioctx, self._image_name) as image:
+ image.metadata_remove(option_name)
+ else:
+ self._rbd.pool_metadata_remove(ioctx, option_name)
+ except KeyError:
+ pass
+
+ option_name = self._ensure_prefix(option_name)
+
+ if self._pool_name:
+ with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
+ _remove(pool_ioctx)
+ else:
+ _remove(self._pool_ioctx)
+
+ def set_configuration(self, configuration):
+ if configuration:
+ for option_name, option_value in configuration.items():
+ if option_value is not None:
+ self.set(option_name, option_value)
+ else:
+ self.remove(option_name)
if content_type in ['application/json', 'text/javascript']:
if not hasattr(request, 'json'):
raise cherrypy.HTTPError(400, 'Expected JSON body')
- params.update(request.json.items())
+ if isinstance(request.json, str):
+ params.update(json.loads(request.json))
+ else:
+ params.update(request.json)
return params