import copy
import os
import time
+from collections import Counter
from typing import Dict, List, Optional
import cherrypy
"type": (str, "type of service"),
"id": (str, "Service Id"),
}], "Services related to the host"),
+ "service_instances": ([{
+ "type": (str, "type of service"),
+ "count": (int, "Number of instances of the service"),
+ }], "Service instances related to the host"),
"ceph_version": (str, "Ceph version"),
"addr": (str, "Host address"),
"labels": ([str], "Labels related to the host"),
}, orch_hosts_map[hostname]) for hostname in orch_hosts_map
]
hosts.extend(orch_hosts_only)
+ for host in hosts:
+ host['service_instances'] = populate_service_instances(
+ host['hostname'], host['services'])
return hosts
+def populate_service_instances(hostname, services):
+ orch = OrchClient.instance()
+ if orch.available():
+ services = (daemon['daemon_type']
+ for daemon in (d.to_dict()
+ for d in orch.services.list_daemons(hostname=hostname)))
+ else:
+ services = (daemon['type'] for daemon in services)
+ return [{'type': k, 'count': v} for k, v in Counter(services).items()]
+
+
def get_hosts(sources=None):
"""
Get hosts from various sources.
orch = OrchClient.instance()
if orch.available():
return merge_hosts_by_hostname(ceph_hosts, orch.hosts.list())
+ for host in ceph_hosts:
+ host['service_instances'] = populate_service_instances(host['hostname'], host['services'])
return ceph_hosts
'facts': (bool, 'Host Facts')
},
responses={200: LIST_HOST_SCHEMA})
- @RESTController.MethodMap(version=APIVersion(1, 1))
+ @RESTController.MethodMap(version=APIVersion(1, 2))
def list(self, sources=None, facts=False):
hosts = get_hosts(sources)
orch = OrchClient.instance()
<div [ngbNavOutlet]="nav"></div>
<ng-template #servicesTpl
- let-value="value">
- <span *ngFor="let instance of value; last as isLast">
- <span class="badge badge-background-primary ms-1" >{{ instance }}</span>
+ let-services="value">
+ <span *ngFor="let service of services">
+ <cd-label [key]="service['type']"
+ [value]="service['count']"
+ class="me-1"></cd-label>
</span>
</ng-template>
const hostname = 'ceph.dev';
const payload = [
{
- services: [
- {
- type: 'mgr',
- id: 'x'
- },
+ service_instances: [
{
type: 'mgr',
- id: 'y'
+ count: 2
},
{
type: 'osd',
- id: '0'
- },
- {
- type: 'osd',
- id: '1'
- },
- {
- type: 'osd',
- id: '2'
+ count: 3
},
{
type: 'rgw',
- id: 'rgw'
+ count: 1
}
],
hostname: hostname,
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import _ from 'lodash';
-import { Observable, Subscription } from 'rxjs';
-import { map, mergeMap } from 'rxjs/operators';
+import { Subscription } from 'rxjs';
+import { mergeMap } from 'rxjs/operators';
import { HostService } from '~/app/shared/api/host.service';
import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
-import { Daemon } from '~/app/shared/models/daemon.interface';
import { FinishedTask } from '~/app/shared/models/finished-task';
import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
this.orchStatus = orchStatus;
const factsAvailable = this.checkHostsFactsAvailable();
return this.hostService.list(`${factsAvailable}`);
- }),
- map((hostList: object[]) =>
- hostList.map((host) => {
- const counts = {};
- host['service_instances'] = new Set<string>();
- if (this.orchStatus?.available) {
- let daemons: Daemon[] = [];
- let observable: Observable<Daemon[]>;
- observable = this.hostService.getDaemons(host['hostname']);
- observable.subscribe((dmns: Daemon[]) => {
- daemons = dmns;
- daemons.forEach((daemon: any) => {
- counts[daemon.daemon_type] = (counts[daemon.daemon_type] || 0) + 1;
- });
- daemons.map((daemon: any) => {
- host['service_instances'].add(
- `${daemon.daemon_type}: ${counts[daemon.daemon_type]}`
- );
- });
- });
- } else {
- host['services'].forEach((service: any) => {
- counts[service.type] = (counts[service.type] || 0) + 1;
- });
- host['services'].map((service: any) => {
- host['service_instances'].add(`${service.type}: ${counts[service.type]}`);
- });
- }
- return host;
- })
- )
+ })
)
.subscribe(
(hostList) => {
list(facts: string): Observable<object[]> {
return this.http.get<object[]>(this.baseURL, {
- headers: { Accept: 'application/vnd.ceph.api.v1.1+json' },
+ headers: { Accept: this.getVersionHeaderValue(1, 2) },
params: { facts: facts }
});
}
responses:
'200':
content:
- application/vnd.ceph.api.v1.1+json:
+ application/vnd.ceph.api.v1.2+json:
schema:
properties:
addr:
items:
type: string
type: array
+ service_instances:
+ description: Service instances related to the host
+ items:
+ properties:
+ count:
+ description: Number of instances of the service
+ type: integer
+ type:
+ description: type of service
+ type: string
+ required:
+ - type
+ - count
+ type: object
+ type: array
service_type:
description: ''
type: string
required:
- hostname
- services
+ - service_instances
- ceph_version
- addr
- labels
from cherrypy._cptools import HandlerWrapperTool
from cherrypy.test import helper
from mgr_module import HandleCommandResult
-from orchestrator import HostSpec, InventoryHost
+from orchestrator import DaemonDescription, HostSpec, InventoryHost
from pyfakefs import fake_filesystem
from .. import mgr
@contextlib.contextmanager
def patch_orch(available: bool, missing_features: Optional[List[str]] = None,
hosts: Optional[List[HostSpec]] = None,
- inventory: Optional[List[dict]] = None):
+ inventory: Optional[List[dict]] = None,
+ daemons: Optional[List[DaemonDescription]] = None):
with mock.patch('dashboard.controllers.orchestrator.OrchClient.instance') as instance:
fake_client = mock.Mock()
fake_client.available.return_value = available
fake_client.get_missing_features.return_value = missing_features
+ if not daemons:
+ daemons = [
+ DaemonDescription(
+ daemon_type='mon',
+ daemon_id='a',
+ hostname='node0'
+ )
+ ]
+ fake_client.services.list_daemons.return_value = daemons
if hosts is not None:
fake_client.hosts.list.return_value = hosts
import unittest
from unittest import mock
-from orchestrator import HostSpec
+from orchestrator import DaemonDescription, HostSpec
from .. import mgr
from ..controllers._version import APIVersion
self._get('{}?facts=true'.format(self.URL_HOST), version=APIVersion(1, 1))
self.assertStatus(200)
self.assertHeader('Content-Type',
- 'application/vnd.ceph.api.v1.1+json')
+ APIVersion(1, 2).to_mime_type())
self.assertJsonBody(hosts_with_facts)
# test with ?facts=false
self._get('{}?facts=false'.format(self.URL_HOST), version=APIVersion(1, 1))
self.assertStatus(200)
self.assertHeader('Content-Type',
- 'application/vnd.ceph.api.v1.1+json')
+ APIVersion(1, 2).to_mime_type())
self.assertJsonBody(hosts_without_facts)
# test with orchestrator available but orch backend!=cephadm
self._get('{}?facts=false'.format(self.URL_HOST), version=APIVersion(1, 1))
self.assertStatus(200)
self.assertHeader('Content-Type',
- 'application/vnd.ceph.api.v1.1+json')
+ APIVersion(1, 2).to_mime_type())
self.assertJsonBody(hosts_without_facts)
def test_get_1(self):
self.assertStatus(404)
def test_get_2(self):
- mgr.list_servers.return_value = [{'hostname': 'node1'}]
+ mgr.list_servers.return_value = [{
+ 'hostname': 'node1',
+ 'services': []
+ }]
with patch_orch(False):
self._get('{}/node1'.format(self.URL_HOST))
self.assertIn('status', self.json_body())
self.assertIn('addr', self.json_body())
+ def test_populate_service_instances(self):
+ mgr.list_servers.return_value = []
+
+ node1_daemons = [
+ DaemonDescription(
+ hostname='node1',
+ daemon_type='mon',
+ daemon_id='a'
+ ),
+ DaemonDescription(
+ hostname='node1',
+ daemon_type='mon',
+ daemon_id='b'
+ )
+ ]
+
+ node2_daemons = [
+ DaemonDescription(
+ hostname='node2',
+ daemon_type='mgr',
+ daemon_id='x'
+ ),
+ DaemonDescription(
+ hostname='node2',
+ daemon_type='mon',
+ daemon_id='c'
+ )
+ ]
+
+ node1_instances = [{
+ 'type': 'mon',
+ 'count': 2
+ }]
+
+ node2_instances = [{
+ 'type': 'mgr',
+ 'count': 1
+ }, {
+ 'type': 'mon',
+ 'count': 1
+ }]
+
+ # test with orchestrator available
+ with patch_orch(True,
+ hosts=[HostSpec('node1'), HostSpec('node2')]) as fake_client:
+ fake_client.services.list_daemons.return_value = node1_daemons
+ self._get('{}/node1'.format(self.URL_HOST))
+ self.assertStatus(200)
+ self.assertIn('service_instances', self.json_body())
+ self.assertEqual(self.json_body()['service_instances'], node1_instances)
+
+ fake_client.services.list_daemons.return_value = node2_daemons
+ self._get('{}/node2'.format(self.URL_HOST))
+ self.assertStatus(200)
+ self.assertIn('service_instances', self.json_body())
+ self.assertEqual(self.json_body()['service_instances'], node2_instances)
+
+ # test with no orchestrator available
+ with patch_orch(False):
+ mgr.list_servers.return_value = [{
+ 'hostname': 'node1',
+ 'services': [{
+ 'type': 'mon',
+ 'id': 'a'
+ }, {
+ 'type': 'mgr',
+ 'id': 'b'
+ }]
+ }]
+ self._get('{}/node1'.format(self.URL_HOST))
+ self.assertStatus(200)
+ self.assertIn('service_instances', self.json_body())
+ self.assertEqual(self.json_body()['service_instances'],
+ [{
+ 'type': 'mon',
+ 'count': 1
+ }, {
+ 'type': 'mgr',
+ 'count': 1
+ }])
+
@mock.patch('dashboard.controllers.host.add_host')
def test_add_host(self, mock_add_host):
with patch_orch(True):
class TestHosts(unittest.TestCase):
def test_get_hosts(self):
mgr.list_servers.return_value = [{
- 'hostname': 'node1'
+ 'hostname': 'node1',
+ 'services': []
}, {
- 'hostname': 'localhost'
+ 'hostname': 'localhost',
+ 'services': []
}]
orch_hosts = [
HostSpec('node1', labels=['foo', 'bar']),