import six
import time
-from .helper import DashboardTestCase, JObj, JList
+from .helper import DashboardTestCase, JAny, JList, JObj
log = logging.getLogger(__name__)
'flags_names': str,
}, allow_unknown=True)
+ pool_list_stat_schema = JObj(sub_elems={
+ 'latest': int,
+ 'rate': float,
+ 'series': JList(JAny(none=False)),
+ })
+
+ pool_list_stats_schema = JObj(sub_elems={
+ 'bytes_used': pool_list_stat_schema,
+ 'max_avail': pool_list_stat_schema,
+ 'rd_bytes': pool_list_stat_schema,
+ 'wr_bytes': pool_list_stat_schema,
+ 'rd': pool_list_stat_schema,
+ 'wr': pool_list_stat_schema,
+ }, allow_unknown=True)
+
def _pool_create(self, data):
try:
self._task_post('/api/pool/', data)
cluster_pools = self.ceph_cluster.mon_manager.list_pools()
self.assertEqual(len(cluster_pools), len(data))
+ self.assertSchemaBody(JList(self.pool_schema))
for pool in data:
self.assertIn('pool_name', pool)
self.assertIn('type', pool)
self.assertIn('application_metadata', pool)
self.assertIn('flags', pool)
self.assertIn('pg_status', pool)
- self.assertIn('stats', pool)
+ self.assertSchema(pool['stats'], self.pool_list_stats_schema)
self.assertIn('flags_names', pool)
self.assertIn(pool['pool_name'], cluster_pools)
self.assertIn('type', pool)
self.assertIn('flags', pool)
self.assertNotIn('pg_status', pool)
- self.assertIn('stats', pool)
+ self.assertSchema(pool['stats'], self.pool_list_stats_schema)
self.assertNotIn('flags_names', pool)
def test_pool_create(self):
<tab i18n-heading
heading="Details">
<cd-table-key-value [renderObjects]="true"
- [data]="selection.first()"
+ [data]="getPoolDetails(selection.first())"
[autoReload]="false">
</cd-table-key-value>
</tab>
</tab>
</tabset>
</cd-table>
+
+ <ng-template #poolUsageTpl
+ let-row="row">
+ <cd-usage-bar *ngIf="row.stats?.max_avail?.latest"
+ [totalBytes]="row.stats.bytes_used.latest + row.stats.max_avail.latest"
+ [usedBytes]="row.stats.bytes_used.latest">
+ </cd-usage-bar>
+ </ng-template>
</tab>
<tab i18n-heading
describe('transformPoolsData', () => {
it('transforms pools data correctly', () => {
- const pools = [{ stats: { rate: 0 }, pg_status: { 'active+clean': 8, down: 2 } }];
- const expected = [{ pg_status: '8 active+clean, 2 down' }];
+ const pools = [
+ {
+ stats: { rd_bytes: { latest: 6, rate: 4, series: [[0, 2], [1, 6]] } },
+ pg_status: { 'active+clean': 8, down: 2 }
+ }
+ ];
+ const expected = [
+ {
+ cdIsBinary: true,
+ pg_status: '8 active+clean, 2 down',
+ stats: {
+ bytes_used: { latest: 0, rate: 0, series: [] },
+ max_avail: { latest: 0, rate: 0, series: [] },
+ rd: { latest: 0, rate: 0, series: [] },
+ rd_bytes: { latest: 6, rate: 4, series: [2, 6] },
+ wr: { latest: 0, rate: 0, series: [] },
+ wr_bytes: { latest: 0, rate: 0, series: [] }
+ }
+ }
+ ];
expect(component.transformPoolsData(pools)).toEqual(expected);
});
});
describe('transformPgStatus', () => {
- it('returns ststus groups correctly', () => {
+ it('returns status groups correctly', () => {
const pgStatus = { 'active+clean': 8 };
const expected = '8 active+clean';
expect(component.transformPgStatus(pgStatus)).toEqual(expected);
});
});
+
+ describe('getPoolDetails', () => {
+ it('returns pool details corretly', () => {
+ const pool = { prop1: 1, cdIsBinary: true, prop2: 2, cdExecuting: true, prop3: 3 };
+ const expected = { prop1: 1, prop2: 2, prop3: 3 };
+
+ expect(component.getPoolDetails(pool)).toEqual(expected);
+ });
+ });
});
-import { Component, OnInit, ViewChild } from '@angular/core';
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { I18n } from '@ngx-translate/i18n-polyfill';
import * as _ from 'lodash';
import { ExecutingTask } from '../../../shared/models/executing-task';
import { FinishedTask } from '../../../shared/models/finished-task';
import { Permissions } from '../../../shared/models/permissions';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { TaskListService } from '../../../shared/services/task-list.service';
import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
export class PoolListComponent implements OnInit {
@ViewChild(TableComponent)
table: TableComponent;
+ @ViewChild('poolUsageTpl')
+ poolUsageTpl: TemplateRef<any>;
pools: Pool[] = [];
columns: CdTableColumn[];
private taskListService: TaskListService,
private modalService: BsModalService,
private i18n: I18n,
- private pgCategoryService: PgCategoryService
+ private pgCategoryService: PgCategoryService,
+ private dimlessPipe: DimlessPipe
) {
this.permissions = this.authStorageService.getPermissions();
this.tableActions = [
name: this.i18n('Delete')
}
];
+ }
+
+ ngOnInit() {
this.columns = [
{
prop: 'pool_name',
name: this.i18n('Name'),
- flexGrow: 3,
+ flexGrow: 4,
cellTransformation: CellTemplate.executing
},
{
{
prop: 'application_metadata',
name: this.i18n('Applications'),
- flexGrow: 3
+ flexGrow: 2
},
{
prop: 'pg_status',
name: this.i18n('PG Status'),
- flexGrow: 1,
+ flexGrow: 3,
cellClass: ({ row, column, value }): any => {
return this.getPgStatusCellClass({ row, column, value });
}
{
prop: 'crush_rule',
name: this.i18n('Crush Ruleset'),
- flexGrow: 2
+ flexGrow: 3
+ },
+ { name: this.i18n('Usage'), cellTemplate: this.poolUsageTpl, flexGrow: 3 },
+ {
+ prop: 'stats.rd_bytes.series',
+ name: this.i18n('Read bytes'),
+ cellTransformation: CellTemplate.sparkline,
+ flexGrow: 3
+ },
+ {
+ prop: 'stats.wr_bytes.series',
+ name: this.i18n('Write bytes'),
+ cellTransformation: CellTemplate.sparkline,
+ flexGrow: 3
+ },
+ {
+ prop: 'stats.rd.rate',
+ name: this.i18n('Read ops'),
+ flexGrow: 1,
+ pipe: this.dimlessPipe,
+ cellTransformation: CellTemplate.perSecond
+ },
+ {
+ prop: 'stats.wr.rate',
+ name: this.i18n('Write ops'),
+ flexGrow: 1,
+ pipe: this.dimlessPipe,
+ cellTransformation: CellTemplate.perSecond
}
];
- }
- ngOnInit() {
this.taskListService.init(
() => this.poolService.getList(),
undefined,
}
transformPoolsData(pools: any) {
- _.map(pools, (pool: object) => {
- delete pool['stats'];
+ const requiredStats = ['bytes_used', 'max_avail', 'rd_bytes', 'wr_bytes', 'rd', 'wr'];
+ const emptyStat = { latest: 0, rate: 0, series: [] };
+
+ _.forEach(pools, (pool: Pool) => {
pool['pg_status'] = this.transformPgStatus(pool['pg_status']);
+ const stats = {};
+ _.forEach(requiredStats, (stat) => {
+ stats[stat] = pool.stats && pool.stats[stat] ? pool.stats[stat] : emptyStat;
+ });
+ pool['stats'] = stats;
+
+ ['rd_bytes', 'wr_bytes'].forEach((stat) => {
+ pool.stats[stat].series = pool.stats[stat].series.map((point) => point[1]);
+ });
+ pool.cdIsBinary = true;
});
return pools;
return strings.join(', ');
}
+
+ getPoolDetails(pool: object) {
+ return _.omit(pool, ['cdExecuting', 'cdIsBinary']);
+ }
}
--- /dev/null
+export class PoolStat {
+ latest: number;
+ rate: number;
+ series: number[];
+}
import { ExecutingTask } from '../../shared/models/executing-task';
+import { PoolStat } from './pool-stat';
export class Pool {
cache_target_full_ratio_micro: number;
flags_names: string;
tier_of: number;
hit_set_grade_decay_rate: number;
- pg_placement_num: number;
use_gmt_hitset: boolean;
last_force_op_resend_preluminous: string;
quota_max_bytes: number;
target_max_objects: number;
pg_num: number;
type: string;
- grade_table: any[];
pool_name: string;
cache_min_evict_age: number;
- snap_mode: string;
cache_mode: string;
min_size: number;
cache_target_dirty_high_ratio_micro: number;
hit_set_count: number;
flags: number;
target_max_bytes: number;
- snap_epoch: number;
hit_set_search_last_n: number;
last_change: string;
min_write_recency_for_promote: number;
read_tier: number;
pg_status: string;
+ stats?: {
+ bytes_used?: PoolStat;
+ max_avail?: PoolStat;
+ rd_bytes?: PoolStat;
+ wr_bytes?: PoolStat;
+ rd?: PoolStat;
+ wr?: PoolStat;
+ };
+ cdIsBinary?: boolean;
constructor(name) {
this.pool_name = name;
];
expect(component._insertFlattenObjects(v)).toEqual([
{ key: 'no', value: 'change' },
+ { key: 'first layer', value: 'something' },
{ key: 'first second l3_1', value: 33 },
- { key: 'first second l3_2', value: 44 },
- { key: 'first layer', value: 'something' }
+ { key: 'first second l3_2', value: 44 }
]);
});
sub3: 56
}
},
- someKey: 0
+ someKey: 0,
+ additionalKeyContainingObject: { type: 'none' },
+ keyWithEmptyObject: {}
};
component.renderObjects = true;
});
it('with parent key', () => {
component.ngOnInit();
expect(component.tableData).toEqual([
+ { key: 'someKey', value: 0 },
+ { key: 'keyWithEmptyObject', value: '' },
{ key: 'options someSetting1', value: 38 },
{ key: 'options anotherSetting2', value: 'somethingElse' },
{ key: 'options suboptions sub1', value: 12 },
{ key: 'options suboptions sub2', value: 34 },
{ key: 'options suboptions sub3', value: 56 },
- { key: 'someKey', value: 0 }
+ { key: 'additionalKeyContainingObject type', value: 'none' }
]);
});
component.appendParentKey = false;
component.ngOnInit();
expect(component.tableData).toEqual([
+ { key: 'someKey', value: 0 },
+ { key: 'keyWithEmptyObject', value: '' },
{ key: 'someSetting1', value: 38 },
{ key: 'anotherSetting2', value: 'somethingElse' },
{ key: 'sub1', value: 12 },
{ key: 'sub2', value: 34 },
{ key: 'sub3', value: 56 },
- { key: 'someKey', value: 0 }
+ { key: 'type', value: 'none' }
]);
});
});
}
_insertFlattenObjects(temp: any[]) {
+ const itemsToRemoveIndexes = [];
+ const itemsToAdd = [];
temp.forEach((v, i) => {
if (_.isObject(v.value)) {
- temp.splice(i, 1);
- this._makePairs(v.value).forEach((item) => {
- if (this.appendParentKey) {
- item.key = v.key + ' ' + item.key;
- }
- temp.splice(i, 0, item);
- i++;
- });
+ if (_.isEmpty(v.value)) {
+ temp[i]['value'] = '';
+ } else {
+ itemsToRemoveIndexes.push(i);
+ this._makePairs(v.value).forEach((item) => {
+ if (this.appendParentKey) {
+ item.key = v.key + ' ' + item.key;
+ }
+ itemsToAdd.push(item);
+ i++;
+ });
+ }
}
});
+
+ _.remove(temp, (item, itemIndex) => {
+ return _.includes(itemsToRemoveIndexes, itemIndex);
+ });
+ itemsToAdd.forEach((item) => {
+ temp.push(item);
+ });
+
return temp;
}
Object.defineProperty(window, 'localStorage', { value: mock() });
Object.defineProperty(window, 'sessionStorage', { value: mock() });
Object.defineProperty(window, 'getComputedStyle', {
- value: () => ['-webkit-appearance']
+ value: () => ({
+ getPropertyValue: (prop) => {
+ return '';
+ }
+ })
});
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/pool/pool-list/pool-list.component.html</context>
- <context context-type="linenumber">40</context>
+ <context context-type="linenumber">48</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html</context>
<context context-type="sourcefile">src/app/ceph/cluster/osd/osd-list/osd-list.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/pool/pool-list/pool-list.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
</trans-unit>
<trans-unit id="7db875ff4cc626d394f37fba95c075b2e4bfeb00" datatype="html">
<source>Standby daemons</source>
<context context-type="sourcefile">src/app/ceph/cluster/osd/osd-list/osd-list.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/pool/pool-list/pool-list.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
</trans-unit>
<trans-unit id="ecbe2fdca919499125bc42c2c67cedae8563db1c" datatype="html">
<source>Writes bytes</source>
<context context-type="sourcefile">src/app/ceph/cluster/osd/osd-list/osd-list.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/pool/pool-list/pool-list.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
</trans-unit>
<trans-unit id="7e0b24a17546cbfab48b03dc874461d1ca68bc9a" datatype="html">
<source>Write ops</source>
<context context-type="sourcefile">src/app/ceph/cluster/osd/osd-list/osd-list.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/pool/pool-list/pool-list.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
</trans-unit>
<trans-unit id="d45455406092f58e6b089cb440b5b7934b801a01" datatype="html">
<source>Mark OSD <x id="INTERPOLATION" equiv-text="{{markAction}}"/></source>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
+ <trans-unit id="f15bf7f588f09ca79795b17bc244e8d336f28171" datatype="html">
+ <source>Write bytes</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/pool/pool-list/pool-list.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
<trans-unit id="44939fd05cedfd077db886528b755e77d5fa3885" datatype="html">
<source>bucket</source>
<context-group purpose="location">
"""
from __future__ import absolute_import
+import collections
import errno
from distutils.version import StrictVersion
from distutils.util import strtobool
import socket
import tempfile
import threading
+import time
from uuid import uuid4
from OpenSSL import crypto
]
MODULE_OPTIONS.extend(options_schema_list())
+ __pool_stats = collections.defaultdict(lambda: collections.defaultdict(
+ lambda: collections.deque(maxlen=10)))
+
def __init__(self, *args, **kwargs):
super(Module, self).__init__(*args, **kwargs)
CherryPyConfig.__init__(self)
def notify(self, notify_type, notify_id):
NotificationQueue.new_notification(notify_type, notify_id)
+ def get_updated_pool_stats(self):
+ df = self.get('df')
+ pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
+ now = time.time()
+ for pool_id, stats in pool_stats.items():
+ for stat_name, stat_val in stats.items():
+ self.__pool_stats[pool_id][stat_name].append((now, stat_val))
+
+ return self.__pool_stats
+
class StandbyModule(MgrStandbyModule, CherryPyConfig):
def __init__(self, *args, **kwargs):
# -*- coding: utf-8 -*-
from __future__ import absolute_import
-import time
-import collections
-from collections import defaultdict
import json
import rados
pools_w_stats = []
pg_summary = mgr.get("pg_summary")
- pool_stats = defaultdict(lambda: defaultdict(
- lambda: collections.deque(maxlen=10)))
-
- df = mgr.get("df")
- pool_stats_dict = dict([(p['id'], p['stats']) for p in df['pools']])
- now = time.time()
- for pool_id, stats in pool_stats_dict.items():
- for stat_name, stat_val in stats.items():
- pool_stats[pool_id][stat_name].appendleft((now, stat_val))
+ pool_stats = mgr.get_updated_pool_stats()
for pool in pools:
pool['pg_status'] = pg_summary['by_pool'][pool['pool'].__str__()]
def get_rate(series):
if len(series) >= 2:
- return differentiate(*series[0:1])
+ return differentiate(*list(series)[-2:])
return 0
for stat_name, stat_series in stats.items():