set(Boost_USE_MULTITHREADED ON)
# dashboard angular2 frontend
-option(WITH_MGR_DASHBOARD_V2_FRONTEND "Build the mgr/dashboard_v2 frontend using `npm`" ON)
+option(WITH_MGR_DASHBOARD_FRONTEND "Build the mgr/dashboard frontend using `npm`" ON)
include_directories(SYSTEM ${PROJECT_BINARY_DIR}/include)
-DWITH_EMBEDDED=OFF \
-DWITH_MANPAGE=ON \
-DWITH_PYTHON3=ON \
- -DWITH_MGR_DASHBOARD_V2_FRONTEND=OFF \
+ -DWITH_MGR_DASHBOARD_FRONTEND=OFF \
%if %{with python2}
-DWITH_PYTHON2=ON \
%else
export DEB_HOST_ARCH ?= $(shell dpkg-architecture -qDEB_HOST_ARCH)
-extraopts += -DUSE_CRYPTOPP=OFF -DWITH_OCF=ON -DWITH_LTTNG=ON -DWITH_PYTHON3=ON -DWITH_EMBEDDED=OFF -DWITH_MGR_DASHBOARD_V2_FRONTEND=OFF
+extraopts += -DUSE_CRYPTOPP=OFF -DWITH_OCF=ON -DWITH_LTTNG=ON -DWITH_PYTHON3=ON -DWITH_EMBEDDED=OFF -DWITH_MGR_DASHBOARD_FRONTEND=OFF
extraopts += -DWITH_CEPHFS_JAVA=ON
extraopts += -DWITH_SYSTEMD=ON -DCEPH_SYSTEMD_ENV_DIR=/etc/default
# assumes that ceph is exmpt from multiarch support, so we override the libdir.
build_dashboard_frontend() {
CURR_DIR=`pwd`
- cd src/pybind/mgr/dashboard_v2/frontend
+ cd src/pybind/mgr/dashboard/frontend
npm install
npm run build -- --prod
cd $CURR_DIR
- tar cf dashboard_frontend.tar $outfile/src/pybind/mgr/dashboard_v2/frontend/dist
+ tar cf dashboard_frontend.tar $outfile/src/pybind/mgr/dashboard/frontend/dist
}
# clean out old cruft...
--- /dev/null
+
+tasks:
+ - install:
+ - ceph:
+ # tests may leave mgrs broken, so don't try and call into them
+ # to invoke e.g. pg dump during teardown.
+ wait-for-scrub: false
+ log-whitelist:
+ - overall HEALTH_
+ - \(MGR_DOWN\)
+ - \(PG_
+ - replacing it with standby
+ - No standby daemons available
+ - \(FS_DEGRADED\)
+ - \(MDS_FAILED\)
+ - \(MDS_DEGRADED\)
+ - \(FS_WITH_FAILED_MDS\)
+ - \(MDS_DAMAGE\)
+ - rgw: [client.0]
+ - cephfs_test_runner:
+ modules:
+ - tasks.mgr.test_dashboard
+ - tasks.mgr.dashboard.test_auth
+ - tasks.mgr.dashboard.test_cephfs
+ - tasks.mgr.dashboard.test_cluster_configuration
+ - tasks.mgr.dashboard.test_dashboard
+ - tasks.mgr.dashboard.test_host
+ - tasks.mgr.dashboard.test_monitor
+ - tasks.mgr.dashboard.test_osd
+ - tasks.mgr.dashboard.test_perf_counters
+ - tasks.mgr.dashboard.test_summary
+ - tasks.mgr.dashboard.test_rgw
+ - tasks.mgr.dashboard.test_rbd
+ - tasks.mgr.dashboard.test_pool
+++ /dev/null
-
-tasks:
- - install:
- - ceph:
- # tests may leave mgrs broken, so don't try and call into them
- # to invoke e.g. pg dump during teardown.
- wait-for-scrub: false
- log-whitelist:
- - overall HEALTH_
- - \(MGR_DOWN\)
- - \(PG_
- - replacing it with standby
- - No standby daemons available
- - \(FS_DEGRADED\)
- - \(MDS_FAILED\)
- - \(MDS_DEGRADED\)
- - \(FS_WITH_FAILED_MDS\)
- - \(MDS_DAMAGE\)
- - rgw: [client.0]
- - cephfs_test_runner:
- modules:
- - tasks.mgr.test_dashboard_v2
- - tasks.mgr.dashboard_v2.test_auth
- - tasks.mgr.dashboard_v2.test_cephfs
- - tasks.mgr.dashboard_v2.test_cluster_configuration
- - tasks.mgr.dashboard_v2.test_dashboard
- - tasks.mgr.dashboard_v2.test_host
- - tasks.mgr.dashboard_v2.test_monitor
- - tasks.mgr.dashboard_v2.test_osd
- - tasks.mgr.dashboard_v2.test_perf_counters
- - tasks.mgr.dashboard_v2.test_summary
- - tasks.mgr.dashboard_v2.test_rgw
- - tasks.mgr.dashboard_v2.test_rbd
- - tasks.mgr.dashboard_v2.test_pool
--- /dev/null
+# -*- coding: utf-8 -*-
+# pylint: disable=W0212
+from __future__ import absolute_import
+
+import json
+import logging
+import os
+import subprocess
+import sys
+
+import requests
+from ..mgr_test_case import MgrTestCase
+
+
+log = logging.getLogger(__name__)
+
+
+def authenticate(func):
+ def decorate(self, *args, **kwargs):
+ self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin', 'admin'])
+ self._post('/api/auth', {'username': 'admin', 'password': 'admin'})
+ self.assertStatus(201)
+ return func(self, *args, **kwargs)
+ return decorate
+
+
+class DashboardTestCase(MgrTestCase):
+ MGRS_REQUIRED = 2
+ MDSS_REQUIRED = 1
+ REQUIRE_FILESYSTEM = True
+ CLIENTS_REQUIRED = 1
+ CEPHFS = False
+
+ @classmethod
+ def setUpClass(cls):
+ super(DashboardTestCase, cls).setUpClass()
+ cls._assign_ports("dashboard", "server_port")
+ cls._load_module("dashboard")
+ cls.base_uri = cls._get_uri("dashboard").rstrip('/')
+
+ if cls.CEPHFS:
+ cls.mds_cluster.clear_firewall()
+
+ # To avoid any issues with e.g. unlink bugs, we destroy and recreate
+ # the filesystem rather than just doing a rm -rf of files
+ cls.mds_cluster.mds_stop()
+ cls.mds_cluster.mds_fail()
+ cls.mds_cluster.delete_all_filesystems()
+ cls.fs = None # is now invalid!
+
+ cls.fs = cls.mds_cluster.newfs(create=True)
+ cls.fs.mds_restart()
+
+ # In case some test messed with auth caps, reset them
+ # pylint: disable=not-an-iterable
+ client_mount_ids = [m.client_id for m in cls.mounts]
+ for client_id in client_mount_ids:
+ cls.mds_cluster.mon_manager.raw_cluster_cmd_result(
+ 'auth', 'caps', "client.{0}".format(client_id),
+ 'mds', 'allow',
+ 'mon', 'allow r',
+ 'osd', 'allow rw pool={0}'.format(cls.fs.get_data_pool_name()))
+
+ # wait for mds restart to complete...
+ cls.fs.wait_for_daemons()
+
+ @classmethod
+ def tearDownClass(cls):
+ super(DashboardTestCase, cls).tearDownClass()
+
+ def __init__(self, *args, **kwargs):
+ super(DashboardTestCase, self).__init__(*args, **kwargs)
+ self._session = requests.Session()
+ self._resp = None
+
+ def _request(self, url, method, data=None):
+ url = "{}{}".format(self.base_uri, url)
+ log.info("request %s to %s", method, url)
+ if method == 'GET':
+ self._resp = self._session.get(url)
+ return self._resp.json()
+ elif method == 'POST':
+ self._resp = self._session.post(url, json=data)
+ elif method == 'DELETE':
+ self._resp = self._session.delete(url, json=data)
+ elif method == 'PUT':
+ self._resp = self._session.put(url, json=data)
+ return None
+
+ def _get(self, url):
+ return self._request(url, 'GET')
+
+ def _post(self, url, data=None):
+ self._request(url, 'POST', data)
+
+ def _delete(self, url, data=None):
+ self._request(url, 'DELETE', data)
+
+ def _put(self, url, data=None):
+ self._request(url, 'PUT', data)
+
+ def cookies(self):
+ return self._resp.cookies
+
+ def jsonBody(self):
+ return self._resp.json()
+
+ def reset_session(self):
+ self._session = requests.Session()
+
+ def assertJsonBody(self, data):
+ body = self._resp.json()
+ self.assertEqual(body, data)
+
+ def assertBody(self, body):
+ self.assertEqual(self._resp.text, body)
+
+ def assertStatus(self, status):
+ self.assertEqual(self._resp.status_code, status)
+
+ @classmethod
+ def _ceph_cmd(cls, cmd):
+ res = cls.mgr_cluster.mon_manager.raw_cluster_cmd(*cmd)
+ log.info("command result: %s", res)
+ return res
+
+ def set_config_key(self, key, value):
+ self._ceph_cmd(['config-key', 'set', key, value])
+
+ def get_config_key(self, key):
+ return self._ceph_cmd(['config-key', 'get', key])
+
+ @classmethod
+ def _rbd_cmd(cls, cmd):
+ args = [
+ 'rbd'
+ ]
+ args.extend(cmd)
+ cls.mgr_cluster.admin_remote.run(args=args)
+
+ @classmethod
+ def mons(cls):
+ out = cls.ceph_cluster.mon_manager.raw_cluster_cmd('mon_status')
+ j = json.loads(out)
+ return [mon['name'] for mon in j['monmap']['mons']]
--- /dev/null
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+
+import time
+
+from .helper import DashboardTestCase
+
+
+class AuthTest(DashboardTestCase):
+ def setUp(self):
+ self.reset_session()
+ self._ceph_cmd(['dashboard', 'set-session-expire', '2'])
+ self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin', 'admin'])
+
+ def test_a_set_login_credentials(self):
+ self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin2', 'admin2'])
+ self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'})
+ self.assertStatus(201)
+ self.assertJsonBody({"username": "admin2"})
+
+ def test_login_valid(self):
+ self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
+ self.assertStatus(201)
+ self.assertJsonBody({"username": "admin"})
+
+ def test_login_stay_signed_in(self):
+ self._post("/api/auth", {
+ 'username': 'admin',
+ 'password': 'admin',
+ 'stay_signed_in': True})
+ self.assertStatus(201)
+ self.assertIn('session_id', self.cookies())
+ for cookie in self.cookies():
+ if cookie.name == 'session_id':
+ self.assertIsNotNone(cookie.expires)
+
+ def test_login_not_stay_signed_in(self):
+ self._post("/api/auth", {
+ 'username': 'admin',
+ 'password': 'admin',
+ 'stay_signed_in': False})
+ self.assertStatus(201)
+ self.assertIn('session_id', self.cookies())
+ for cookie in self.cookies():
+ if cookie.name == 'session_id':
+ self.assertIsNone(cookie.expires)
+
+ def test_login_invalid(self):
+ self._post("/api/auth", {'username': 'admin', 'password': 'inval'})
+ self.assertStatus(403)
+ self.assertJsonBody({"detail": "Invalid credentials"})
+
+ def test_logout(self):
+ self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
+ self._delete("/api/auth")
+ self.assertStatus(204)
+ self.assertBody('')
+ self._get("/api/host")
+ self.assertStatus(401)
+
+ def test_session_expire(self):
+ self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
+ self.assertStatus(201)
+ self._get("/api/host")
+ self.assertStatus(200)
+ time.sleep(3)
+ self._get("/api/host")
+ self.assertStatus(401)
+
+ def test_unauthorized(self):
+ self._get("/api/host")
+ self.assertStatus(401)
+ self._get("/api")
+ self.assertStatus(401)
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class CephfsTest(DashboardTestCase):
+ CEPHFS = True
+
+ @authenticate
+ def test_cephfs_clients(self):
+ fs_id = self.fs.get_namespace_id()
+ data = self._get("/api/cephfs/clients/{}".format(fs_id))
+ self.assertStatus(200)
+
+ self.assertIn('status', data)
+ self.assertIn('data', data)
+
+ @authenticate
+ def test_cephfs_data(self):
+ fs_id = self.fs.get_namespace_id()
+ data = self._get("/api/cephfs/data/{}/".format(fs_id))
+ self.assertStatus(200)
+
+ self.assertIn('cephfs', data)
+ self.assertIn('standbys', data)
+ self.assertIn('versions', data)
+ self.assertIsNotNone(data['cephfs'])
+ self.assertIsNotNone(data['standbys'])
+ self.assertIsNotNone(data['versions'])
+
+ @authenticate
+ def test_cephfs_mds_counters(self):
+ fs_id = self.fs.get_namespace_id()
+ data = self._get("/api/cephfs/mds_counters/{}".format(fs_id))
+ self.assertStatus(200)
+
+ self.assertIsInstance(data, dict)
+ self.assertIsNotNone(data)
--- /dev/null
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class ClusterConfigurationTest(DashboardTestCase):
+ @authenticate
+ def test_list(self):
+ data = self._get('/api/cluster_conf')
+ self.assertStatus(200)
+ self.assertIsInstance(data, list)
+ self.assertGreater(len(data), 1000)
+ for conf in data:
+ self._validate_single(conf)
+
+ @authenticate
+ def test_get(self):
+ data = self._get('/api/cluster_conf/admin_socket')
+ self.assertStatus(200)
+ self._validate_single(data)
+ self.assertIn('enum_values', data)
+
+ data = self._get('/api/cluster_conf/fantasy_name')
+ self.assertStatus(404)
+
+ def _validate_single(self, data):
+ self.assertIn('name', data)
+ self.assertIn('daemon_default', data)
+ self.assertIn('long_desc', data)
+ self.assertIn('level', data)
+ self.assertIn('default', data)
+ self.assertIn('see_also', data)
+ self.assertIn('tags', data)
+ self.assertIn('min', data)
+ self.assertIn('max', data)
+ self.assertIn('services', data)
+ self.assertIn('type', data)
+ self.assertIn('desc', data)
+
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class DashboardTest(DashboardTestCase):
+ CEPHFS = True
+
+ @authenticate
+ def test_health(self):
+ data = self._get("/api/dashboard/health")
+ self.assertStatus(200)
+
+ self.assertIn('health', data)
+ self.assertIn('mon_status', data)
+ self.assertIn('fs_map', data)
+ self.assertIn('osd_map', data)
+ self.assertIn('clog', data)
+ self.assertIn('audit_log', data)
+ self.assertIn('pools', data)
+ self.assertIn('mgr_map', data)
+ self.assertIn('df', data)
+ self.assertIsNotNone(data['health'])
+ self.assertIsNotNone(data['mon_status'])
+ self.assertIsNotNone(data['fs_map'])
+ self.assertIsNotNone(data['osd_map'])
+ self.assertIsNotNone(data['clog'])
+ self.assertIsNotNone(data['audit_log'])
+ self.assertIsNotNone(data['pools'])
+
+ cluster_pools = self.ceph_cluster.mon_manager.list_pools()
+ self.assertEqual(len(cluster_pools), len(data['pools']))
+ for pool in data['pools']:
+ self.assertIn(pool['pool_name'], cluster_pools)
+
+ self.assertIsNotNone(data['mgr_map'])
+ self.assertIsNotNone(data['df'])
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class HostControllerTest(DashboardTestCase):
+
+ @authenticate
+ def test_host_list(self):
+ data = self._get('/api/host')
+ self.assertStatus(200)
+
+ for server in data:
+ self.assertIn('services', server)
+ self.assertIn('hostname', server)
+ self.assertIn('ceph_version', server)
+ self.assertIsNotNone(server['hostname'])
+ self.assertIsNotNone(server['ceph_version'])
+ self.assertGreaterEqual(len(server['services']), 1)
+ for service in server['services']:
+ self.assertIn('type', service)
+ self.assertIn('id', service)
+ self.assertIsNotNone(service['type'])
+ self.assertIsNotNone(service['id'])
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class MonitorTest(DashboardTestCase):
+ @authenticate
+ def test_monitor_default(self):
+ data = self._get("/api/monitor")
+ self.assertStatus(200)
+
+ self.assertIn('mon_status', data)
+ self.assertIn('in_quorum', data)
+ self.assertIn('out_quorum', data)
+ self.assertIsNotNone(data['mon_status'])
+ self.assertIsNotNone(data['in_quorum'])
+ self.assertIsNotNone(data['out_quorum'])
--- /dev/null
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class OsdTest(DashboardTestCase):
+
+ def assert_in_and_not_none(self, data, properties):
+ for prop in properties:
+ self.assertIn(prop, data)
+ self.assertIsNotNone(data[prop])
+
+ @authenticate
+ def test_list(self):
+ data = self._get('/api/osd')
+ self.assertStatus(200)
+
+ self.assertGreaterEqual(len(data), 1)
+ data = data[0]
+ self.assert_in_and_not_none(data, ['host', 'tree', 'state', 'stats', 'stats_history'])
+ self.assert_in_and_not_none(data['host'], ['name'])
+ self.assert_in_and_not_none(data['tree'], ['id'])
+ self.assert_in_and_not_none(data['stats'], ['numpg', 'stat_bytes_used', 'stat_bytes',
+ 'op_r', 'op_w'])
+ self.assert_in_and_not_none(data['stats_history'], ['op_out_bytes', 'op_in_bytes'])
+
+ @authenticate
+ def test_details(self):
+ data = self._get('/api/osd/0')
+ self.assertStatus(200)
+ self.assert_in_and_not_none(data, ['osd_metadata', 'histogram'])
+ self.assert_in_and_not_none(data['histogram'], ['osd'])
+ self.assert_in_and_not_none(data['histogram']['osd'], ['op_w_latency_in_bytes_histogram',
+ 'op_r_latency_out_bytes_histogram'])
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class PerfCountersControllerTest(DashboardTestCase):
+
+ @authenticate
+ def test_perf_counters_list(self):
+ data = self._get('/api/perf_counters')
+ self.assertStatus(200)
+
+ self.assertIsInstance(data, dict)
+ for mon in self.mons():
+ self.assertIn('mon.{}'.format(mon), data)
+
+ osds = self.ceph_cluster.mon_manager.get_osd_dump()
+ for osd in osds:
+ self.assertIn('osd.{}'.format(osd['osd']), data)
+
+ @authenticate
+ def test_perf_counters_mon_get(self):
+ mon = self.mons()[0]
+ data = self._get('/api/perf_counters/mon/{}'.format(mon))
+ self.assertStatus(200)
+
+ self.assertIsInstance(data, dict)
+ self.assertEqual('mon', data['service']['type'])
+ self.assertEqual(mon, data['service']['id'])
+ self.assertIsInstance(data['counters'], list)
+ self.assertGreater(len(data['counters']), 0)
+ counter = data['counters'][0]
+ self.assertIsInstance(counter, dict)
+ self.assertIn('description', counter)
+ self.assertIn('name', counter)
+ self.assertIn('unit', counter)
+ self.assertIn('value', counter)
+
+ @authenticate
+ def test_perf_counters_mgr_get(self):
+ mgr = self.mgr_cluster.mgr_ids[0]
+ data = self._get('/api/perf_counters/mgr/{}'.format(mgr))
+ self.assertStatus(200)
+
+ self.assertIsInstance(data, dict)
+ self.assertEqual('mgr', data['service']['type'])
+ self.assertEqual(mgr, data['service']['id'])
+ self.assertIsInstance(data['counters'], list)
+ self.assertGreater(len(data['counters']), 0)
+ counter = data['counters'][0]
+ self.assertIsInstance(counter, dict)
+ self.assertIn('description', counter)
+ self.assertIn('name', counter)
+ self.assertIn('unit', counter)
+ self.assertIn('value', counter)
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class DashboardTest(DashboardTestCase):
+ @authenticate
+ def test_pool_list(self):
+ data = self._get("/api/pool")
+ self.assertStatus(200)
+
+ cluster_pools = self.ceph_cluster.mon_manager.list_pools()
+ self.assertEqual(len(cluster_pools), len(data))
+ for pool in data:
+ self.assertIn('pool_name', pool)
+ self.assertIn('type', pool)
+ self.assertIn('flags', pool)
+ self.assertIn('flags_names', pool)
+ self.assertNotIn('stats', pool)
+ self.assertIn(pool['pool_name'], cluster_pools)
+
+ @authenticate
+ def test_pool_list_attrs(self):
+ data = self._get("/api/pool?attrs=type,flags")
+ self.assertStatus(200)
+
+ cluster_pools = self.ceph_cluster.mon_manager.list_pools()
+ self.assertEqual(len(cluster_pools), len(data))
+ for pool in data:
+ self.assertIn('pool_name', pool)
+ self.assertIn('type', pool)
+ self.assertIn('flags', pool)
+ self.assertNotIn('flags_names', pool)
+ self.assertNotIn('stats', pool)
+ self.assertIn(pool['pool_name'], cluster_pools)
+
+ @authenticate
+ def test_pool_list_stats(self):
+ data = self._get("/api/pool?stats=true")
+ self.assertStatus(200)
+
+ cluster_pools = self.ceph_cluster.mon_manager.list_pools()
+ self.assertEqual(len(cluster_pools), len(data))
+ for pool in data:
+ self.assertIn('pool_name', pool)
+ self.assertIn('type', pool)
+ self.assertIn('flags', pool)
+ self.assertIn('stats', pool)
+ self.assertIn('flags_names', pool)
+ self.assertIn(pool['pool_name'], cluster_pools)
+
+ @authenticate
+ def test_pool_get(self):
+ cluster_pools = self.ceph_cluster.mon_manager.list_pools()
+ pool = self._get("/api/pool/{}?stats=true&attrs=type,flags,stats"
+ .format(cluster_pools[0]))
+ self.assertEqual(pool['pool_name'], cluster_pools[0])
+ self.assertIn('type', pool)
+ self.assertIn('flags', pool)
+ self.assertIn('stats', pool)
+ self.assertNotIn('flags_names', pool)
--- /dev/null
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+
+import unittest
+
+from .helper import DashboardTestCase, authenticate
+
+
+class RbdTest(DashboardTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(RbdTest, cls).setUpClass()
+ cls._ceph_cmd(['osd', 'pool', 'create', 'rbd', '100', '100'])
+ cls._ceph_cmd(['osd', 'pool', 'application', 'enable', 'rbd', 'rbd'])
+ cls._rbd_cmd(['create', '--size=1G', 'img1'])
+ cls._rbd_cmd(['create', '--size=2G', 'img2'])
+
+ @classmethod
+ def tearDownClass(cls):
+ super(RbdTest, cls).tearDownClass()
+ cls._ceph_cmd(['osd', 'pool', 'delete', 'rbd', 'rbd', '--yes-i-really-really-mean-it'])
+
+ @authenticate
+ def test_list(self):
+ data = self._get('/api/rbd/rbd')
+ self.assertStatus(200)
+
+ img1 = data['value'][0]
+ self.assertEqual(img1['name'], 'img1')
+ self.assertEqual(img1['size'], 1073741824)
+ self.assertEqual(img1['num_objs'], 256)
+ self.assertEqual(img1['obj_size'], 4194304)
+ self.assertEqual(img1['features_name'],
+ 'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
+
+ img2 = data['value'][1]
+ self.assertEqual(img2['name'], 'img2')
+ self.assertEqual(img2['size'], 2147483648)
+ self.assertEqual(img2['num_objs'], 512)
+ self.assertEqual(img2['obj_size'], 4194304)
+ self.assertEqual(img2['features_name'],
+ 'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
+
+ @authenticate
+ def test_create(self):
+ rbd_name = 'test_rbd'
+ data = {'pool_name': 'rbd',
+ 'name': rbd_name,
+ 'size': 10240}
+ self._post('/api/rbd', data)
+ self.assertStatus(201)
+ self.assertJsonBody({"success": True})
+
+ # TODO: change to GET the specific RBD instead of the list as soon as it is available?
+ get_res = self._get('/api/rbd/rbd')
+ self.assertStatus(200)
+
+ for rbd in get_res['value']:
+ if rbd['name'] == rbd_name:
+ self.assertEqual(rbd['size'], 10240)
+ self.assertEqual(rbd['num_objs'], 1)
+ self.assertEqual(rbd['obj_size'], 4194304)
+ self.assertEqual(rbd['features_name'],
+ 'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
+ break
+
+ # TODO: Re-enable this test for bluestore cluster by figuring out how to skip none-bluestore
+ # ones automatically
+ @unittest.skip("requires bluestore cluster")
+ @authenticate
+ def test_create_rbd_in_data_pool(self):
+ self._ceph_cmd(['osd', 'pool', 'create', 'data_pool', '12', '12', 'erasure'])
+ self._ceph_cmd(['osd', 'pool', 'application', 'enable', 'data_pool', 'rbd'])
+ self._ceph_cmd(['osd', 'pool', 'set', 'data_pool', 'allow_ec_overwrites', 'true'])
+
+ rbd_name = 'test_rbd_in_data_pool'
+ data = {'pool_name': 'rbd',
+ 'name': rbd_name,
+ 'size': 10240,
+ 'data_pool': 'data_pool'}
+ self._post('/api/rbd', data)
+ self.assertStatus(201)
+ self.assertJsonBody({"success": True})
+
+ # TODO: possibly change to GET the specific RBD (see above)
+ get_res = self._get('/api/rbd/rbd')
+ self.assertStatus(200)
+
+ for rbd in get_res['value']:
+ if rbd['name'] == rbd_name:
+ self.assertEqual(rbd['size'], 10240)
+ self.assertEqual(rbd['num_objs'], 1)
+ self.assertEqual(rbd['obj_size'], 4194304)
+ self.assertEqual(rbd['features_name'], 'data-pool, deep-flatten, exclusive-lock, '
+ 'fast-diff, layering, object-map')
+ break
+
+ self._ceph_cmd(['osd', 'pool', 'delete', 'data_pool', 'data_pool',
+ '--yes-i-really-really-mean-it'])
+
+ @authenticate
+ def test_create_rbd_twice(self):
+ data = {'pool_name': 'rbd',
+ 'name': 'test_rbd_twice',
+ 'size': 10240}
+ self._post('/api/rbd', data)
+
+ self._post('/api/rbd', data)
+ self.assertStatus(400)
+ self.assertJsonBody({"success": False, "errno": 17,
+ "detail": "[errno 17] error creating image"})
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class RgwControllerTest(DashboardTestCase):
+
+ @authenticate
+ def test_rgw_daemon_list(self):
+ data = self._get('/api/rgw/daemon')
+ self.assertStatus(200)
+
+ self.assertEqual(len(data), 1)
+ data = data[0]
+ self.assertIn('id', data)
+ self.assertIn('version', data)
+ self.assertIn('server_hostname', data)
+
+ @authenticate
+ def test_rgw_daemon_get(self):
+ data = self._get('/api/rgw/daemon')
+ self.assertStatus(200)
+ data = self._get('/api/rgw/daemon/{}'.format(data[0]['id']))
+ self.assertStatus(200)
+
+ self.assertIn('rgw_metadata', data)
+ self.assertIn('rgw_id', data)
+ self.assertIn('rgw_status', data)
+ self.assertTrue(data['rgw_metadata'])
--- /dev/null
+from __future__ import absolute_import
+
+from .helper import DashboardTestCase, authenticate
+
+
+class SummaryTest(DashboardTestCase):
+ CEPHFS = True
+
+ @authenticate
+ def test_summary(self):
+ data = self._get("/api/summary")
+ self.assertStatus(200)
+
+ self.assertIn('filesystems', data)
+ self.assertIn('health_status', data)
+ self.assertIn('rbd_pools', data)
+ self.assertIn('mgr_id', data)
+ self.assertIn('have_mon_connection', data)
+ self.assertIn('rbd_mirroring', data)
+ self.assertIsNotNone(data['filesystems'])
+ self.assertIsNotNone(data['health_status'])
+ self.assertIsNotNone(data['rbd_pools'])
+ self.assertIsNotNone(data['mgr_id'])
+ self.assertIsNotNone(data['have_mon_connection'])
+ self.assertEqual(data['rbd_mirroring'], {'errors': 0, 'warnings': 0})
+++ /dev/null
-# -*- coding: utf-8 -*-
-# pylint: disable=W0212
-from __future__ import absolute_import
-
-import json
-import logging
-import os
-import subprocess
-import sys
-
-import requests
-from ..mgr_test_case import MgrTestCase
-
-
-log = logging.getLogger(__name__)
-
-
-def authenticate(func):
- def decorate(self, *args, **kwargs):
- self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin', 'admin'])
- self._post('/api/auth', {'username': 'admin', 'password': 'admin'})
- self.assertStatus(201)
- return func(self, *args, **kwargs)
- return decorate
-
-
-class DashboardTestCase(MgrTestCase):
- MGRS_REQUIRED = 2
- MDSS_REQUIRED = 1
- REQUIRE_FILESYSTEM = True
- CLIENTS_REQUIRED = 1
- CEPHFS = False
-
- @classmethod
- def setUpClass(cls):
- super(DashboardTestCase, cls).setUpClass()
- cls._assign_ports("dashboard_v2", "server_port")
- cls._load_module("dashboard_v2")
- cls.base_uri = cls._get_uri("dashboard_v2").rstrip('/')
-
- if cls.CEPHFS:
- cls.mds_cluster.clear_firewall()
-
- # To avoid any issues with e.g. unlink bugs, we destroy and recreate
- # the filesystem rather than just doing a rm -rf of files
- cls.mds_cluster.mds_stop()
- cls.mds_cluster.mds_fail()
- cls.mds_cluster.delete_all_filesystems()
- cls.fs = None # is now invalid!
-
- cls.fs = cls.mds_cluster.newfs(create=True)
- cls.fs.mds_restart()
-
- # In case some test messed with auth caps, reset them
- # pylint: disable=not-an-iterable
- client_mount_ids = [m.client_id for m in cls.mounts]
- for client_id in client_mount_ids:
- cls.mds_cluster.mon_manager.raw_cluster_cmd_result(
- 'auth', 'caps', "client.{0}".format(client_id),
- 'mds', 'allow',
- 'mon', 'allow r',
- 'osd', 'allow rw pool={0}'.format(cls.fs.get_data_pool_name()))
-
- # wait for mds restart to complete...
- cls.fs.wait_for_daemons()
-
- @classmethod
- def tearDownClass(cls):
- super(DashboardTestCase, cls).tearDownClass()
-
- def __init__(self, *args, **kwargs):
- super(DashboardTestCase, self).__init__(*args, **kwargs)
- self._session = requests.Session()
- self._resp = None
-
- def _request(self, url, method, data=None):
- url = "{}{}".format(self.base_uri, url)
- log.info("request %s to %s", method, url)
- if method == 'GET':
- self._resp = self._session.get(url)
- return self._resp.json()
- elif method == 'POST':
- self._resp = self._session.post(url, json=data)
- elif method == 'DELETE':
- self._resp = self._session.delete(url, json=data)
- elif method == 'PUT':
- self._resp = self._session.put(url, json=data)
- return None
-
- def _get(self, url):
- return self._request(url, 'GET')
-
- def _post(self, url, data=None):
- self._request(url, 'POST', data)
-
- def _delete(self, url, data=None):
- self._request(url, 'DELETE', data)
-
- def _put(self, url, data=None):
- self._request(url, 'PUT', data)
-
- def cookies(self):
- return self._resp.cookies
-
- def jsonBody(self):
- return self._resp.json()
-
- def reset_session(self):
- self._session = requests.Session()
-
- def assertJsonBody(self, data):
- body = self._resp.json()
- self.assertEqual(body, data)
-
- def assertBody(self, body):
- self.assertEqual(self._resp.text, body)
-
- def assertStatus(self, status):
- self.assertEqual(self._resp.status_code, status)
-
- @classmethod
- def _ceph_cmd(cls, cmd):
- res = cls.mgr_cluster.mon_manager.raw_cluster_cmd(*cmd)
- log.info("command result: %s", res)
- return res
-
- def set_config_key(self, key, value):
- self._ceph_cmd(['config-key', 'set', key, value])
-
- def get_config_key(self, key):
- return self._ceph_cmd(['config-key', 'get', key])
-
- @classmethod
- def _rbd_cmd(cls, cmd):
- args = [
- 'rbd'
- ]
- args.extend(cmd)
- cls.mgr_cluster.admin_remote.run(args=args)
-
- @classmethod
- def mons(cls):
- out = cls.ceph_cluster.mon_manager.raw_cluster_cmd('mon_status')
- j = json.loads(out)
- return [mon['name'] for mon in j['monmap']['mons']]
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-from __future__ import absolute_import
-
-import time
-
-from .helper import DashboardTestCase
-
-
-class AuthTest(DashboardTestCase):
- def setUp(self):
- self.reset_session()
- self._ceph_cmd(['dashboard', 'set-session-expire', '2'])
- self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin', 'admin'])
-
- def test_a_set_login_credentials(self):
- self._ceph_cmd(['dashboard', 'set-login-credentials', 'admin2', 'admin2'])
- self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'})
- self.assertStatus(201)
- self.assertJsonBody({"username": "admin2"})
-
- def test_login_valid(self):
- self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
- self.assertStatus(201)
- self.assertJsonBody({"username": "admin"})
-
- def test_login_stay_signed_in(self):
- self._post("/api/auth", {
- 'username': 'admin',
- 'password': 'admin',
- 'stay_signed_in': True})
- self.assertStatus(201)
- self.assertIn('session_id', self.cookies())
- for cookie in self.cookies():
- if cookie.name == 'session_id':
- self.assertIsNotNone(cookie.expires)
-
- def test_login_not_stay_signed_in(self):
- self._post("/api/auth", {
- 'username': 'admin',
- 'password': 'admin',
- 'stay_signed_in': False})
- self.assertStatus(201)
- self.assertIn('session_id', self.cookies())
- for cookie in self.cookies():
- if cookie.name == 'session_id':
- self.assertIsNone(cookie.expires)
-
- def test_login_invalid(self):
- self._post("/api/auth", {'username': 'admin', 'password': 'inval'})
- self.assertStatus(403)
- self.assertJsonBody({"detail": "Invalid credentials"})
-
- def test_logout(self):
- self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
- self._delete("/api/auth")
- self.assertStatus(204)
- self.assertBody('')
- self._get("/api/host")
- self.assertStatus(401)
-
- def test_session_expire(self):
- self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
- self.assertStatus(201)
- self._get("/api/host")
- self.assertStatus(200)
- time.sleep(3)
- self._get("/api/host")
- self.assertStatus(401)
-
- def test_unauthorized(self):
- self._get("/api/host")
- self.assertStatus(401)
- self._get("/api")
- self.assertStatus(401)
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class CephfsTest(DashboardTestCase):
- CEPHFS = True
-
- @authenticate
- def test_cephfs_clients(self):
- fs_id = self.fs.get_namespace_id()
- data = self._get("/api/cephfs/clients/{}".format(fs_id))
- self.assertStatus(200)
-
- self.assertIn('status', data)
- self.assertIn('data', data)
-
- @authenticate
- def test_cephfs_data(self):
- fs_id = self.fs.get_namespace_id()
- data = self._get("/api/cephfs/data/{}/".format(fs_id))
- self.assertStatus(200)
-
- self.assertIn('cephfs', data)
- self.assertIn('standbys', data)
- self.assertIn('versions', data)
- self.assertIsNotNone(data['cephfs'])
- self.assertIsNotNone(data['standbys'])
- self.assertIsNotNone(data['versions'])
-
- @authenticate
- def test_cephfs_mds_counters(self):
- fs_id = self.fs.get_namespace_id()
- data = self._get("/api/cephfs/mds_counters/{}".format(fs_id))
- self.assertStatus(200)
-
- self.assertIsInstance(data, dict)
- self.assertIsNotNone(data)
+++ /dev/null
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class ClusterConfigurationTest(DashboardTestCase):
- @authenticate
- def test_list(self):
- data = self._get('/api/cluster_conf')
- self.assertStatus(200)
- self.assertIsInstance(data, list)
- self.assertGreater(len(data), 1000)
- for conf in data:
- self._validate_single(conf)
-
- @authenticate
- def test_get(self):
- data = self._get('/api/cluster_conf/admin_socket')
- self.assertStatus(200)
- self._validate_single(data)
- self.assertIn('enum_values', data)
-
- data = self._get('/api/cluster_conf/fantasy_name')
- self.assertStatus(404)
-
- def _validate_single(self, data):
- self.assertIn('name', data)
- self.assertIn('daemon_default', data)
- self.assertIn('long_desc', data)
- self.assertIn('level', data)
- self.assertIn('default', data)
- self.assertIn('see_also', data)
- self.assertIn('tags', data)
- self.assertIn('min', data)
- self.assertIn('max', data)
- self.assertIn('services', data)
- self.assertIn('type', data)
- self.assertIn('desc', data)
-
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class DashboardTest(DashboardTestCase):
- CEPHFS = True
-
- @authenticate
- def test_health(self):
- data = self._get("/api/dashboard/health")
- self.assertStatus(200)
-
- self.assertIn('health', data)
- self.assertIn('mon_status', data)
- self.assertIn('fs_map', data)
- self.assertIn('osd_map', data)
- self.assertIn('clog', data)
- self.assertIn('audit_log', data)
- self.assertIn('pools', data)
- self.assertIn('mgr_map', data)
- self.assertIn('df', data)
- self.assertIsNotNone(data['health'])
- self.assertIsNotNone(data['mon_status'])
- self.assertIsNotNone(data['fs_map'])
- self.assertIsNotNone(data['osd_map'])
- self.assertIsNotNone(data['clog'])
- self.assertIsNotNone(data['audit_log'])
- self.assertIsNotNone(data['pools'])
-
- cluster_pools = self.ceph_cluster.mon_manager.list_pools()
- self.assertEqual(len(cluster_pools), len(data['pools']))
- for pool in data['pools']:
- self.assertIn(pool['pool_name'], cluster_pools)
-
- self.assertIsNotNone(data['mgr_map'])
- self.assertIsNotNone(data['df'])
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class HostControllerTest(DashboardTestCase):
-
- @authenticate
- def test_host_list(self):
- data = self._get('/api/host')
- self.assertStatus(200)
-
- for server in data:
- self.assertIn('services', server)
- self.assertIn('hostname', server)
- self.assertIn('ceph_version', server)
- self.assertIsNotNone(server['hostname'])
- self.assertIsNotNone(server['ceph_version'])
- self.assertGreaterEqual(len(server['services']), 1)
- for service in server['services']:
- self.assertIn('type', service)
- self.assertIn('id', service)
- self.assertIsNotNone(service['type'])
- self.assertIsNotNone(service['id'])
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class MonitorTest(DashboardTestCase):
- @authenticate
- def test_monitor_default(self):
- data = self._get("/api/monitor")
- self.assertStatus(200)
-
- self.assertIn('mon_status', data)
- self.assertIn('in_quorum', data)
- self.assertIn('out_quorum', data)
- self.assertIsNotNone(data['mon_status'])
- self.assertIsNotNone(data['in_quorum'])
- self.assertIsNotNone(data['out_quorum'])
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class OsdTest(DashboardTestCase):
-
- def assert_in_and_not_none(self, data, properties):
- for prop in properties:
- self.assertIn(prop, data)
- self.assertIsNotNone(data[prop])
-
- @authenticate
- def test_list(self):
- data = self._get('/api/osd')
- self.assertStatus(200)
-
- self.assertGreaterEqual(len(data), 1)
- data = data[0]
- self.assert_in_and_not_none(data, ['host', 'tree', 'state', 'stats', 'stats_history'])
- self.assert_in_and_not_none(data['host'], ['name'])
- self.assert_in_and_not_none(data['tree'], ['id'])
- self.assert_in_and_not_none(data['stats'], ['numpg', 'stat_bytes_used', 'stat_bytes',
- 'op_r', 'op_w'])
- self.assert_in_and_not_none(data['stats_history'], ['op_out_bytes', 'op_in_bytes'])
-
- @authenticate
- def test_details(self):
- data = self._get('/api/osd/0')
- self.assertStatus(200)
- self.assert_in_and_not_none(data, ['osd_metadata', 'histogram'])
- self.assert_in_and_not_none(data['histogram'], ['osd'])
- self.assert_in_and_not_none(data['histogram']['osd'], ['op_w_latency_in_bytes_histogram',
- 'op_r_latency_out_bytes_histogram'])
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class PerfCountersControllerTest(DashboardTestCase):
-
- @authenticate
- def test_perf_counters_list(self):
- data = self._get('/api/perf_counters')
- self.assertStatus(200)
-
- self.assertIsInstance(data, dict)
- for mon in self.mons():
- self.assertIn('mon.{}'.format(mon), data)
-
- osds = self.ceph_cluster.mon_manager.get_osd_dump()
- for osd in osds:
- self.assertIn('osd.{}'.format(osd['osd']), data)
-
- @authenticate
- def test_perf_counters_mon_get(self):
- mon = self.mons()[0]
- data = self._get('/api/perf_counters/mon/{}'.format(mon))
- self.assertStatus(200)
-
- self.assertIsInstance(data, dict)
- self.assertEqual('mon', data['service']['type'])
- self.assertEqual(mon, data['service']['id'])
- self.assertIsInstance(data['counters'], list)
- self.assertGreater(len(data['counters']), 0)
- counter = data['counters'][0]
- self.assertIsInstance(counter, dict)
- self.assertIn('description', counter)
- self.assertIn('name', counter)
- self.assertIn('unit', counter)
- self.assertIn('value', counter)
-
- @authenticate
- def test_perf_counters_mgr_get(self):
- mgr = self.mgr_cluster.mgr_ids[0]
- data = self._get('/api/perf_counters/mgr/{}'.format(mgr))
- self.assertStatus(200)
-
- self.assertIsInstance(data, dict)
- self.assertEqual('mgr', data['service']['type'])
- self.assertEqual(mgr, data['service']['id'])
- self.assertIsInstance(data['counters'], list)
- self.assertGreater(len(data['counters']), 0)
- counter = data['counters'][0]
- self.assertIsInstance(counter, dict)
- self.assertIn('description', counter)
- self.assertIn('name', counter)
- self.assertIn('unit', counter)
- self.assertIn('value', counter)
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class DashboardTest(DashboardTestCase):
- @authenticate
- def test_pool_list(self):
- data = self._get("/api/pool")
- self.assertStatus(200)
-
- cluster_pools = self.ceph_cluster.mon_manager.list_pools()
- self.assertEqual(len(cluster_pools), len(data))
- for pool in data:
- self.assertIn('pool_name', pool)
- self.assertIn('type', pool)
- self.assertIn('flags', pool)
- self.assertIn('flags_names', pool)
- self.assertNotIn('stats', pool)
- self.assertIn(pool['pool_name'], cluster_pools)
-
- @authenticate
- def test_pool_list_attrs(self):
- data = self._get("/api/pool?attrs=type,flags")
- self.assertStatus(200)
-
- cluster_pools = self.ceph_cluster.mon_manager.list_pools()
- self.assertEqual(len(cluster_pools), len(data))
- for pool in data:
- self.assertIn('pool_name', pool)
- self.assertIn('type', pool)
- self.assertIn('flags', pool)
- self.assertNotIn('flags_names', pool)
- self.assertNotIn('stats', pool)
- self.assertIn(pool['pool_name'], cluster_pools)
-
- @authenticate
- def test_pool_list_stats(self):
- data = self._get("/api/pool?stats=true")
- self.assertStatus(200)
-
- cluster_pools = self.ceph_cluster.mon_manager.list_pools()
- self.assertEqual(len(cluster_pools), len(data))
- for pool in data:
- self.assertIn('pool_name', pool)
- self.assertIn('type', pool)
- self.assertIn('flags', pool)
- self.assertIn('stats', pool)
- self.assertIn('flags_names', pool)
- self.assertIn(pool['pool_name'], cluster_pools)
-
- @authenticate
- def test_pool_get(self):
- cluster_pools = self.ceph_cluster.mon_manager.list_pools()
- pool = self._get("/api/pool/{}?stats=true&attrs=type,flags,stats"
- .format(cluster_pools[0]))
- self.assertEqual(pool['pool_name'], cluster_pools[0])
- self.assertIn('type', pool)
- self.assertIn('flags', pool)
- self.assertIn('stats', pool)
- self.assertNotIn('flags_names', pool)
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-from __future__ import absolute_import
-
-import unittest
-
-from .helper import DashboardTestCase, authenticate
-
-
-class RbdTest(DashboardTestCase):
-
- @classmethod
- def setUpClass(cls):
- super(RbdTest, cls).setUpClass()
- cls._ceph_cmd(['osd', 'pool', 'create', 'rbd', '100', '100'])
- cls._ceph_cmd(['osd', 'pool', 'application', 'enable', 'rbd', 'rbd'])
- cls._rbd_cmd(['create', '--size=1G', 'img1'])
- cls._rbd_cmd(['create', '--size=2G', 'img2'])
-
- @classmethod
- def tearDownClass(cls):
- super(RbdTest, cls).tearDownClass()
- cls._ceph_cmd(['osd', 'pool', 'delete', 'rbd', 'rbd', '--yes-i-really-really-mean-it'])
-
- @authenticate
- def test_list(self):
- data = self._get('/api/rbd/rbd')
- self.assertStatus(200)
-
- img1 = data['value'][0]
- self.assertEqual(img1['name'], 'img1')
- self.assertEqual(img1['size'], 1073741824)
- self.assertEqual(img1['num_objs'], 256)
- self.assertEqual(img1['obj_size'], 4194304)
- self.assertEqual(img1['features_name'],
- 'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
-
- img2 = data['value'][1]
- self.assertEqual(img2['name'], 'img2')
- self.assertEqual(img2['size'], 2147483648)
- self.assertEqual(img2['num_objs'], 512)
- self.assertEqual(img2['obj_size'], 4194304)
- self.assertEqual(img2['features_name'],
- 'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
-
- @authenticate
- def test_create(self):
- rbd_name = 'test_rbd'
- data = {'pool_name': 'rbd',
- 'name': rbd_name,
- 'size': 10240}
- self._post('/api/rbd', data)
- self.assertStatus(201)
- self.assertJsonBody({"success": True})
-
- # TODO: change to GET the specific RBD instead of the list as soon as it is available?
- get_res = self._get('/api/rbd/rbd')
- self.assertStatus(200)
-
- for rbd in get_res['value']:
- if rbd['name'] == rbd_name:
- self.assertEqual(rbd['size'], 10240)
- self.assertEqual(rbd['num_objs'], 1)
- self.assertEqual(rbd['obj_size'], 4194304)
- self.assertEqual(rbd['features_name'],
- 'deep-flatten, exclusive-lock, fast-diff, layering, object-map')
- break
-
- # TODO: Re-enable this test for bluestore cluster by figuring out how to skip none-bluestore
- # ones automatically
- @unittest.skip("requires bluestore cluster")
- @authenticate
- def test_create_rbd_in_data_pool(self):
- self._ceph_cmd(['osd', 'pool', 'create', 'data_pool', '12', '12', 'erasure'])
- self._ceph_cmd(['osd', 'pool', 'application', 'enable', 'data_pool', 'rbd'])
- self._ceph_cmd(['osd', 'pool', 'set', 'data_pool', 'allow_ec_overwrites', 'true'])
-
- rbd_name = 'test_rbd_in_data_pool'
- data = {'pool_name': 'rbd',
- 'name': rbd_name,
- 'size': 10240,
- 'data_pool': 'data_pool'}
- self._post('/api/rbd', data)
- self.assertStatus(201)
- self.assertJsonBody({"success": True})
-
- # TODO: possibly change to GET the specific RBD (see above)
- get_res = self._get('/api/rbd/rbd')
- self.assertStatus(200)
-
- for rbd in get_res['value']:
- if rbd['name'] == rbd_name:
- self.assertEqual(rbd['size'], 10240)
- self.assertEqual(rbd['num_objs'], 1)
- self.assertEqual(rbd['obj_size'], 4194304)
- self.assertEqual(rbd['features_name'], 'data-pool, deep-flatten, exclusive-lock, '
- 'fast-diff, layering, object-map')
- break
-
- self._ceph_cmd(['osd', 'pool', 'delete', 'data_pool', 'data_pool',
- '--yes-i-really-really-mean-it'])
-
- @authenticate
- def test_create_rbd_twice(self):
- data = {'pool_name': 'rbd',
- 'name': 'test_rbd_twice',
- 'size': 10240}
- self._post('/api/rbd', data)
-
- self._post('/api/rbd', data)
- self.assertStatus(400)
- self.assertJsonBody({"success": False, "errno": 17,
- "detail": "[errno 17] error creating image"})
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class RgwControllerTest(DashboardTestCase):
-
- @authenticate
- def test_rgw_daemon_list(self):
- data = self._get('/api/rgw/daemon')
- self.assertStatus(200)
-
- self.assertEqual(len(data), 1)
- data = data[0]
- self.assertIn('id', data)
- self.assertIn('version', data)
- self.assertIn('server_hostname', data)
-
- @authenticate
- def test_rgw_daemon_get(self):
- data = self._get('/api/rgw/daemon')
- self.assertStatus(200)
- data = self._get('/api/rgw/daemon/{}'.format(data[0]['id']))
- self.assertStatus(200)
-
- self.assertIn('rgw_metadata', data)
- self.assertIn('rgw_id', data)
- self.assertIn('rgw_status', data)
- self.assertTrue(data['rgw_metadata'])
+++ /dev/null
-from __future__ import absolute_import
-
-from .helper import DashboardTestCase, authenticate
-
-
-class SummaryTest(DashboardTestCase):
- CEPHFS = True
-
- @authenticate
- def test_summary(self):
- data = self._get("/api/summary")
- self.assertStatus(200)
-
- self.assertIn('filesystems', data)
- self.assertIn('health_status', data)
- self.assertIn('rbd_pools', data)
- self.assertIn('mgr_id', data)
- self.assertIn('have_mon_connection', data)
- self.assertIn('rbd_mirroring', data)
- self.assertIsNotNone(data['filesystems'])
- self.assertIsNotNone(data['health_status'])
- self.assertIsNotNone(data['rbd_pools'])
- self.assertIsNotNone(data['mgr_id'])
- self.assertIsNotNone(data['have_mon_connection'])
- self.assertEqual(data['rbd_mirroring'], {'errors': 0, 'warnings': 0})
--- /dev/null
+
+
+from mgr_test_case import MgrTestCase
+
+import logging
+import requests
+
+
+log = logging.getLogger(__name__)
+
+
+class TestDashboard(MgrTestCase):
+ MGRS_REQUIRED = 3
+
+ def test_standby(self):
+ self._assign_ports("dashboard", "server_port")
+ self._load_module("dashboard")
+
+ original_active = self.mgr_cluster.get_active_id()
+
+ original_uri = self._get_uri("dashboard")
+ log.info("Originally running at {0}".format(original_uri))
+
+ self.mgr_cluster.mgr_fail(original_active)
+
+ failed_over_uri = self._get_uri("dashboard")
+ log.info("After failover running at {0}".format(failed_over_uri))
+
+ self.assertNotEqual(original_uri, failed_over_uri)
+
+ # The original active daemon should have come back up as a standby
+ # and be doing redirects to the new active daemon
+ r = requests.get(original_uri, allow_redirects=False)
+ self.assertEqual(r.status_code, 303)
+ self.assertEqual(r.headers['Location'], failed_over_uri)
+
+ def test_urls(self):
+ self._assign_ports("dashboard", "server_port")
+ self._load_module("dashboard")
+
+ base_uri = self._get_uri("dashboard")
+
+ # This is a very simple smoke test to check that the dashboard can
+ # give us a 200 response to requests. We're not testing that
+ # the content is correct or even renders!
+
+ urls = [
+ "/",
+ ]
+
+ failures = []
+
+ for url in urls:
+ r = requests.get(base_uri + url, allow_redirects=False)
+ if r.status_code >= 300 and r.status_code < 400:
+ log.error("Unexpected redirect to: {0} (from {1})".format(
+ r.headers['Location'], base_uri))
+ if r.status_code != 200:
+ failures.append(url)
+
+ log.info("{0}: {1} ({2} bytes)".format(
+ url, r.status_code, len(r.content)
+ ))
+
+ self.assertListEqual(failures, [])
+++ /dev/null
-
-
-from mgr_test_case import MgrTestCase
-
-import logging
-import requests
-
-
-log = logging.getLogger(__name__)
-
-
-class TestDashboard(MgrTestCase):
- MGRS_REQUIRED = 3
-
- def test_standby(self):
- self._assign_ports("dashboard_v2", "server_port")
- self._load_module("dashboard_v2")
-
- original_active = self.mgr_cluster.get_active_id()
-
- original_uri = self._get_uri("dashboard_v2")
- log.info("Originally running at {0}".format(original_uri))
-
- self.mgr_cluster.mgr_fail(original_active)
-
- failed_over_uri = self._get_uri("dashboard_v2")
- log.info("After failover running at {0}".format(failed_over_uri))
-
- self.assertNotEqual(original_uri, failed_over_uri)
-
- # The original active daemon should have come back up as a standby
- # and be doing redirects to the new active daemon
- r = requests.get(original_uri, allow_redirects=False)
- self.assertEqual(r.status_code, 303)
- self.assertEqual(r.headers['Location'], failed_over_uri)
-
- def test_urls(self):
- self._assign_ports("dashboard_v2", "server_port")
- self._load_module("dashboard_v2")
-
- base_uri = self._get_uri("dashboard_v2")
-
- # This is a very simple smoke test to check that the dashboard can
- # give us a 200 response to requests. We're not testing that
- # the content is correct or even renders!
-
- urls = [
- "/",
- ]
-
- failures = []
-
- for url in urls:
- r = requests.get(base_uri + url, allow_redirects=False)
- if r.status_code >= 300 and r.status_code < 400:
- log.error("Unexpected redirect to: {0} (from {1})".format(
- r.headers['Location'], base_uri))
- if r.status_code != 200:
- failures.append(url)
-
- log.info("{0}: {1} ({2} bytes)".format(
- url, r.status_code, len(r.content)
- ))
-
- self.assertListEqual(failures, [])
-add_subdirectory(dashboard_v2)
+add_subdirectory(dashboard)
--- /dev/null
+[run]
+omit = tests/*
+ */python*/*
+ ceph_module_mock.py
+ __init__.py
+ */mgr_module.py
+
--- /dev/null
+# EditorConfig helps developers define and maintain consistent coding styles
+# between different editors and IDEs.: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+
+# Set default charset
+[*.{js,py}]
+charset = utf-8
+
+# 4 space indentation for Python files
+[*.py]
+indent_style = space
+indent_size = 4
+
+# Indentation override for all JS under frontend directory
+[frontend/**.js]
+indent_style = space
+indent_size = 2
+
+# Indentation override for all HTML under frontend directory
+[frontend/**.html]
+indent_style = space
+indent_size = 2
--- /dev/null
+.coverage*
+htmlcov
+.tox
+coverage.xml
+junit*xml
+__pycache__
+.cache
+ceph.conf
+wheelhouse*
+
+# IDE
+.vscode
+.idea
+*.egg
+
+# virtualenv
+venv
--- /dev/null
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=rados,rbd
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint.
+jobs=1
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Specify a configuration file.
+#rcfile=
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=print-statement,
+ parameter-unpacking,
+ unpacking-in-except,
+ old-raise-syntax,
+ backtick,
+ long-suffix,
+ old-ne-operator,
+ old-octal-literal,
+ import-star-module-level,
+ non-ascii-bytes-literal,
+ raw-checker-failed,
+ bad-inline-option,
+ locally-disabled,
+ locally-enabled,
+ file-ignored,
+ suppressed-message,
+ useless-suppression,
+ deprecated-pragma,
+ apply-builtin,
+ basestring-builtin,
+ buffer-builtin,
+ cmp-builtin,
+ coerce-builtin,
+ execfile-builtin,
+ file-builtin,
+ long-builtin,
+ raw_input-builtin,
+ reduce-builtin,
+ standarderror-builtin,
+ unicode-builtin,
+ xrange-builtin,
+ coerce-method,
+ delslice-method,
+ getslice-method,
+ setslice-method,
+ no-absolute-import,
+ old-division,
+ dict-iter-method,
+ dict-view-method,
+ next-method-called,
+ metaclass-assignment,
+ indexing-exception,
+ raising-string,
+ reload-builtin,
+ oct-method,
+ hex-method,
+ nonzero-method,
+ cmp-method,
+ input-builtin,
+ round-builtin,
+ intern-builtin,
+ unichr-builtin,
+ map-builtin-not-iterating,
+ zip-builtin-not-iterating,
+ range-builtin-not-iterating,
+ filter-builtin-not-iterating,
+ using-cmp-argument,
+ eq-without-hash,
+ div-method,
+ idiv-method,
+ rdiv-method,
+ exception-message-attribute,
+ invalid-str-codec,
+ sys-max-int,
+ bad-python3-import,
+ deprecated-string-function,
+ deprecated-str-translate-call,
+ deprecated-itertools-function,
+ deprecated-types-field,
+ next-method-defined,
+ dict-items-not-iterating,
+ dict-keys-not-iterating,
+ dict-values-not-iterating,
+ missing-docstring,
+ invalid-name,
+ no-self-use,
+ too-few-public-methods,
+ no-member,
+ fixme
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[REPORTS]
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio).You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=optparse.Values,sys.exit
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins
+
+
+[BASIC]
+
+# Naming style matching correct argument names
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style
+#argument-rgx=
+
+# Naming style matching correct attribute names
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,
+ bar,
+ baz,
+ toto,
+ tutu,
+ tata
+
+# Naming style matching correct class attribute names
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style
+#class-attribute-rgx=
+
+# Naming style matching correct class names
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-style
+#class-rgx=
+
+# Naming style matching correct constant names
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style
+#inlinevar-rgx=
+
+# Naming style matching correct method names
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style
+#method-rgx=
+
+# Naming style matching correct module names
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style
+#variable-rgx=
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,
+ dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,
+ TERMIOS,
+ Bastion,
+ rexec
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in a if statement
+max-bool-expr=5
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
--- /dev/null
+set(MGR_DASHBOARD_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/mgr-dashboard-virtualenv)
+
+add_custom_target(mgr-dashboard-test-venv
+ COMMAND
+ ${CMAKE_SOURCE_DIR}/src/tools/setup-virtualenv.sh ${MGR_DASHBOARD_VIRTUALENV} &&
+ ${MGR_DASHBOARD_VIRTUALENV}/bin/pip install --no-index --use-wheel --find-links=file:${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/wheelhouse -r requirements.txt
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard
+ COMMENT "dashboard tests virtualenv is being created")
+add_dependencies(tests mgr-dashboard-test-venv)
+
+if(WITH_MGR_DASHBOARD_FRONTEND AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
+ find_program(NPM_BIN
+ NAMES npm
+ HINTS $ENV{NPM_ROOT}/bin)
+ if(NOT NPM_BIN)
+ message(FATAL_ERROR "WITH_MGR_DASHBOARD_FRONTEND set, but npm not found")
+ endif()
+
+add_custom_command(
+ OUTPUT "${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend/node_modules"
+ COMMAND ${NPM_BIN} install
+ DEPENDS frontend/package.json
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend
+ COMMENT "dashboard frontend dependencies are being installed"
+)
+
+add_custom_target(mgr-dashboard-frontend-deps
+ DEPENDS frontend/node_modules
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend
+)
+
+
+# Glob some frontend files. With CMake 3.6, this can be simplified
+# to *.ts *.html. Just add:
+# list(FILTER frontend_src INCLUDE REGEX "frontend/src")
+file(
+ GLOB_RECURSE frontend_src
+ frontend/src/*.ts
+ frontend/src/*.html
+ frontend/src/*/*.ts
+ frontend/src/*/*.html
+ frontend/src/*/*/*.ts
+ frontend/src/*/*/*.html
+ frontend/src/*/*/*/*.ts
+ frontend/src/*/*/*/*.html
+ frontend/src/*/*/*/*/*.ts
+ frontend/src/*/*/*/*/*.html
+ frontend/src/*/*/*/*/*/*.ts
+ frontend/src/*/*/*/*/*/*.html)
+
+if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
+ set(npm_command ${NPM_BIN} run build -- --prod)
+else()
+ set(npm_command ${NPM_BIN} run build)
+endif()
+
+add_custom_command(
+ OUTPUT "${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend/dist"
+ COMMAND ${npm_command}
+ DEPENDS ${frontend_src} frontend/node_modules
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend
+ COMMENT "dashboard frontend is being created"
+)
+add_custom_target(mgr-dashboard-frontend-build
+ DEPENDS frontend/dist mgr-dashboard-frontend-deps
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend
+)
+add_dependencies(ceph-mgr mgr-dashboard-frontend-build)
+endif(WITH_MGR_DASHBOARD_FRONTEND AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
--- /dev/null
+Dashboard Developer Documentation
+====================================
+
+Frontend Development
+--------------------
+
+Before you can start the dashboard from within a development environment, you
+will need to generate the frontend code and either use a compiled and running
+Ceph cluster (e.g. started by ``vstart.sh``) or the standalone development web
+server.
+
+The build process is based on `Node.js <https://nodejs.org/>`_ and requires the
+`Node Package Manager <https://www.npmjs.com/>`_ ``npm`` to be installed.
+
+Prerequisites
+~~~~~~~~~~~~~
+
+Run ``npm install`` in directory ``src/pybind/mgr/dashboard/frontend`` to
+install the required packages locally.
+
+.. note::
+
+ If you do not have the `Angular CLI <https://github.com/angular/angular-cli>`_
+ installed globally, then you need to execute ``ng`` commands with an
+ additional ``npm run`` before it.
+
+Setting up a Development Server
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create the ``proxy.conf.json`` file based on ``proxy.conf.json.sample``.
+
+Run ``npm start -- --proxy-config proxy.conf.json`` for a dev server.
+Navigate to ``http://localhost:4200/``. The app will automatically
+reload if you change any of the source files.
+
+Code Scaffolding
+~~~~~~~~~~~~~~~~
+
+Run ``ng generate component component-name`` to generate a new
+component. You can also use
+``ng generate directive|pipe|service|class|guard|interface|enum|module``.
+
+Build the Project
+~~~~~~~~~~~~~~~~~
+
+Run ``npm run build`` to build the project. The build artifacts will be
+stored in the ``dist/`` directory. Use the ``-prod`` flag for a
+production build. Navigate to ``http://localhost:8080``.
+
+Running Unit Tests
+~~~~~~~~~~~~~~~~~~
+
+Run ``npm run test`` to execute the unit tests via `Karma
+<https://karma-runner.github.io>`_.
+
+Running End-to-End Tests
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Run ``npm run e2e`` to execute the end-to-end tests via
+`Protractor <http://www.protractortest.org/>`__.
+
+Further Help
+~~~~~~~~~~~~
+
+To get more help on the Angular CLI use ``ng help`` or go check out the
+`Angular CLI
+README <https://github.com/angular/angular-cli/blob/master/README.md>`__.
+
+Example of a Generator
+~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+ # Create module 'Core'
+ src/app> ng generate module core -m=app --routing
+
+ # Create module 'Auth' under module 'Core'
+ src/app/core> ng generate module auth -m=core --routing
+ or, alternatively:
+ src/app> ng generate module core/auth -m=core --routing
+
+ # Create component 'Login' under module 'Auth'
+ src/app/core/auth> ng generate component login -m=core/auth
+ or, alternatively:
+ src/app> ng generate component core/auth/login -m=core/auth
+
+Frontend Typescript Code Style Guide Recommendations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Group the imports based on its source and separate them with a blank
+line.
+
+The source groups can be either from Angular, external or internal.
+
+Example:
+
+.. code:: javascript
+
+ import { Component } from '@angular/core';
+ import { Router } from '@angular/router';
+
+ import { ToastsManager } from 'ng2-toastr';
+
+ import { Credentials } from '../../../shared/models/credentials.model';
+ import { HostService } from './services/host.service';
+
+
+Backend Development
+-------------------
+
+The Python backend code of this module requires a number of Python modules to be
+installed. They are listed in file ``requirements.txt``. Using `pip
+<https://pypi.python.org/pypi/pip>`_ you may install all required dependencies
+by issuing ``pip install -r requirements.txt`` in directory
+``src/pybind/mgr/dashboard``.
+
+If you're using the `ceph-dev-docker development environment
+<https://github.com/ricardoasmarques/ceph-dev-docker/>`_, simply run
+``./install_deps.sh`` from the toplevel directory to install them.
+
+Unit Testing and Linting
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+We included a ``tox`` configuration file that will run the unit tests under
+Python 2 or 3, as well as linting tools to guarantee the uniformity of code.
+
+You need to install ``tox`` and ``coverage`` before running it. To install the
+packages in your system, either install it via your operating system's package
+management tools, e.g. by running ``dnf install python-tox python-coverage`` on
+Fedora Linux.
+
+Alternatively, you can use Python's native package installation method::
+
+ $ pip install tox
+ $ pip install coverage
+
+The unit tests must run against a real Ceph cluster (no mocks are used). This
+has the advantage of catching bugs originated from changes in the internal Ceph
+code.
+
+Our ``tox.ini`` script will start a ``vstart`` Ceph cluster before running the
+python unit tests, and then it stops the cluster after the tests are run. Of
+course this implies that you have built/compiled Ceph previously.
+
+To run tox, run the following command in the root directory (where ``tox.ini``
+is located)::
+
+ $ PATH=../../../../build/bin:$PATH tox
+
+We also collect coverage information from the backend code. You can check the
+coverage information provided by the tox output, or by running the following
+command after tox has finished successfully::
+
+ $ coverage html
+
+This command will create a directory ``htmlcov`` with an HTML representation of
+the code coverage of the backend.
+
+You can also run a single step of the tox script (aka tox environment), for
+instance if you only want to run the linting tools, do::
+
+ $ PATH=../../../../build/bin:$PATH tox -e lint
+
+How to run a single unit test without using ``tox``?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When developing the code of a controller and respective test code, it's useful
+to be able to run that single test file without going through the whole ``tox``
+workflow.
+
+Since the tests must run against a real Ceph cluster, the first thing is to have
+a Ceph cluster running. For that we can leverage the tox environment that starts
+a Ceph cluster::
+
+ $ PATH=../../../../build/bin:$PATH tox -e ceph-cluster-start
+
+The command above uses ``vstart.sh`` script to start a Ceph cluster and
+automatically enables the ``dashboard`` module, and configures its cherrypy
+web server to listen in port ``9865``.
+
+After starting the Ceph cluster we can run our test file using ``py.test`` like
+this::
+
+ DASHBOARD_PORT=9865 UNITTEST=true py.test -s tests/test_mycontroller.py
+
+You can run tests multiple times without having to start and stop the Ceph
+cluster.
+
+After you finish your tests, you can stop the Ceph cluster using another tox
+environment::
+
+ $ tox -e ceph-cluster-stop
+
+How to add a new controller?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you want to add a new endpoint to the backend, you just need to add a
+class derived from ``BaseController`` decorated with ``ApiController`` in a
+Python file located under the ``controllers`` directory. The Dashboard module
+will automatically load your new controller upon start.
+
+For example create a file ``ping2.py`` under ``controllers`` directory with the
+following code::
+
+ import cherrypy
+ from ..tools import ApiController, BaseController
+
+ @ApiController('ping2')
+ class Ping2(BaseController):
+ @cherrypy.expose
+ def default(self, *args):
+ return "Hello"
+
+Every path given in the ``ApiController`` decorator will automatically be
+prefixed with ``api``. After reloading the Dashboard module you can access the
+above mentioned controller by pointing your browser to
+http://mgr_hostname:8080/api/ping2.
+
+It is also possible to have nested controllers. The ``RgwController`` uses
+this technique to make the daemons available through the URL
+http://mgr_hostname:8080/api/rgw/daemon::
+
+ @ApiController('rgw')
+ @AuthRequired()
+ class Rgw(RESTController):
+ pass
+
+
+ @ApiController('rgw/daemon')
+ @AuthRequired()
+ class RgwDaemon(RESTController):
+
+ def list(self):
+ pass
+
+
+Note that paths must be unique and that a path like ``rgw/daemon`` has to have
+a parent ``rgw``. Otherwise it won't work.
+
+How does the RESTController work?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We also provide a simple mechanism to create REST based controllers using the
+``RESTController`` class. Any class which inherits from ``RESTController`` will,
+by default, return JSON.
+
+The ``RESTController`` is basically an additional abstraction layer which eases
+and unifies the work with collections. A collection is just an array of objects
+with a specific type. ``RESTController`` enables some default mappings of
+request types and given parameters to specific method names. This may sound
+complicated at first, but it's fairly easy. Lets have look at the following
+example::
+
+ import cherrypy
+ from ..tools import ApiController, RESTController
+
+ @ApiController('ping2')
+ class Ping2(RESTController):
+ def list(self):
+ return {"msg": "Hello"}
+
+ def get(self, id):
+ return self.objects[id]
+
+In this case, the ``list`` method is automatically used for all requests to
+``api/ping2`` where no additional argument is given and where the request type
+is ``GET``. If the request is given an additional argument, the ID in our
+case, it won't map to ``list`` anymore but to ``get`` and return the element
+with the given ID (assuming that ``self.objects`` has been filled before). The
+same applies to other request types:
+
++--------------+------------+----------------+-------------+
+| Request type | Arguments | Method | Status Code |
++==============+============+================+=============+
+| GET | No | list | 200 |
++--------------+------------+----------------+-------------+
+| PUT | No | bulk_set | 200 |
++--------------+------------+----------------+-------------+
+| PATCH | No | bulk_set | 200 |
++--------------+------------+----------------+-------------+
+| POST | No | create | 201 |
++--------------+------------+----------------+-------------+
+| DELETE | No | bulk_delete | 204 |
++--------------+------------+----------------+-------------+
+| GET | Yes | get | 200 |
++--------------+------------+----------------+-------------+
+| PUT | Yes | set | 200 |
++--------------+------------+----------------+-------------+
+| PATCH | Yes | set | 200 |
++--------------+------------+----------------+-------------+
+| DELETE | Yes | delete | 204 |
++--------------+------------+----------------+-------------+
+
+How to restrict access to a controller?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you require that only authenticated users can access you controller, just
+add the ``AuthRequired`` decorator to your controller class.
+
+Example::
+
+ import cherrypy
+ from ..tools import ApiController, AuthRequired, RESTController
+
+
+ @ApiController('ping2')
+ @AuthRequired()
+ class Ping2(RESTController):
+ def list(self):
+ return {"msg": "Hello"}
+
+Now only authenticated users will be able to "ping" your controller.
+
+
+How to access the manager module instance from a controller?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We provide the manager module instance as a global variable that can be
+imported in any module. We also provide a logger instance in the same way.
+
+Example::
+
+ import cherrypy
+ from .. import logger, mgr
+ from ..tools import ApiController, RESTController
+
+
+ @ApiController('servers')
+ class Servers(RESTController):
+ def list(self):
+ logger.debug('Listing available servers')
+ return {'servers': mgr.list_servers()}
+
+
+How to write a unit test for a controller?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+We provide a test helper class called ``ControllerTestCase`` to easily create
+unit tests for your controller.
+
+If we want to write a unit test for the above ``Ping2`` controller, create a
+``test_ping2.py`` file under the ``tests`` directory with the following code::
+
+ from .helper import ControllerTestCase
+ from .controllers.ping2 import Ping2
+
+
+ class Ping2Test(ControllerTestCase):
+ @classmethod
+ def setup_test(cls):
+ Ping2._cp_config['tools.authentica.on'] = False
+
+ def test_ping2(self):
+ self._get("/api/ping2")
+ self.assertStatus(200)
+ self.assertJsonBody({'msg': 'Hello'})
+
+The ``ControllerTestCase`` class will call the dashboard module code that loads
+the controllers and initializes the CherryPy webserver. Then it will call the
+``setup_test()`` class method to execute additional instructions that each test
+case needs to add to the test.
+In the example above we use the ``setup_test()`` method to disable the
+authentication handler for the ``Ping2`` controller.
+
+
+How to listen for manager notifications in a controller?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The manager notifies the modules of several types of cluster events, such
+as cluster logging event, etc...
+
+Each module has a "global" handler function called ``notify`` that the manager
+calls to notify the module. But this handler function must not block or spend
+too much time processing the event notification.
+For this reason we provide a notification queue that controllers can register
+themselves with to receive cluster notifications.
+
+The example below represents a controller that implements a very simple live
+log viewer page::
+
+ from __future__ import absolute_import
+
+ import collections
+
+ import cherrypy
+
+ from ..tools import ApiController, BaseController, NotificationQueue
+
+
+ @ApiController('livelog')
+ class LiveLog(BaseController):
+ log_buffer = collections.deque(maxlen=1000)
+
+ def __init__(self):
+ super(LiveLog, self).__init__()
+ NotificationQueue.register(self.log, 'clog')
+
+ def log(self, log_struct):
+ self.log_buffer.appendleft(log_struct)
+
+ @cherrypy.expose
+ def default(self):
+ ret = '<html><meta http-equiv="refresh" content="2" /><body>'
+ for l in self.log_buffer:
+ ret += "{}<br>".format(l)
+ ret += "</body></html>"
+ return ret
+
+As you can see above, the ``NotificationQueue`` class provides a register
+method that receives the function as its first argument, and receives the
+"notification type" as the second argument.
+You can omit the second argument of the ``register`` method, and in that case
+you are registering to listen all notifications of any type.
+
+Here is an list of notification types (these might change in the future) that
+can be used:
+
+* ``clog``: cluster log notifications
+* ``command``: notification when a command issued by ``MgrModule.send_command``
+ completes
+* ``perf_schema_update``: perf counters schema update
+* ``mon_map``: monitor map update
+* ``fs_map``: cephfs map update
+* ``osd_map``: OSD map update
+* ``service_map``: services (RGW, RBD-Mirror, etc.) map update
+* ``mon_status``: monitor status regular update
+* ``health``: health status regular update
+* ``pg_summary``: regular update of PG status information
+
+
+How to write a unit test when a controller accesses a Ceph module?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Consider the following example that implements a controller that retrieves the
+list of RBD images of the ``rbd`` pool::
+
+ import rbd
+ from .. import mgr
+ from ..tools import ApiController, RESTController
+
+
+ @ApiController('rbdimages')
+ class RbdImages(RESTController):
+ def __init__(self):
+ self.ioctx = mgr.rados.open_ioctx('rbd')
+ self.rbd = rbd.RBD()
+
+ def list(self):
+ return [{'name': n} for n in self.rbd.list(self.ioctx)]
+
+In the example above, we want to mock the return value of the ``rbd.list``
+function, so that we can test the JSON response of the controller.
+
+The unit test code will look like the following::
+
+ import mock
+ from .helper import ControllerTestCase
+
+
+ class RbdImagesTest(ControllerTestCase):
+ @mock.patch('rbd.RBD.list')
+ def test_list(self, rbd_list_mock):
+ rbd_list_mock.return_value = ['img1', 'img2']
+ self._get('/api/rbdimages')
+ self.assertJsonBody([{'name': 'img1'}, {'name': 'img2'}])
+
+
+
+How to add a new configuration setting?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you need to store some configuration setting for a new feature, we already
+provide an easy mechanism for you to specify/use the new config setting.
+
+For instance, if you want to add a new configuration setting to hold the
+email address of the dashboard admin, just add a setting name as a class
+attribute to the ``Options`` class in the ``settings.py`` file::
+
+ # ...
+ class Options(object):
+ # ...
+
+ ADMIN_EMAIL_ADDRESS = ('admin@admin.com', str)
+
+The value of the class attribute is a pair composed by the default value for that
+setting, and the python type of the value.
+
+By declaring the ``ADMIN_EMAIL_ADDRESS`` class attribute, when you restart the
+dashboard plugin, you will atomatically gain two additional CLI commands to
+get and set that setting::
+
+ $ ceph dashboard get-admin-email-address
+ $ ceph dashboard set-admin-email-address <value>
+
+To access, or modify the config setting value from your Python code, either
+inside a controller or anywhere else, you just need to import the ``Settings``
+class and access it like this::
+
+ from settings import Settings
+
+ # ...
+ tmp_var = Settings.ADMIN_EMAIL_ADDRESS
+
+ # ....
+ Settings.ADMIN_EMAIL_ADDRESS = 'myemail@admin.com'
+
+The settings management implementation will make sure that if you change a
+setting value from the Python code you will see that change when accessing
+that setting from the CLI and vice-versa.
+
--- /dev/null
+Dashboard and Administration Module for Ceph Manager
+=========================================================================
+
+Overview
+--------
+
+The original Ceph Manager Dashboard that was shipped with Ceph "Luminous"
+started out as a simple read-only view into various run-time information and
+performance data of a Ceph cluster.
+
+However, there is a `growing demand <http://pad.ceph.com/p/mimic-dashboard>`_
+for adding more web-based management capabilities, to make it easier for
+administrators that prefer a WebUI over the command line.
+
+This module is an ongoing project to add a native web based monitoring and
+administration application to Ceph Manager. It aims at becoming a successor of
+the existing dashboard, which provides read-only functionality and uses a
+simpler architecture to achieve the original goal.
+
+The code and architecture of this module is derived from and inspired by the
+`openATTIC Ceph management and monitoring tool <https://openattic.org/>`_ (both
+the backend and WebUI). The development is actively driven by the team behind
+openATTIC.
+
+The intention is to reuse as much of the existing openATTIC code as possible,
+while adapting it to the different environment. The current openATTIC backend
+implementation is based on Django and the Django REST framework, the Manager
+module's backend code will use the CherryPy framework and a custom REST API
+implementation instead.
+
+The WebUI implementation will be developed using Angular/TypeScript, merging
+both functionality from the existing dashboard as well as adding new
+functionality originally developed for the standalone version of openATTIC.
+
+The porting and migration of the existing openATTIC and dashboard functionality
+will be done in stages. The tasks are currently tracked in the `openATTIC team's
+JIRA instance <https://tracker.openattic.org/browse/OP-3039>`_.
+
+Enabling and Starting the Dashboard
+-----------------------------------
+
+If you have installed Ceph from distribution packages, the package management
+system should have taken care of installing all the required dependencies.
+
+If you want to start the dashboard from within a development environment, you
+need to have built Ceph (see the toplevel ``README.md`` file and the `developer
+documentation <http://docs.ceph.com/docs/master/dev/>`_ for details on how to
+accomplish this.
+
+Finally, you need to build the dashboard frontend code. See the file
+``HACKING.rst`` in this directory for instructions on setting up the necessary
+development environment.
+
+From within a running Ceph cluster, you can start the Dashboard module by
+running the following command::
+
+ $ ceph mgr module enable dashboard
+
+You can see currently enabled Manager modules with::
+
+ $ ceph mgr module ls
+
+In order to be able to log in, you need to define a username and password, which
+will be stored in the MON's configuration database::
+
+ $ ceph dashboard set-login-credentials <username> <password>
+
+The password will be stored as a hash using ``bcrypt``.
+
+The Dashboard's WebUI should then be reachable on TCP port 8080.
+
+Working on the Dashboard Code
+-----------------------------
+
+If you're interested in helping with the development of the dashboard, please
+see the file ``HACKING.rst`` for details on how to set up a development
+environment and some other development-related topics.
--- /dev/null
+# -*- coding: utf-8 -*-
+# pylint: disable=wrong-import-position,global-statement,protected-access
+"""
+openATTIC module
+"""
+from __future__ import absolute_import
+
+import os
+
+
+if 'UNITTEST' not in os.environ:
+ class _LoggerProxy(object):
+ def __init__(self):
+ self._logger = None
+
+ def __getattr__(self, item):
+ if self._logger is None:
+ raise AttributeError("logger not initialized")
+ return getattr(self._logger, item)
+
+ class _ModuleProxy(object):
+ def __init__(self):
+ self._mgr = None
+
+ def init(self, module_inst):
+ global logger
+ self._mgr = module_inst
+ logger._logger = self._mgr._logger
+
+ def __getattr__(self, item):
+ if self._mgr is None:
+ raise AttributeError("global manager module instance not initialized")
+ return getattr(self._mgr, item)
+
+ mgr = _ModuleProxy()
+ logger = _LoggerProxy()
+
+ from .module import Module, StandbyModule
+else:
+ import logging
+ logging.basicConfig(level=logging.DEBUG)
+ logger = logging.getLogger(__name__)
+ logging.root.handlers[0].setLevel(logging.DEBUG)
+ os.environ['PATH'] = '{}:{}'.format(os.path.abspath('../../../../build/bin'),
+ os.environ['PATH'])
+
+ # Mock ceph module otherwise every module that is involved in a testcase and imports it will
+ # raise an ImportError
+ import sys
+ import mock
+ sys.modules['ceph_module'] = mock.Mock()
+
+ mgr = mock.Mock()
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import time
+import sys
+
+import bcrypt
+import cherrypy
+
+from ..tools import ApiController, RESTController, Session
+from .. import logger, mgr
+
+
+@ApiController('auth')
+class Auth(RESTController):
+ """
+ Provide login and logout actions.
+
+ Supported config-keys:
+
+ | KEY | DEFAULT | DESCR |
+ ------------------------------------------------------------------------|
+ | username | None | Username |
+ | password | None | Password encrypted using bcrypt |
+ | session-expire | 1200 | Session will expire after <expires> |
+ | | seconds without activity |
+ """
+
+ @RESTController.args_from_json
+ def create(self, username, password, stay_signed_in=False):
+ now = time.time()
+ config_username = mgr.get_config('username', None)
+ config_password = mgr.get_config('password', None)
+ hash_password = Auth.password_hash(password,
+ config_password)
+ if username == config_username and hash_password == config_password:
+ cherrypy.session.regenerate()
+ cherrypy.session[Session.USERNAME] = username
+ cherrypy.session[Session.TS] = now
+ cherrypy.session[Session.EXPIRE_AT_BROWSER_CLOSE] = not stay_signed_in
+ logger.debug('Login successful')
+ return {'username': username}
+
+ cherrypy.response.status = 403
+ if config_username is None:
+ logger.warning('No Credentials configured. Need to call `ceph dashboard '
+ 'set-login-credentials <username> <password>` first.')
+ else:
+ logger.debug('Login failed')
+ return {'detail': 'Invalid credentials'}
+
+ def bulk_delete(self):
+ logger.debug('Logout successful')
+ cherrypy.session[Session.USERNAME] = None
+ cherrypy.session[Session.TS] = None
+
+ @staticmethod
+ def password_hash(password, salt_password=None):
+ if not salt_password:
+ salt_password = bcrypt.gensalt()
+ if sys.version_info > (3, 0):
+ return bcrypt.hashpw(password, salt_password)
+ return bcrypt.hashpw(password.encode('utf8'), salt_password)
+
+ @staticmethod
+ def check_auth():
+ username = cherrypy.session.get(Session.USERNAME)
+ if not username:
+ logger.debug('Unauthorized access to %s',
+ cherrypy.url(relative='server'))
+ raise cherrypy.HTTPError(401, 'You are not authorized to access '
+ 'that resource')
+ now = time.time()
+ expires = float(mgr.get_config(
+ 'session-expire', Session.DEFAULT_EXPIRE))
+ if expires > 0:
+ username_ts = cherrypy.session.get(Session.TS, None)
+ if username_ts and float(username_ts) < (now - expires):
+ cherrypy.session[Session.USERNAME] = None
+ cherrypy.session[Session.TS] = None
+ logger.debug('Session expired')
+ raise cherrypy.HTTPError(401,
+ 'Session expired. You are not '
+ 'authorized to access that resource')
+ cherrypy.session[Session.TS] = now
+
+ @staticmethod
+ def set_login_credentials(username, password):
+ mgr.set_config('username', username)
+ hashed_passwd = Auth.password_hash(password)
+ mgr.set_config('password', hashed_passwd)
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from collections import defaultdict
+import json
+
+import cherrypy
+from mgr_module import CommandResult
+
+from .. import mgr
+from ..tools import ApiController, AuthRequired, BaseController, ViewCache
+
+
+@ApiController('cephfs')
+@AuthRequired()
+class CephFS(BaseController):
+ def __init__(self):
+ super(CephFS, self).__init__()
+
+ # Stateful instances of CephFSClients, hold cached results. Key to
+ # dict is FSCID
+ self.cephfs_clients = {}
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def clients(self, fs_id):
+ fs_id = self.fs_id_to_int(fs_id)
+
+ return self._clients(fs_id)
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def data(self, fs_id):
+ fs_id = self.fs_id_to_int(fs_id)
+
+ return self.fs_status(fs_id)
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def mds_counters(self, fs_id):
+ """
+ Result format: map of daemon name to map of counter to list of datapoints
+ rtype: dict[str, dict[str, list]]
+ """
+
+ # Opinionated list of interesting performance counters for the GUI --
+ # if you need something else just add it. See how simple life is
+ # when you don't have to write general purpose APIs?
+ counters = [
+ "mds_server.handle_client_request",
+ "mds_log.ev",
+ "mds_cache.num_strays",
+ "mds.exported",
+ "mds.exported_inodes",
+ "mds.imported",
+ "mds.imported_inodes",
+ "mds.inodes",
+ "mds.caps",
+ "mds.subtrees"
+ ]
+
+ fs_id = self.fs_id_to_int(fs_id)
+
+ result = {}
+ mds_names = self._get_mds_names(fs_id)
+
+ for mds_name in mds_names:
+ result[mds_name] = {}
+ for counter in counters:
+ data = mgr.get_counter("mds", mds_name, counter)
+ if data is not None:
+ result[mds_name][counter] = data[counter]
+ else:
+ result[mds_name][counter] = []
+
+ return dict(result)
+
+ @staticmethod
+ def fs_id_to_int(fs_id):
+ try:
+ return int(fs_id)
+ except ValueError:
+ raise cherrypy.HTTPError(400, "Invalid cephfs id {}".format(fs_id))
+
+ def _get_mds_names(self, filesystem_id=None):
+ names = []
+
+ fsmap = mgr.get("fs_map")
+ for fs in fsmap['filesystems']:
+ if filesystem_id is not None and fs['id'] != filesystem_id:
+ continue
+ names.extend([info['name']
+ for _, info in fs['mdsmap']['info'].items()])
+
+ if filesystem_id is None:
+ names.extend(info['name'] for info in fsmap['standbys'])
+
+ return names
+
+ def get_rate(self, daemon_type, daemon_name, stat):
+ data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
+
+ if data and len(data) > 1:
+ return (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
+
+ return 0
+
+ # pylint: disable=too-many-locals,too-many-statements,too-many-branches
+ def fs_status(self, fs_id):
+ mds_versions = defaultdict(list)
+
+ fsmap = mgr.get("fs_map")
+ filesystem = None
+ for fs in fsmap['filesystems']:
+ if fs['id'] == fs_id:
+ filesystem = fs
+ break
+
+ if filesystem is None:
+ raise cherrypy.HTTPError(404,
+ "CephFS id {0} not found".format(fs_id))
+
+ rank_table = []
+
+ mdsmap = filesystem['mdsmap']
+
+ client_count = 0
+
+ for rank in mdsmap["in"]:
+ up = "mds_{0}".format(rank) in mdsmap["up"]
+ if up:
+ gid = mdsmap['up']["mds_{0}".format(rank)]
+ info = mdsmap['info']['gid_{0}'.format(gid)]
+ dns = self.get_latest("mds", info['name'], "mds.inodes")
+ inos = self.get_latest("mds", info['name'], "mds_mem.ino")
+
+ if rank == 0:
+ client_count = self.get_latest("mds", info['name'],
+ "mds_sessions.session_count")
+ elif client_count == 0:
+ # In case rank 0 was down, look at another rank's
+ # sessionmap to get an indication of clients.
+ client_count = self.get_latest("mds", info['name'],
+ "mds_sessions.session_count")
+
+ laggy = "laggy_since" in info
+
+ state = info['state'].split(":")[1]
+ if laggy:
+ state += "(laggy)"
+
+ # if state == "active" and not laggy:
+ # c_state = self.colorize(state, self.GREEN)
+ # else:
+ # c_state = self.colorize(state, self.YELLOW)
+
+ # Populate based on context of state, e.g. client
+ # ops for an active daemon, replay progress, reconnect
+ # progress
+ activity = ""
+
+ if state == "active":
+ activity = self.get_rate("mds",
+ info['name'],
+ "mds_server.handle_client_request")
+
+ metadata = mgr.get_metadata('mds', info['name'])
+ mds_versions[metadata.get('ceph_version', 'unknown')].append(
+ info['name'])
+ rank_table.append(
+ {
+ "rank": rank,
+ "state": state,
+ "mds": info['name'],
+ "activity": activity,
+ "dns": dns,
+ "inos": inos
+ }
+ )
+
+ else:
+ rank_table.append(
+ {
+ "rank": rank,
+ "state": "failed",
+ "mds": "",
+ "activity": "",
+ "dns": 0,
+ "inos": 0
+ }
+ )
+
+ # Find the standby replays
+ # pylint: disable=unused-variable
+ for gid_str, daemon_info in mdsmap['info'].iteritems():
+ if daemon_info['state'] != "up:standby-replay":
+ continue
+
+ inos = self.get_latest("mds", daemon_info['name'], "mds_mem.ino")
+ dns = self.get_latest("mds", daemon_info['name'], "mds.inodes")
+
+ activity = self.get_rate(
+ "mds", daemon_info['name'], "mds_log.replay")
+
+ rank_table.append(
+ {
+ "rank": "{0}-s".format(daemon_info['rank']),
+ "state": "standby-replay",
+ "mds": daemon_info['name'],
+ "activity": activity,
+ "dns": dns,
+ "inos": inos
+ }
+ )
+
+ df = mgr.get("df")
+ pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
+ osdmap = mgr.get("osd_map")
+ pools = dict([(p['pool'], p) for p in osdmap['pools']])
+ metadata_pool_id = mdsmap['metadata_pool']
+ data_pool_ids = mdsmap['data_pools']
+
+ pools_table = []
+ for pool_id in [metadata_pool_id] + data_pool_ids:
+ pool_type = "metadata" if pool_id == metadata_pool_id else "data"
+ stats = pool_stats[pool_id]
+ pools_table.append({
+ "pool": pools[pool_id]['pool_name'],
+ "type": pool_type,
+ "used": stats['bytes_used'],
+ "avail": stats['max_avail']
+ })
+
+ standby_table = []
+ for standby in fsmap['standbys']:
+ metadata = mgr.get_metadata('mds', standby['name'])
+ mds_versions[metadata.get('ceph_version', 'unknown')].append(
+ standby['name'])
+
+ standby_table.append({
+ 'name': standby['name']
+ })
+
+ return {
+ "cephfs": {
+ "id": fs_id,
+ "name": mdsmap['fs_name'],
+ "client_count": client_count,
+ "ranks": rank_table,
+ "pools": pools_table
+ },
+ "standbys": standby_table,
+ "versions": mds_versions
+ }
+
+ def _clients(self, fs_id):
+ cephfs_clients = self.cephfs_clients.get(fs_id, None)
+ if cephfs_clients is None:
+ cephfs_clients = CephFSClients(mgr, fs_id)
+ self.cephfs_clients[fs_id] = cephfs_clients
+
+ try:
+ status, clients = cephfs_clients.get()
+ except AttributeError:
+ raise cherrypy.HTTPError(404,
+ "No cephfs with id {0}".format(fs_id))
+ if clients is None:
+ raise cherrypy.HTTPError(404,
+ "No cephfs with id {0}".format(fs_id))
+
+ # Decorate the metadata with some fields that will be
+ # indepdendent of whether it's a kernel or userspace
+ # client, so that the javascript doesn't have to grok that.
+ for client in clients:
+ if "ceph_version" in client['client_metadata']:
+ client['type'] = "userspace"
+ client['version'] = client['client_metadata']['ceph_version']
+ client['hostname'] = client['client_metadata']['hostname']
+ elif "kernel_version" in client['client_metadata']:
+ client['type'] = "kernel"
+ client['version'] = client['client_metadata']['kernel_version']
+ client['hostname'] = client['client_metadata']['hostname']
+ else:
+ client['type'] = "unknown"
+ client['version'] = ""
+ client['hostname'] = ""
+
+ return {
+ 'status': status,
+ 'data': clients
+ }
+
+ def get_latest(self, daemon_type, daemon_name, stat):
+ data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
+ if data:
+ return data[-1][1]
+ return 0
+
+
+class CephFSClients(object):
+ def __init__(self, module_inst, fscid):
+ self._module = module_inst
+ self.fscid = fscid
+
+ # pylint: disable=unused-variable
+ @ViewCache()
+ def get(self):
+ mds_spec = "{0}:0".format(self.fscid)
+ result = CommandResult("")
+ self._module.send_command(result, "mds", mds_spec,
+ json.dumps({
+ "prefix": "session ls",
+ }),
+ "")
+ r, outb, outs = result.wait()
+ # TODO handle nonzero returns, e.g. when rank isn't active
+ assert r == 0
+ return json.loads(outb)
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import cherrypy
+
+from .. import mgr
+from ..tools import ApiController, RESTController, AuthRequired
+
+
+@ApiController('cluster_conf')
+@AuthRequired()
+class ClusterConfiguration(RESTController):
+ def list(self):
+ options = mgr.get("config_options")['options']
+ return options
+
+ def get(self, name):
+ for option in mgr.get('config_options')['options']:
+ if option['name'] == name:
+ return option
+
+ raise cherrypy.HTTPError(404)
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import collections
+import json
+
+import cherrypy
+from mgr_module import CommandResult
+
+from .. import mgr
+from ..services.ceph_service import CephService
+from ..tools import ApiController, AuthRequired, BaseController, NotificationQueue
+
+
+LOG_BUFFER_SIZE = 30
+
+
+@ApiController('dashboard')
+@AuthRequired()
+class Dashboard(BaseController):
+ def __init__(self):
+ super(Dashboard, self).__init__()
+
+ self._log_initialized = False
+
+ self.log_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE)
+ self.audit_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE)
+
+ def append_log(self, log_struct):
+ if log_struct['channel'] == "audit":
+ self.audit_buffer.appendleft(log_struct)
+ else:
+ self.log_buffer.appendleft(log_struct)
+
+ def load_buffer(self, buf, channel_name):
+ result = CommandResult("")
+ mgr.send_command(result, "mon", "", json.dumps({
+ "prefix": "log last",
+ "format": "json",
+ "channel": channel_name,
+ "num": LOG_BUFFER_SIZE
+ }), "")
+ r, outb, outs = result.wait()
+ if r != 0:
+ # Oh well. We won't let this stop us though.
+ self.log.error("Error fetching log history (r={0}, \"{1}\")".format(
+ r, outs))
+ else:
+ try:
+ lines = json.loads(outb)
+ except ValueError:
+ self.log.error("Error decoding log history")
+ else:
+ for l in lines:
+ buf.appendleft(l)
+
+ # pylint: disable=R0914
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def health(self):
+ if not self._log_initialized:
+ self._log_initialized = True
+
+ self.load_buffer(self.log_buffer, "cluster")
+ self.load_buffer(self.audit_buffer, "audit")
+
+ NotificationQueue.register(self.append_log, 'clog')
+
+ # Fuse osdmap with pg_summary to get description of pools
+ # including their PG states
+
+ osd_map = self.osd_map()
+
+ pools = CephService.get_pool_list_with_stats()
+
+ # Not needed, skip the effort of transmitting this
+ # to UI
+ del osd_map['pg_temp']
+
+ df = mgr.get("df")
+ df['stats']['total_objects'] = sum(
+ [p['stats']['objects'] for p in df['pools']])
+
+ return {
+ "health": self.health_data(),
+ "mon_status": self.mon_status(),
+ "fs_map": mgr.get('fs_map'),
+ "osd_map": osd_map,
+ "clog": list(self.log_buffer),
+ "audit_log": list(self.audit_buffer),
+ "pools": pools,
+ "mgr_map": mgr.get("mgr_map"),
+ "df": df
+ }
+
+ def mon_status(self):
+ mon_status_data = mgr.get("mon_status")
+ return json.loads(mon_status_data['json'])
+
+ def osd_map(self):
+ osd_map = mgr.get("osd_map")
+
+ assert osd_map is not None
+
+ osd_map['tree'] = mgr.get("osd_map_tree")
+ osd_map['crush'] = mgr.get("osd_map_crush")
+ osd_map['crush_map_text'] = mgr.get("osd_map_crush_map_text")
+ osd_map['osd_metadata'] = mgr.get("osd_metadata")
+
+ return osd_map
+
+ def health_data(self):
+ health_data = mgr.get("health")
+ health = json.loads(health_data['json'])
+
+ # Transform the `checks` dict into a list for the convenience
+ # of rendering from javascript.
+ checks = []
+ for k, v in health['checks'].items():
+ v['type'] = k
+ checks.append(v)
+
+ checks = sorted(checks, key=lambda c: c['severity'])
+
+ health['checks'] = checks
+
+ return health
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .. import mgr
+from ..tools import ApiController, AuthRequired, RESTController
+
+
+@ApiController('host')
+@AuthRequired()
+class Host(RESTController):
+ def list(self):
+ return mgr.list_servers()
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+
+import cherrypy
+
+from .. import mgr
+from ..tools import ApiController, AuthRequired, BaseController
+
+
+@ApiController('monitor')
+@AuthRequired()
+class Monitor(BaseController):
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def default(self):
+ in_quorum, out_quorum = [], []
+
+ counters = ['mon.num_sessions']
+
+ mon_status_json = mgr.get("mon_status")
+ mon_status = json.loads(mon_status_json['json'])
+
+ for mon in mon_status["monmap"]["mons"]:
+ mon["stats"] = {}
+ for counter in counters:
+ data = mgr.get_counter("mon", mon["name"], counter)
+ if data is not None:
+ mon["stats"][counter.split(".")[1]] = data[counter]
+ else:
+ mon["stats"][counter.split(".")[1]] = []
+ if mon["rank"] in mon_status["quorum"]:
+ in_quorum.append(mon)
+ else:
+ out_quorum.append(mon)
+
+ return {
+ 'mon_status': mon_status,
+ 'in_quorum': in_quorum,
+ 'out_quorum': out_quorum
+ }
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+
+from mgr_module import CommandResult
+
+from .. import logger, mgr
+from ..tools import ApiController, AuthRequired, RESTController
+
+
+@ApiController('osd')
+@AuthRequired()
+class Osd(RESTController):
+ def get_counter(self, daemon_name, stat):
+ return mgr.get_counter('osd', daemon_name, stat)[stat]
+
+ def get_rate(self, daemon_name, stat):
+ data = self.get_counter(daemon_name, stat)
+ rate = 0
+ if data and len(data) > 1:
+ rate = (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
+ return rate
+
+ def get_latest(self, daemon_name, stat):
+ data = self.get_counter(daemon_name, stat)
+ latest = 0
+ if data and data[-1] and len(data[-1]) == 2:
+ latest = data[-1][1]
+ return latest
+
+ def list(self):
+ osds = self.get_osd_map()
+ # Extending by osd stats information
+ for s in mgr.get('osd_stats')['osd_stats']:
+ osds[str(s['osd'])].update({'osd_stats': s})
+ # Extending by osd node information
+ nodes = mgr.get('osd_map_tree')['nodes']
+ osd_tree = [(str(o['id']), o) for o in nodes if o['id'] >= 0]
+ for o in osd_tree:
+ osds[o[0]].update({'tree': o[1]})
+ # Extending by osd parent node information
+ hosts = [(h['name'], h) for h in nodes if h['id'] < 0]
+ for h in hosts:
+ for o_id in h[1]['children']:
+ if o_id >= 0:
+ osds[str(o_id)]['host'] = h[1]
+ # Extending by osd histogram data
+ for o_id in osds:
+ o = osds[o_id]
+ o['stats'] = {}
+ o['stats_history'] = {}
+ osd_spec = str(o['osd'])
+ for s in ['osd.op_w', 'osd.op_in_bytes', 'osd.op_r', 'osd.op_out_bytes']:
+ prop = s.split('.')[1]
+ o['stats'][prop] = self.get_rate(osd_spec, s)
+ o['stats_history'][prop] = self.get_counter(osd_spec, s)
+ # Gauge stats
+ for s in ['osd.numpg', 'osd.stat_bytes', 'osd.stat_bytes_used']:
+ o['stats'][s.split('.')[1]] = self.get_latest(osd_spec, s)
+ return osds.values()
+
+ def get_osd_map(self):
+ osds = {}
+ for osd in mgr.get('osd_map')['osds']:
+ osd['id'] = osd['osd']
+ osds[str(osd['id'])] = osd
+ return osds
+
+ def get(self, svc_id):
+ result = CommandResult('')
+ mgr.send_command(result, 'osd', svc_id,
+ json.dumps({
+ 'prefix': 'perf histogram dump',
+ }),
+ '')
+ r, outb, outs = result.wait()
+ if r != 0:
+ histogram = None
+ logger.warning('Failed to load histogram for OSD %s', svc_id)
+ logger.debug(outs)
+ histogram = outs
+ else:
+ histogram = json.loads(outb)
+ return {
+ 'osd_map': self.get_osd_map()[svc_id],
+ 'osd_metadata': mgr.get_metadata('osd', svc_id),
+ 'histogram': histogram,
+ }
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .. import mgr
+from ..tools import ApiController, AuthRequired, RESTController
+
+
+class PerfCounter(RESTController):
+ def __init__(self, service_type):
+ self._service_type = service_type
+
+ def _get_rate(self, daemon_type, daemon_name, stat):
+ data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
+ if data and len(data) > 1:
+ return (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
+ return 0
+
+ def _get_latest(self, daemon_type, daemon_name, stat):
+ data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
+ if data:
+ return data[-1][1]
+ return 0
+
+ def get(self, service_id):
+ schema = mgr.get_perf_schema(
+ self._service_type, str(service_id)).values()[0]
+ counters = []
+
+ for key, value in sorted(schema.items()):
+ counter = dict()
+ counter['name'] = str(key)
+ counter['description'] = value['description']
+ # pylint: disable=W0212
+ if mgr._stattype_to_str(value['type']) == 'counter':
+ counter['value'] = self._get_rate(
+ self._service_type, service_id, key)
+ counter['unit'] = mgr._unit_to_str(value['units'])
+ else:
+ counter['value'] = self._get_latest(
+ self._service_type, service_id, key)
+ counter['unit'] = ''
+ counters.append(counter)
+
+ return {
+ 'service': {
+ 'type': self._service_type,
+ 'id': service_id
+ },
+ 'counters': counters
+ }
+
+
+@ApiController('perf_counters')
+@AuthRequired()
+class PerfCounters(RESTController):
+ def __init__(self):
+ self.mds = PerfCounter('mds')
+ self.mon = PerfCounter('mon')
+ self.osd = PerfCounter('osd')
+ self.rgw = PerfCounter('rgw')
+ self.rbd_mirror = PerfCounter('rbd-mirror')
+ self.mgr = PerfCounter('mgr')
+
+ def list(self):
+ counters = mgr.get_all_perf_counters()
+ return counters
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from ..services.ceph_service import CephService
+from ..tools import ApiController, RESTController, AuthRequired
+
+
+@ApiController('pool')
+@AuthRequired()
+class Pool(RESTController):
+
+ @classmethod
+ def _serialize_pool(cls, pool, attrs):
+ if not attrs or not isinstance(attrs, list):
+ return pool
+
+ res = {}
+ for attr in attrs:
+ if attr not in pool:
+ continue
+ if attr == 'type':
+ res[attr] = {1: 'replicated', 3: 'erasure'}[pool[attr]]
+ else:
+ res[attr] = pool[attr]
+
+ # pool_name is mandatory
+ res['pool_name'] = pool['pool_name']
+ return res
+
+ @staticmethod
+ def _str_to_bool(var):
+ if isinstance(var, bool):
+ return var
+ return var.lower() in ("true", "yes", "1", 1)
+
+ def list(self, attrs=None, stats=False):
+ if attrs:
+ attrs = attrs.split(',')
+
+ if self._str_to_bool(stats):
+ pools = CephService.get_pool_list_with_stats()
+ else:
+ pools = CephService.get_pool_list()
+
+ return [self._serialize_pool(pool, attrs) for pool in pools]
+
+ def get(self, pool_name, attrs=None, stats=False):
+ pools = self.list(attrs, stats)
+ return [pool for pool in pools if pool['pool_name'] == pool_name][0]
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import math
+import cherrypy
+import rbd
+
+from .. import mgr
+from ..tools import ApiController, AuthRequired, RESTController, ViewCache
+
+
+@ApiController('rbd')
+@AuthRequired()
+class Rbd(RESTController):
+
+ RBD_FEATURES_NAME_MAPPING = {
+ rbd.RBD_FEATURE_LAYERING: "layering",
+ rbd.RBD_FEATURE_STRIPINGV2: "striping",
+ rbd.RBD_FEATURE_EXCLUSIVE_LOCK: "exclusive-lock",
+ rbd.RBD_FEATURE_OBJECT_MAP: "object-map",
+ rbd.RBD_FEATURE_FAST_DIFF: "fast-diff",
+ rbd.RBD_FEATURE_DEEP_FLATTEN: "deep-flatten",
+ rbd.RBD_FEATURE_JOURNALING: "journaling",
+ rbd.RBD_FEATURE_DATA_POOL: "data-pool",
+ rbd.RBD_FEATURE_OPERATIONS: "operations",
+ }
+
+ def __init__(self):
+ self.rbd = None
+
+ @staticmethod
+ def _format_bitmask(features):
+ """
+ Formats the bitmask:
+
+ >>> Rbd._format_bitmask(45)
+ 'deep-flatten, exclusive-lock, layering, object-map'
+ """
+ names = [val for key, val in Rbd.RBD_FEATURES_NAME_MAPPING.items()
+ if key & features == key]
+ return ', '.join(sorted(names))
+
+ @staticmethod
+ def _format_features(features):
+ """
+ Converts the features list to bitmask:
+
+ >>> Rbd._format_features(['deep-flatten', 'exclusive-lock', 'layering', 'object-map'])
+ 45
+
+ >>> Rbd._format_features(None) is None
+ True
+
+ >>> Rbd._format_features('not a list') is None
+ True
+ """
+ if not features or not isinstance(features, list):
+ return None
+
+ res = 0
+ for key, value in Rbd.RBD_FEATURES_NAME_MAPPING.items():
+ if value in features:
+ res = key | res
+ return res
+
+ @ViewCache()
+ def _rbd_list(self, pool_name):
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ self.rbd = rbd.RBD()
+ names = self.rbd.list(ioctx)
+ result = []
+ for name in names:
+ i = rbd.Image(ioctx, name)
+ stat = i.stat()
+ stat['name'] = name
+ features = i.features()
+ stat['features'] = features
+ stat['features_name'] = self._format_bitmask(features)
+
+ try:
+ parent_info = i.parent_info()
+ parent = "{}@{}".format(parent_info[0], parent_info[1])
+ if parent_info[0] != pool_name:
+ parent = "{}/{}".format(parent_info[0], parent)
+ stat['parent'] = parent
+ except rbd.ImageNotFound:
+ pass
+ result.append(stat)
+ return result
+
+ def get(self, pool_name):
+ # pylint: disable=unbalanced-tuple-unpacking
+ status, value = self._rbd_list(pool_name)
+ if status == ViewCache.VALUE_EXCEPTION:
+ raise value
+ return {'status': status, 'value': value}
+
+ def create(self, data):
+ if not self.rbd:
+ self.rbd = rbd.RBD()
+
+ # Get input values
+ name = data.get('name')
+ pool_name = data.get('pool_name')
+ size = data.get('size')
+ obj_size = data.get('obj_size')
+ features = data.get('features')
+ stripe_unit = data.get('stripe_unit')
+ stripe_count = data.get('stripe_count')
+ data_pool = data.get('data_pool')
+
+ # Set order
+ order = None
+ if obj_size and obj_size > 0:
+ order = int(round(math.log(float(obj_size), 2)))
+
+ # Set features
+ feature_bitmask = self._format_features(features)
+
+ ioctx = mgr.rados.open_ioctx(pool_name)
+
+ try:
+ self.rbd.create(ioctx, name, size, order=order, old_format=False,
+ features=feature_bitmask, stripe_unit=stripe_unit,
+ stripe_count=stripe_count, data_pool=data_pool)
+ except rbd.OSError as e:
+ cherrypy.response.status = 400
+ return {'success': False, 'detail': str(e), 'errno': e.errno}
+ return {'success': True}
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+import re
+
+from functools import partial
+
+import cherrypy
+import rbd
+
+from .. import logger, mgr
+from ..services.ceph_service import CephService
+from ..tools import ApiController, AuthRequired, BaseController, ViewCache
+
+
+@ViewCache()
+def get_daemons_and_pools(): # pylint: disable=R0915
+ def get_daemons():
+ daemons = []
+ for hostname, server in CephService.get_service_map('rbd-mirror').items():
+ for service in server['services']:
+ id = service['id'] # pylint: disable=W0622
+ metadata = service['metadata']
+ status = service['status']
+
+ try:
+ status = json.loads(status['json'])
+ except (ValueError, KeyError) as _:
+ status = {}
+
+ instance_id = metadata['instance_id']
+ if id == instance_id:
+ # new version that supports per-cluster leader elections
+ id = metadata['id']
+
+ # extract per-daemon service data and health
+ daemon = {
+ 'id': id,
+ 'instance_id': instance_id,
+ 'version': metadata['ceph_version'],
+ 'server_hostname': hostname,
+ 'service': service,
+ 'server': server,
+ 'metadata': metadata,
+ 'status': status
+ }
+ daemon = dict(daemon, **get_daemon_health(daemon))
+ daemons.append(daemon)
+
+ return sorted(daemons, key=lambda k: k['instance_id'])
+
+ def get_daemon_health(daemon):
+ health = {
+ 'health_color': 'info',
+ 'health': 'Unknown'
+ }
+ for _, pool_data in daemon['status'].items(): # TODO: simplify
+ if (health['health'] != 'error' and
+ [k for k, v in pool_data.get('callouts', {}).items()
+ if v['level'] == 'error']):
+ health = {
+ 'health_color': 'error',
+ 'health': 'Error'
+ }
+ elif (health['health'] != 'error' and
+ [k for k, v in pool_data.get('callouts', {}).items()
+ if v['level'] == 'warning']):
+ health = {
+ 'health_color': 'warning',
+ 'health': 'Warning'
+ }
+ elif health['health_color'] == 'info':
+ health = {
+ 'health_color': 'success',
+ 'health': 'OK'
+ }
+ return health
+
+ def get_pools(daemons): # pylint: disable=R0912, R0915
+ pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')]
+ pool_stats = {}
+ rbdctx = rbd.RBD()
+ for pool_name in pool_names:
+ logger.debug("Constructing IOCtx %s", pool_name)
+ try:
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ except TypeError:
+ logger.exception("Failed to open pool %s", pool_name)
+ continue
+
+ try:
+ mirror_mode = rbdctx.mirror_mode_get(ioctx)
+ except: # noqa pylint: disable=W0702
+ logger.exception("Failed to query mirror mode %s", pool_name)
+
+ stats = {}
+ if mirror_mode == rbd.RBD_MIRROR_MODE_DISABLED:
+ continue
+ elif mirror_mode == rbd.RBD_MIRROR_MODE_IMAGE:
+ mirror_mode = "image"
+ elif mirror_mode == rbd.RBD_MIRROR_MODE_POOL:
+ mirror_mode = "pool"
+ else:
+ mirror_mode = "unknown"
+ stats['health_color'] = "warning"
+ stats['health'] = "Warning"
+
+ pool_stats[pool_name] = dict(stats, **{
+ 'mirror_mode': mirror_mode
+ })
+
+ for daemon in daemons:
+ for _, pool_data in daemon['status'].items():
+ stats = pool_stats.get(pool_data['name'], None)
+ if stats is None:
+ continue
+
+ if pool_data.get('leader', False):
+ # leader instance stores image counts
+ stats['leader_id'] = daemon['metadata']['instance_id']
+ stats['image_local_count'] = pool_data.get('image_local_count', 0)
+ stats['image_remote_count'] = pool_data.get('image_remote_count', 0)
+
+ if (stats.get('health_color', '') != 'error' and
+ pool_data.get('image_error_count', 0) > 0):
+ stats['health_color'] = 'error'
+ stats['health'] = 'Error'
+ elif (stats.get('health_color', '') != 'error' and
+ pool_data.get('image_warning_count', 0) > 0):
+ stats['health_color'] = 'warning'
+ stats['health'] = 'Warning'
+ elif stats.get('health', None) is None:
+ stats['health_color'] = 'success'
+ stats['health'] = 'OK'
+
+ for _, stats in pool_stats.items():
+ if stats.get('health', None) is None:
+ # daemon doesn't know about pool
+ stats['health_color'] = 'error'
+ stats['health'] = 'Error'
+ elif stats.get('leader_id', None) is None:
+ # no daemons are managing the pool as leader instance
+ stats['health_color'] = 'warning'
+ stats['health'] = 'Warning'
+ return pool_stats
+
+ daemons = get_daemons()
+ return {
+ 'daemons': daemons,
+ 'pools': get_pools(daemons)
+ }
+
+
+@ApiController('rbdmirror')
+@AuthRequired()
+class RbdMirror(BaseController):
+
+ def __init__(self):
+ self.pool_data = {}
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def default(self):
+ status, content_data = self._get_content_data()
+ return {'status': status, 'content_data': content_data}
+
+ @ViewCache()
+ def _get_pool_datum(self, pool_name):
+ data = {}
+ logger.debug("Constructing IOCtx %s", pool_name)
+ try:
+ ioctx = mgr.rados.open_ioctx(pool_name)
+ except TypeError:
+ logger.exception("Failed to open pool %s", pool_name)
+ return None
+
+ mirror_state = {
+ 'down': {
+ 'health': 'issue',
+ 'state_color': 'warning',
+ 'state': 'Unknown',
+ 'description': None
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_UNKNOWN: {
+ 'health': 'issue',
+ 'state_color': 'warning',
+ 'state': 'Unknown'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_ERROR: {
+ 'health': 'issue',
+ 'state_color': 'error',
+ 'state': 'Error'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_SYNCING: {
+ 'health': 'syncing'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_STARTING_REPLAY: {
+ 'health': 'ok',
+ 'state_color': 'success',
+ 'state': 'Starting'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_REPLAYING: {
+ 'health': 'ok',
+ 'state_color': 'success',
+ 'state': 'Replaying'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_STOPPING_REPLAY: {
+ 'health': 'ok',
+ 'state_color': 'success',
+ 'state': 'Stopping'
+ },
+ rbd.MIRROR_IMAGE_STATUS_STATE_STOPPED: {
+ 'health': 'ok',
+ 'state_color': 'info',
+ 'state': 'Primary'
+ }
+ }
+
+ rbdctx = rbd.RBD()
+ try:
+ mirror_image_status = rbdctx.mirror_image_status_list(ioctx)
+ data['mirror_images'] = sorted([
+ dict({
+ 'name': image['name'],
+ 'description': image['description']
+ }, **mirror_state['down' if not image['up'] else image['state']])
+ for image in mirror_image_status
+ ], key=lambda k: k['name'])
+ except rbd.ImageNotFound:
+ pass
+ except: # noqa pylint: disable=W0702
+ logger.exception("Failed to list mirror image status %s", pool_name)
+
+ return data
+
+ @ViewCache()
+ def _get_content_data(self): # pylint: disable=R0914
+
+ def get_pool_datum(pool_name):
+ pool_datum = self.pool_data.get(pool_name, None)
+ if pool_datum is None:
+ pool_datum = partial(self._get_pool_datum, pool_name)
+ self.pool_data[pool_name] = pool_datum
+
+ _, value = pool_datum()
+ return value
+
+ pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')]
+ _, data = get_daemons_and_pools()
+ if isinstance(data, Exception):
+ logger.exception("Failed to get rbd-mirror daemons list")
+ raise type(data)(str(data))
+ daemons = data.get('daemons', [])
+ pool_stats = data.get('pools', {})
+
+ pools = []
+ image_error = []
+ image_syncing = []
+ image_ready = []
+ for pool_name in pool_names:
+ pool = get_pool_datum(pool_name) or {}
+ stats = pool_stats.get(pool_name, {})
+ if stats.get('mirror_mode', None) is None:
+ continue
+
+ mirror_images = pool.get('mirror_images', [])
+ for mirror_image in mirror_images:
+ image = {
+ 'pool_name': pool_name,
+ 'name': mirror_image['name']
+ }
+
+ if mirror_image['health'] == 'ok':
+ image.update({
+ 'state_color': mirror_image['state_color'],
+ 'state': mirror_image['state'],
+ 'description': mirror_image['description']
+ })
+ image_ready.append(image)
+ elif mirror_image['health'] == 'syncing':
+ p = re.compile("bootstrapping, IMAGE_COPY/COPY_OBJECT (.*)%")
+ image.update({
+ 'progress': (p.findall(mirror_image['description']) or [0])[0]
+ })
+ image_syncing.append(image)
+ else:
+ image.update({
+ 'state_color': mirror_image['state_color'],
+ 'state': mirror_image['state'],
+ 'description': mirror_image['description']
+ })
+ image_error.append(image)
+
+ pools.append(dict({
+ 'name': pool_name
+ }, **stats))
+
+ return {
+ 'daemons': daemons,
+ 'pools': pools,
+ 'image_error': image_error,
+ 'image_syncing': image_syncing,
+ 'image_ready': image_ready
+ }
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+
+from .. import logger
+from ..services.ceph_service import CephService
+from ..tools import ApiController, RESTController, AuthRequired
+
+
+@ApiController('rgw')
+@AuthRequired()
+class Rgw(RESTController):
+ pass
+
+
+@ApiController('rgw/daemon')
+@AuthRequired()
+class RgwDaemon(RESTController):
+
+ def list(self):
+ daemons = []
+ for hostname, server in CephService.get_service_map('rgw').items():
+ for service in server['services']:
+ metadata = service['metadata']
+ status = service['status']
+ if 'json' in status:
+ try:
+ status = json.loads(status['json'])
+ except ValueError:
+ logger.warning("%s had invalid status json", service['id'])
+ status = {}
+ else:
+ logger.warning('%s has no key "json" in status', service['id'])
+
+ # extract per-daemon service data and health
+ daemon = {
+ 'id': service['id'],
+ 'version': metadata['ceph_version'],
+ 'server_hostname': hostname
+ }
+
+ daemons.append(daemon)
+
+ return sorted(daemons, key=lambda k: k['id'])
+
+ def get(self, svc_id):
+ daemon = {
+ 'rgw_metadata': [],
+ 'rgw_id': svc_id,
+ 'rgw_status': []
+ }
+ service = CephService.get_service('rgw', svc_id)
+ if not service:
+ return daemon
+
+ metadata = service['metadata']
+ status = service['status']
+ if 'json' in status:
+ try:
+ status = json.loads(status['json'])
+ except ValueError:
+ logger.warning("%s had invalid status json", service['id'])
+ status = {}
+ else:
+ logger.warning('%s has no key "json" in status', service['id'])
+
+ daemon['rgw_metadata'] = metadata
+ daemon['rgw_status'] = status
+ return daemon
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import json
+
+import cherrypy
+
+from .. import logger, mgr
+from ..controllers.rbd_mirroring import get_daemons_and_pools
+from ..tools import AuthRequired, ApiController, BaseController
+from ..services.ceph_service import CephService
+
+
+@ApiController('summary')
+@AuthRequired()
+class Summary(BaseController):
+ def _rbd_pool_data(self):
+ pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')]
+ return sorted(pool_names)
+
+ def _health_status(self):
+ health_data = mgr.get("health")
+ return json.loads(health_data["json"])['status']
+
+ def _filesystems(self):
+ fsmap = mgr.get("fs_map")
+ return [
+ {
+ "id": f['id'],
+ "name": f['mdsmap']['fs_name']
+ }
+ for f in fsmap['filesystems']
+ ]
+
+ def _rbd_mirroring(self):
+ _, data = get_daemons_and_pools()
+
+ if isinstance(data, Exception):
+ logger.exception("Failed to get rbd-mirror daemons and pools")
+ raise type(data)(str(data))
+ else:
+ daemons = data.get('daemons', [])
+ pools = data.get('pools', {})
+
+ warnings = 0
+ errors = 0
+ for daemon in daemons:
+ if daemon['health_color'] == 'error':
+ errors += 1
+ elif daemon['health_color'] == 'warning':
+ warnings += 1
+ for _, pool in pools.items():
+ if pool['health_color'] == 'error':
+ errors += 1
+ elif pool['health_color'] == 'warning':
+ warnings += 1
+ return {'warnings': warnings, 'errors': errors}
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def default(self):
+ return {
+ 'rbd_pools': self._rbd_pool_data(),
+ 'health_status': self._health_status(),
+ 'filesystems': self._filesystems(),
+ 'rbd_mirroring': self._rbd_mirroring(),
+ 'mgr_id': mgr.get_mgr_id(),
+ 'have_mon_connection': mgr.have_mon_connection()
+ }
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from .. import mgr
+from ..services.ceph_service import CephService
+from ..tools import ApiController, AuthRequired, RESTController
+
+SERVICE_TYPE = 'tcmu-runner'
+
+
+@ApiController('tcmuiscsi')
+@AuthRequired()
+class TcmuIscsi(RESTController):
+ # pylint: disable=too-many-locals,too-many-nested-blocks
+ def list(self): # pylint: disable=unused-argument
+ daemons = {}
+ images = {}
+ for service in CephService.get_service_list(SERVICE_TYPE):
+ metadata = service['metadata']
+ status = service['status']
+ hostname = service['hostname']
+
+ daemon = daemons.get(hostname, None)
+ if daemon is None:
+ daemon = {
+ 'server_hostname': hostname,
+ 'version': metadata['ceph_version'],
+ 'optimized_paths': 0,
+ 'non_optimized_paths': 0
+ }
+ daemons[hostname] = daemon
+
+ service_id = service['id']
+ device_id = service_id.split(':')[-1]
+ image = images.get(device_id)
+ if image is None:
+ image = {
+ 'device_id': device_id,
+ 'pool_name': metadata['pool_name'],
+ 'name': metadata['image_name'],
+ 'id': metadata.get('image_id', None),
+ 'optimized_paths': [],
+ 'non_optimized_paths': []
+ }
+ images[device_id] = image
+
+ if status.get('lock_owner', 'false') == 'true':
+ daemon['optimized_paths'] += 1
+ image['optimized_paths'].append(hostname)
+
+ perf_key_prefix = "librbd-{id}-{pool}-{name}.".format(
+ id=metadata.get('image_id', ''),
+ pool=metadata['pool_name'],
+ name=metadata['image_name'])
+ perf_key = "{}lock_acquired_time".format(perf_key_prefix)
+ lock_acquired_time = (mgr.get_counter(
+ 'tcmu-runner', service_id, perf_key)[perf_key] or
+ [[0, 0]])[-1][1] / 1000000000
+ if lock_acquired_time > image.get('optimized_since', 0):
+ image['optimized_since'] = lock_acquired_time
+ image['stats'] = {}
+ image['stats_history'] = {}
+ for s in ['rd', 'wr', 'rd_bytes', 'wr_bytes']:
+ perf_key = "{}{}".format(perf_key_prefix, s)
+ image['stats'][s] = mgr.get_rate(
+ 'tcmu-runner', service_id, perf_key)
+ image['stats_history'][s] = mgr.get_counter(
+ 'tcmu-runner', service_id, perf_key)[perf_key]
+ else:
+ daemon['non_optimized_paths'] += 1
+ image['non_optimized_paths'].append(hostname)
+
+ return {
+ 'daemons': sorted(daemons.values(), key=lambda d: d['server_hostname']),
+ 'images': sorted(images.values(), key=lambda i: ['id']),
+ }
--- /dev/null
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "project": {
+ "name": "ceph-dashboard"
+ },
+ "apps": [
+ {
+ "root": "src",
+ "outDir": "dist",
+ "assets": [
+ "assets",
+ "favicon.ico"
+ ],
+ "index": "index.html",
+ "main": "main.ts",
+ "polyfills": "polyfills.ts",
+ "test": "test.ts",
+ "tsconfig": "tsconfig.app.json",
+ "testTsconfig": "tsconfig.spec.json",
+ "prefix": "cd",
+ "styles": [
+ "../node_modules/bootstrap/dist/css/bootstrap.css",
+ "../node_modules/ng2-toastr/bundles/ng2-toastr.min.css",
+ "../node_modules/font-awesome/css/font-awesome.css",
+ "../node_modules/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css",
+ "styles.scss"
+ ],
+ "scripts": [
+ "../node_modules/chart.js/dist/Chart.bundle.js"
+ ],
+ "environmentSource": "environments/environment.ts",
+ "environments": {
+ "dev": "environments/environment.ts",
+ "prod": "environments/environment.prod.ts"
+ }
+ }
+ ],
+ "e2e": {
+ "protractor": {
+ "config": "./protractor.conf.js"
+ }
+ },
+ "lint": [
+ {
+ "project": "src/tsconfig.app.json",
+ "exclude": "**/node_modules/**"
+ },
+ {
+ "project": "src/tsconfig.spec.json",
+ "exclude": "**/node_modules/**"
+ },
+ {
+ "project": "e2e/tsconfig.e2e.json",
+ "exclude": "**/node_modules/**"
+ }
+ ],
+ "test": {
+ "karma": {
+ "config": "./karma.conf.js"
+ }
+ },
+ "defaults": {
+ "styleExt": "scss",
+ "component": {}
+ }
+}
--- /dev/null
+# Editor configuration, see http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
--- /dev/null
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# compiled output
+/dist
+/tmp
+/out-tsc
+
+# dependencies
+/node_modules
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# misc
+/.sass-cache
+/connect.lock
+/coverage
+/libpeerconnection.log
+npm-debug.log
+testem.log
+/typings
+
+# e2e
+/e2e/*.js
+/e2e/*.map
+
+# System Files
+.DS_Store
+Thumbs.db
+
+# Package lock files
+yarn.lock
+package-lock.json
+
+# Ceph
+!core
+!*.core
--- /dev/null
+import { AppPage } from './app.po';
+
+describe('ceph-dashboard App', () => {
+ let page: AppPage;
+
+ beforeEach(() => {
+ page = new AppPage();
+ });
+
+ it('should display welcome message', () => {
+ page.navigateTo();
+ expect(page.getParagraphText()).toEqual('Welcome to oa!');
+ });
+});
--- /dev/null
+import { browser, by, element } from 'protractor';
+
+export class AppPage {
+ navigateTo() {
+ return browser.get('/');
+ }
+
+ getParagraphText() {
+ return element(by.css('oa-root h1')).getText();
+ }
+}
--- /dev/null
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/e2e",
+ "baseUrl": "./",
+ "module": "commonjs",
+ "target": "es5",
+ "types": [
+ "jasmine",
+ "jasminewd2",
+ "node"
+ ]
+ }
+}
--- /dev/null
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular/cli'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage-istanbul-reporter'),
+ require('@angular/cli/plugins/karma'),
+ require('karma-phantomjs-launcher'),
+ require('karma-junit-reporter')
+ ],
+ client:{
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageIstanbulReporter: {
+ reports: [ 'html', 'lcovonly', 'cobertura' ],
+ fixWebpackSourcePaths: true
+ },
+ angularCli: {
+ environment: 'dev'
+ },
+ reporters: ['progress', 'kjhtml', 'junit'],
+ junitReporter: {
+ 'outputFile': 'junit.frontend.xml',
+ 'suite': 'dashboard',
+ 'useBrowserName': false
+ },
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['Chrome'],
+ singleRun: false
+ });
+};
--- /dev/null
+{
+ "name": "ceph-dashboard",
+ "version": "0.0.0",
+ "license": "MIT",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve",
+ "build": "ng build",
+ "test": "ng test",
+ "lint": "ng lint",
+ "e2e": "ng e2e"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "^5.0.0",
+ "@angular/common": "^5.0.0",
+ "@angular/compiler": "^5.0.0",
+ "@angular/core": "^5.0.0",
+ "@angular/forms": "^5.0.0",
+ "@angular/http": "^5.0.0",
+ "@angular/platform-browser": "^5.0.0",
+ "@angular/platform-browser-dynamic": "^5.0.0",
+ "@angular/router": "^5.0.0",
+ "@swimlane/ngx-datatable": "^11.1.7",
+ "@types/lodash": "^4.14.95",
+ "awesome-bootstrap-checkbox": "0.3.7",
+ "bootstrap": "^3.3.7",
+ "chart.js": "^2.7.1",
+ "core-js": "^2.4.1",
+ "font-awesome": "4.7.0",
+ "lodash": "^4.17.4",
+ "moment": "2.20.1",
+ "ng2-charts": "^1.6.0",
+ "ng2-toastr": "4.1.2",
+ "ngx-bootstrap": "^2.0.1",
+ "rxjs": "^5.5.2",
+ "zone.js": "^0.8.14"
+ },
+ "devDependencies": {
+ "@angular/cli": "^1.6.5",
+ "@angular/compiler-cli": "^5.0.0",
+ "@angular/language-service": "^5.0.0",
+ "@types/jasmine": "~2.5.53",
+ "@types/jasminewd2": "~2.0.2",
+ "@types/node": "~6.0.60",
+ "codelyzer": "^4.0.1",
+ "copy-webpack-plugin": "4.3.0",
+ "jasmine-core": "~2.6.2",
+ "jasmine-spec-reporter": "~4.1.0",
+ "karma": "~1.7.0",
+ "karma-chrome-launcher": "~2.1.1",
+ "karma-cli": "~1.0.1",
+ "karma-coverage-istanbul-reporter": "^1.2.1",
+ "karma-jasmine": "~1.1.0",
+ "karma-jasmine-html-reporter": "^0.2.2",
+ "karma-junit-reporter": "^1.2.0",
+ "karma-phantomjs-launcher": "^1.0.4",
+ "node": "^8.9.4",
+ "protractor": "~5.1.2",
+ "ts-node": "~3.2.0",
+ "tslint": "~5.9.1",
+ "tslint-eslint-rules": "^4.1.1",
+ "typescript": "~2.4.2"
+ }
+}
--- /dev/null
+// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/lib/config.ts
+
+const { SpecReporter } = require('jasmine-spec-reporter');
+
+exports.config = {
+ allScriptsTimeout: 11000,
+ specs: [
+ './e2e/**/*.e2e-spec.ts'
+ ],
+ capabilities: {
+ 'browserName': 'chrome'
+ },
+ directConnect: true,
+ baseUrl: 'http://localhost:4200/',
+ framework: 'jasmine',
+ jasmineNodeOpts: {
+ showColors: true,
+ defaultTimeoutInterval: 30000,
+ print: function() {}
+ },
+ onPrepare() {
+ require('ts-node').register({
+ project: 'e2e/tsconfig.e2e.json'
+ });
+ jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
+ }
+};
--- /dev/null
+{
+ "/api/": {
+ "target": "http://localhost:8080",
+ "secure": false,
+ "logLevel": "debug"
+ }
+}
--- /dev/null
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
+import { MirroringComponent } from './ceph/block/mirroring/mirroring.component';
+import { PoolDetailComponent } from './ceph/block/pool-detail/pool-detail.component';
+import { CephfsComponent } from './ceph/cephfs/cephfs/cephfs.component';
+import { ClientsComponent } from './ceph/cephfs/clients/clients.component';
+import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
+import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
+import { MonitorComponent } from './ceph/cluster/monitor/monitor.component';
+import { OsdListComponent } from './ceph/cluster/osd/osd-list/osd-list.component';
+import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component';
+import {
+ PerformanceCounterComponent
+} from './ceph/performance-counter/performance-counter/performance-counter.component';
+import { RgwDaemonListComponent } from './ceph/rgw/rgw-daemon-list/rgw-daemon-list.component';
+import { LoginComponent } from './core/auth/login/login.component';
+import { NotFoundComponent } from './core/not-found/not-found.component';
+import { AuthGuardService } from './shared/services/auth-guard.service';
+
+const routes: Routes = [
+ { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
+ { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuardService] },
+ { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
+ { path: 'login', component: LoginComponent },
+ { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
+ {
+ path: 'rgw',
+ component: RgwDaemonListComponent,
+ canActivate: [AuthGuardService]
+ },
+ { path: 'block/iscsi', component: IscsiComponent, canActivate: [AuthGuardService] },
+ { path: 'block/pool/:name', component: PoolDetailComponent, canActivate: [AuthGuardService] },
+ {
+ path: 'perf_counters/:type/:id',
+ component: PerformanceCounterComponent,
+ canActivate: [AuthGuardService]
+ },
+ { path: 'monitor', component: MonitorComponent, canActivate: [AuthGuardService] },
+ { path: 'cephfs/:id/clients', component: ClientsComponent, canActivate: [AuthGuardService] },
+ { path: 'cephfs/:id', component: CephfsComponent, canActivate: [AuthGuardService] },
+ { path: 'configuration', component: ConfigurationComponent, canActivate: [AuthGuardService] },
+ { path: 'mirroring', component: MirroringComponent, canActivate: [AuthGuardService] },
+ { path: '404', component: NotFoundComponent },
+ { path: 'osd', component: OsdListComponent, canActivate: [AuthGuardService] },
+ { path: '**', redirectTo: '/404'}
+];
+
+@NgModule({
+ imports: [RouterModule.forRoot(routes, { useHash: true })],
+ exports: [RouterModule]
+})
+export class AppRoutingModule { }
--- /dev/null
+<cd-navigation *ngIf="!isLoginActive()"></cd-navigation>
+<div class="container-fluid"
+ [ngClass]="{'full-height':isLoginActive()}">
+ <router-outlet></router-outlet>
+</div>
--- /dev/null
+import { async, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastModule } from 'ng2-toastr';
+
+import { AppComponent } from './app.component';
+import { BlockModule } from './ceph/block/block.module';
+import { ClusterModule } from './ceph/cluster/cluster.module';
+import { CoreModule } from './core/core.module';
+import { SharedModule } from './shared/shared.module';
+
+describe('AppComponent', () => {
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule,
+ CoreModule,
+ SharedModule,
+ ToastModule.forRoot(),
+ ClusterModule,
+ BlockModule
+ ],
+ declarations: [AppComponent]
+ }).compileComponents();
+ })
+ );
+});
--- /dev/null
+import { Component, ViewContainerRef } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { ToastsManager } from 'ng2-toastr';
+
+import { AuthStorageService } from './shared/services/auth-storage.service';
+
+@Component({
+ selector: 'cd-root',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.scss']
+})
+export class AppComponent {
+ title = 'cd';
+
+ constructor(private authStorageService: AuthStorageService,
+ private router: Router,
+ public toastr: ToastsManager,
+ private vcr: ViewContainerRef) {
+ this.toastr.setRootViewContainerRef(vcr);
+ }
+
+ isLoginActive() {
+ return this.router.url === '/login' || !this.authStorageService.isLoggedIn();
+ }
+
+}
--- /dev/null
+import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { ToastModule, ToastOptions } from 'ng2-toastr/ng2-toastr';
+
+import { AccordionModule, BsDropdownModule, TabsModule } from 'ngx-bootstrap';
+import { AppRoutingModule } from './app-routing.module';
+import { AppComponent } from './app.component';
+import { CephModule } from './ceph/ceph.module';
+import { CoreModule } from './core/core.module';
+import { AuthInterceptorService } from './shared/services/auth-interceptor.service';
+import { SharedModule } from './shared/shared.module';
+
+export class CustomOption extends ToastOptions {
+ animate = 'flyRight';
+ newestOnTop = true;
+ showCloseButton = true;
+ enableHTML = true;
+}
+
+@NgModule({
+ declarations: [
+ AppComponent
+ ],
+ imports: [
+ HttpClientModule,
+ BrowserModule,
+ BrowserAnimationsModule,
+ ToastModule.forRoot(),
+ AppRoutingModule,
+ HttpClientModule,
+ CoreModule,
+ SharedModule,
+ CephModule,
+ AccordionModule.forRoot(),
+ BsDropdownModule.forRoot(),
+ TabsModule.forRoot(),
+ HttpClientModule,
+ BrowserAnimationsModule
+ ],
+ exports: [SharedModule],
+ providers: [
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: AuthInterceptorService,
+ multi: true
+ },
+ {
+ provide: ToastOptions,
+ useClass: CustomOption
+ },
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule { }
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+
+import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { ComponentsModule } from '../../shared/components/components.module';
+import { PipesModule } from '../../shared/pipes/pipes.module';
+import { ServicesModule } from '../../shared/services/services.module';
+import { SharedModule } from '../../shared/shared.module';
+import { IscsiComponent } from './iscsi/iscsi.component';
+import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
+import { MirroringComponent } from './mirroring/mirroring.component';
+import { PoolDetailComponent } from './pool-detail/pool-detail.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ TabsModule.forRoot(),
+ ProgressbarModule.forRoot(),
+ SharedModule,
+ ComponentsModule,
+ PipesModule,
+ ServicesModule
+ ],
+ declarations: [
+ PoolDetailComponent,
+ IscsiComponent,
+ MirroringComponent,
+ MirrorHealthColorPipe
+ ]
+})
+export class BlockModule { }
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li i18n
+ class="breadcrumb-item">Block</li>
+ <li i18n
+ class="breadcrumb-item active"
+ aria-current="page">iSCSI</li>
+ </ol>
+</nav>
+
+<legend i18n>Daemons</legend>
+<cd-table [data]="daemons"
+ (fetchData)="refresh()"
+ [columns]="daemonsColumns">
+</cd-table>
+
+<legend i18n>Images</legend>
+<cd-table [data]="images"
+ [columns]="imagesColumns">
+</cd-table>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AppModule } from '../../../app.module';
+import { TcmuIscsiService } from '../../../shared/services/tcmu-iscsi.service';
+import { IscsiComponent } from './iscsi.component';
+
+describe('IscsiComponent', () => {
+ let component: IscsiComponent;
+ let fixture: ComponentFixture<IscsiComponent>;
+
+ const fakeService = {
+ tcmuiscsi: () => {
+ return new Promise(function(resolve, reject) {
+ return;
+ });
+ },
+ };
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [AppModule],
+ providers: [{ provide: TcmuIscsiService, useValue: fakeService }]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+
+import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
+import { ListPipe } from '../../../shared/pipes/list.pipe';
+import { RelativeDatePipe } from '../../../shared/pipes/relative-date.pipe';
+import { TcmuIscsiService } from '../../../shared/services/tcmu-iscsi.service';
+
+@Component({
+ selector: 'cd-iscsi',
+ templateUrl: './iscsi.component.html',
+ styleUrls: ['./iscsi.component.scss']
+})
+export class IscsiComponent {
+
+ daemons = [];
+ daemonsColumns: any;
+ images = [];
+ imagesColumns: any;
+
+ constructor(private tcmuIscsiService: TcmuIscsiService,
+ cephShortVersionPipe: CephShortVersionPipe,
+ dimlessBinaryPipe: DimlessBinaryPipe,
+ dimlessPipe: DimlessPipe,
+ relativeDatePipe: RelativeDatePipe,
+ listPipe: ListPipe) {
+ this.daemonsColumns = [
+ {
+ name: 'Hostname',
+ prop: 'server_hostname'
+ },
+ {
+ name: '# Active/Optimized',
+ prop: 'optimized_paths',
+ },
+ {
+ name: '# Active/Non-Optimized',
+ prop: 'non_optimized_paths'
+ },
+ {
+ name: 'Version',
+ prop: 'version',
+ pipe: cephShortVersionPipe
+ }
+ ];
+ this.imagesColumns = [
+ {
+ name: 'Pool',
+ prop: 'pool_name'
+ },
+ {
+ name: 'Image',
+ prop: 'name'
+ },
+ {
+ name: 'Active/Optimized',
+ prop: 'optimized_paths',
+ pipe: listPipe
+ },
+ {
+ name: 'Active/Non-Optimized',
+ prop: 'non_optimized_paths',
+ pipe: listPipe
+ },
+ {
+ name: 'Read Bytes',
+ prop: 'stats.rd_bytes',
+ pipe: dimlessBinaryPipe
+ },
+ {
+ name: 'Write Bytes',
+ prop: 'stats.wr_bytes',
+ pipe: dimlessBinaryPipe
+ },
+ {
+ name: 'Read Ops',
+ prop: 'stats.rd',
+ pipe: dimlessPipe
+ },
+ {
+ name: 'Write Ops',
+ prop: 'stats.wr',
+ pipe: dimlessPipe
+ },
+ {
+ name: 'A/O Since',
+ prop: 'optimized_since',
+ pipe: relativeDatePipe
+ },
+ ];
+
+ }
+
+ refresh() {
+ this.tcmuIscsiService.tcmuiscsi().then((resp) => {
+ this.daemons = resp.daemons;
+ this.images = resp.images;
+ });
+ }
+
+}
--- /dev/null
+import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
+
+describe('MirrorHealthColorPipe', () => {
+ it('create an instance', () => {
+ const pipe = new MirrorHealthColorPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'mirrorHealthColor'
+})
+export class MirrorHealthColorPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ if (value === 'warning') {
+ return 'label label-warning';
+ } else if (value === 'error') {
+ return 'label label-danger';
+ } else if (value === 'success') {
+ return 'label label-success';
+ }
+ return 'label label-info';
+ }
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item" i18n>Block</li>
+ <li class="breadcrumb-item active"
+ aria-current="page" i18n>Mirroring</li>
+ </ol>
+</nav>
+
+<cd-view-cache [status]="status"></cd-view-cache>
+
+<div class="row">
+ <div class="col-sm-6">
+ <fieldset>
+ <legend i18n>Daemons</legend>
+
+ <cd-table [data]="daemons.data"
+ columnMode="flex"
+ [columns]="daemons.columns"
+ [autoReload]="30000"
+ (fetchData)="refresh()">
+ </cd-table>
+ </fieldset>
+ </div>
+
+ <div class="col-sm-6">
+ <fieldset>
+ <legend i18n>Pools</legend>
+
+ <cd-table [data]="pools.data"
+ columnMode="flex"
+ [autoReload]="0"
+ (fetchData)="refresh()"
+ [columns]="pools.columns">
+ </cd-table>
+ </fieldset>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col-md-12">
+ <fieldset>
+ <legend i18n>Images</legend>
+ <tabset>
+ <tab heading="Issues" i18n-heading>
+ <cd-table [data]="image_error.data"
+ columnMode="flex"
+ [autoReload]="0"
+ (fetchData)="refresh()"
+ [columns]="image_error.columns">
+ </cd-table>
+ </tab>
+ <tab heading="Syncing" i18n-heading>
+ <cd-table [data]="image_syncing.data"
+ columnMode="flex"
+ [autoReload]="0"
+ (fetchData)="refresh()"
+ [columns]="image_syncing.columns">
+ </cd-table>
+ </tab>
+ <tab heading="Ready" i18n-heading>
+ <cd-table [data]="image_ready.data"
+ columnMode="flex"
+ [autoReload]="0"
+ (fetchData)="refresh()"
+ [columns]="image_ready.columns">
+ </cd-table>
+ </tab>
+ </tabset>
+ </fieldset>
+ </div>
+</div>
+
+<ng-template #healthTmpl
+ let-row="row"
+ let-value="value">
+ <span [ngClass]="row.health_color | mirrorHealthColor">{{ value }}</span>
+</ng-template>
+
+<ng-template #stateTmpl
+ let-row="row"
+ let-value="value">
+ <span [ngClass]="row.state_color | mirrorHealthColor">{{ value }}</span>
+</ng-template>
+
+<ng-template #syncTmpl>
+ <span class="label label-info">Syncing</span>
+</ng-template>
+
+<ng-template #progressTmpl
+ let-value="value">
+ <progressbar type="info"
+ [value]="value">
+ </progressbar>
+</ng-template>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BsDropdownModule, TabsModule } from 'ngx-bootstrap';
+import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
+import { Observable } from 'rxjs/Observable';
+
+import { RbdMirroringService } from '../../../shared/services/rbd-mirroring.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { MirrorHealthColorPipe } from '../mirror-health-color.pipe';
+import { MirroringComponent } from './mirroring.component';
+
+describe('MirroringComponent', () => {
+ let component: MirroringComponent;
+ let fixture: ComponentFixture<MirroringComponent>;
+
+ const fakeService = {
+ get: (service_type: string, service_id: string) => {
+ return Observable.create(observer => {
+ return () => console.log('disposed');
+ });
+ }
+ };
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ declarations: [MirroringComponent, MirrorHealthColorPipe],
+ imports: [
+ SharedModule,
+ BsDropdownModule.forRoot(),
+ TabsModule.forRoot(),
+ ProgressbarModule.forRoot(),
+ HttpClientTestingModule
+ ],
+ providers: [{ provide: RbdMirroringService, useValue: fakeService }]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MirroringComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
+import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
+import { RbdMirroringService } from '../../../shared/services/rbd-mirroring.service';
+
+@Component({
+ selector: 'cd-mirroring',
+ templateUrl: './mirroring.component.html',
+ styleUrls: ['./mirroring.component.scss']
+})
+export class MirroringComponent implements OnInit {
+ @ViewChild('healthTmpl') healthTmpl: TemplateRef<any>;
+ @ViewChild('stateTmpl') stateTmpl: TemplateRef<any>;
+ @ViewChild('syncTmpl') syncTmpl: TemplateRef<any>;
+ @ViewChild('progressTmpl') progressTmpl: TemplateRef<any>;
+
+ contentData: any;
+
+ status: ViewCacheStatus;
+ daemons = {
+ data: [],
+ columns: []
+ };
+ pools = {
+ data: [],
+ columns: {}
+ };
+ image_error = {
+ data: [],
+ columns: {}
+ };
+ image_syncing = {
+ data: [],
+ columns: {}
+ };
+ image_ready = {
+ data: [],
+ columns: {}
+ };
+
+ constructor(
+ private http: HttpClient,
+ private rbdMirroringService: RbdMirroringService,
+ private cephShortVersionPipe: CephShortVersionPipe
+ ) { }
+
+ ngOnInit() {
+ this.daemons.columns = [
+ { prop: 'instance_id', name: 'Instance', flexGrow: 2 },
+ { prop: 'id', name: 'ID', flexGrow: 2 },
+ { prop: 'server_hostname', name: 'Hostname', flexGrow: 2 },
+ {
+ prop: 'server_hostname',
+ name: 'Version',
+ pipe: this.cephShortVersionPipe,
+ flexGrow: 2
+ },
+ {
+ prop: 'health',
+ name: 'Health',
+ cellTemplate: this.healthTmpl,
+ flexGrow: 1
+ }
+ ];
+
+ this.pools.columns = [
+ { prop: 'name', name: 'Name', flexGrow: 2 },
+ { prop: 'mirror_mode', name: 'Mode', flexGrow: 2 },
+ { prop: 'leader_id', name: 'Leader', flexGrow: 2 },
+ { prop: 'image_local_count', name: '# Local', flexGrow: 2 },
+ { prop: 'image_remote_count', name: '# Remote', flexGrow: 2 },
+ {
+ prop: 'health',
+ name: 'Health',
+ cellTemplate: this.healthTmpl,
+ flexGrow: 1
+ }
+ ];
+
+ this.image_error.columns = [
+ { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
+ { prop: 'name', name: 'Image', flexGrow: 2 },
+ { prop: 'description', name: 'Issue', flexGrow: 4 },
+ {
+ prop: 'state',
+ name: 'State',
+ cellTemplate: this.stateTmpl,
+ flexGrow: 1
+ }
+ ];
+
+ this.image_syncing.columns = [
+ { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
+ { prop: 'name', name: 'Image', flexGrow: 2 },
+ {
+ prop: 'progress',
+ name: 'Progress',
+ cellTemplate: this.progressTmpl,
+ flexGrow: 2
+ },
+ {
+ prop: 'state',
+ name: 'State',
+ cellTemplate: this.syncTmpl,
+ flexGrow: 1
+ }
+ ];
+
+ this.image_ready.columns = [
+ { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
+ { prop: 'name', name: 'Image', flexGrow: 2 },
+ { prop: 'description', name: 'Description', flexGrow: 4 },
+ {
+ prop: 'state',
+ name: 'State',
+ cellTemplate: this.stateTmpl,
+ flexGrow: 1
+ }
+ ];
+ }
+
+ refresh() {
+ this.rbdMirroringService.get().subscribe((data: any) => {
+ this.daemons.data = data.content_data.daemons;
+ this.pools.data = data.content_data.pools;
+ this.image_error.data = data.content_data.image_error;
+ this.image_syncing.data = data.content_data.image_syncing;
+ this.image_ready.data = data.content_data.image_ready;
+
+ this.status = data.status;
+ });
+ }
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li i18n
+ class="breadcrumb-item">Block</li>
+ <li i18n
+ class="breadcrumb-item">Pools</li>
+ <li class="breadcrumb-item active"
+ aria-current="page">{{ name }}</li>
+ </ol>
+</nav>
+
+<cd-view-cache [status]="viewCacheStatus"></cd-view-cache>
+
+<cd-table [data]="images"
+ columnMode="flex"
+ [columns]="columns"
+ (fetchData)="loadImages()">
+</cd-table>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { AlertModule, BsDropdownModule, TabsModule } from 'ngx-bootstrap';
+
+import { ComponentsModule } from '../../../shared/components/components.module';
+import { SharedModule } from '../../../shared/shared.module';
+import { PoolDetailComponent } from './pool-detail.component';
+
+describe('PoolDetailComponent', () => {
+ let component: PoolDetailComponent;
+ let fixture: ComponentFixture<PoolDetailComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SharedModule,
+ BsDropdownModule.forRoot(),
+ TabsModule.forRoot(),
+ AlertModule.forRoot(),
+ ComponentsModule,
+ RouterTestingModule,
+ HttpClientTestingModule
+ ],
+ declarations: [ PoolDetailComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PoolDetailComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+
+import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
+import { PoolService } from '../../../shared/services/pool.service';
+
+@Component({
+ selector: 'cd-pool-detail',
+ templateUrl: './pool-detail.component.html',
+ styleUrls: ['./pool-detail.component.scss']
+})
+export class PoolDetailComponent implements OnInit, OnDestroy {
+ name: string;
+ images: any;
+ columns: CdTableColumn[];
+ retries: number;
+ routeParamsSubscribe: any;
+ viewCacheStatus: ViewCacheStatus;
+
+ constructor(
+ private route: ActivatedRoute,
+ private poolService: PoolService,
+ dimlessBinaryPipe: DimlessBinaryPipe,
+ dimlessPipe: DimlessPipe
+ ) {
+ this.columns = [
+ {
+ name: 'Name',
+ prop: 'name',
+ flexGrow: 2
+ },
+ {
+ name: 'Size',
+ prop: 'size',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ pipe: dimlessBinaryPipe
+ },
+ {
+ name: 'Objects',
+ prop: 'num_objs',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ pipe: dimlessPipe
+ },
+ {
+ name: 'Object size',
+ prop: 'obj_size',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ pipe: dimlessBinaryPipe
+ },
+ {
+ name: 'Features',
+ prop: 'features_name',
+ flexGrow: 3
+ },
+ {
+ name: 'Parent',
+ prop: 'parent',
+ flexGrow: 2
+ }
+ ];
+ }
+
+ ngOnInit() {
+ this.routeParamsSubscribe = this.route.params.subscribe((params: { name: string }) => {
+ this.name = params.name;
+ this.images = [];
+ this.retries = 0;
+ });
+ }
+
+ ngOnDestroy() {
+ this.routeParamsSubscribe.unsubscribe();
+ }
+
+ loadImages() {
+ this.poolService.rbdPoolImages(this.name).then(
+ resp => {
+ this.viewCacheStatus = resp.status;
+ this.images = resp.value;
+ },
+ () => {
+ this.viewCacheStatus = ViewCacheStatus.ValueException;
+ }
+ );
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { SharedModule } from '../shared/shared.module';
+import { BlockModule } from './block/block.module';
+import { CephfsModule } from './cephfs/cephfs.module';
+import { ClusterModule } from './cluster/cluster.module';
+import { DashboardModule } from './dashboard/dashboard.module';
+import { PerformanceCounterModule } from './performance-counter/performance-counter.module';
+import { RgwModule } from './rgw/rgw.module';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ ClusterModule,
+ DashboardModule,
+ RgwModule,
+ PerformanceCounterModule,
+ BlockModule,
+ CephfsModule,
+ SharedModule
+ ],
+ declarations: []
+})
+export class CephModule { }
--- /dev/null
+<div class="chart-container">
+ <canvas baseChart
+ #chartCanvas
+ [datasets]="chart?.datasets"
+ [options]="chart?.options"
+ [chartType]="chart?.chartType">
+ </canvas>
+ <div class="chartjs-tooltip"
+ #chartTooltip>
+ <table></table>
+ </div>
+</div>
--- /dev/null
+@import '../../../../styles/chart-tooltip.scss';
+
+.chart-container {
+ height: 500px;
+ width: 100%;
+}
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ChartsModule } from 'ng2-charts/ng2-charts';
+
+import { CephfsChartComponent } from './cephfs-chart.component';
+
+describe('CephfsChartComponent', () => {
+ let component: CephfsChartComponent;
+ let fixture: ComponentFixture<CephfsChartComponent>;
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [ChartsModule],
+ declarations: [CephfsChartComponent]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsChartComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, ElementRef, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
+
+import * as _ from 'lodash';
+import * as moment from 'moment';
+
+import { ChartTooltip } from '../../../shared/models/chart-tooltip';
+
+@Component({
+ selector: 'cd-cephfs-chart',
+ templateUrl: './cephfs-chart.component.html',
+ styleUrls: ['./cephfs-chart.component.scss']
+})
+export class CephfsChartComponent implements OnChanges, OnInit {
+ @ViewChild('chartCanvas') chartCanvas: ElementRef;
+ @ViewChild('chartTooltip') chartTooltip: ElementRef;
+
+ @Input() mdsCounter: any;
+
+ lhsCounter = 'mds.inodes';
+ rhsCounter = 'mds_server.handle_client_request';
+
+ chart: any;
+
+ constructor() {}
+
+ ngOnInit() {
+ if (_.isUndefined(this.mdsCounter)) {
+ return;
+ }
+
+ const getTitle = title => {
+ return moment(title).format('LTS');
+ };
+
+ const getStyleTop = tooltip => {
+ return tooltip.caretY - tooltip.height - 15 + 'px';
+ };
+
+ const getStyleLeft = tooltip => {
+ return tooltip.caretX + 'px';
+ };
+
+ const chartTooltip = new ChartTooltip(
+ this.chartCanvas,
+ this.chartTooltip,
+ getStyleLeft,
+ getStyleTop
+ );
+ chartTooltip.getTitle = getTitle;
+ chartTooltip.checkOffset = true;
+
+ const lhsData = this.convert_timeseries(this.mdsCounter[this.lhsCounter]);
+ const rhsData = this.delta_timeseries(this.mdsCounter[this.rhsCounter]);
+
+ this.chart = {
+ datasets: [
+ {
+ label: this.lhsCounter,
+ yAxisID: 'LHS',
+ data: lhsData,
+ tension: 0.1
+ },
+ {
+ label: this.rhsCounter,
+ yAxisID: 'RHS',
+ data: rhsData,
+ tension: 0.1
+ }
+ ],
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ legend: {
+ position: 'top'
+ },
+ scales: {
+ xAxes: [
+ {
+ position: 'top',
+ type: 'time',
+ time: {
+ displayFormats: {
+ quarter: 'MMM YYYY'
+ }
+ }
+ }
+ ],
+ yAxes: [
+ {
+ id: 'LHS',
+ type: 'linear',
+ position: 'left',
+ min: 0
+ },
+ {
+ id: 'RHS',
+ type: 'linear',
+ position: 'right',
+ min: 0
+ }
+ ]
+ },
+ tooltips: {
+ enabled: false,
+ mode: 'index',
+ intersect: false,
+ position: 'nearest',
+ custom: tooltip => {
+ chartTooltip.customTooltips(tooltip);
+ }
+ }
+ },
+ chartType: 'line'
+ };
+ }
+
+ ngOnChanges() {
+ if (!this.chart) {
+ return;
+ }
+
+ const lhsData = this.convert_timeseries(this.mdsCounter[this.lhsCounter]);
+ const rhsData = this.delta_timeseries(this.mdsCounter[this.rhsCounter]);
+
+ this.chart.datasets[0].data = lhsData;
+ this.chart.datasets[1].data = rhsData;
+ }
+
+ // Convert ceph-mgr's time series format (list of 2-tuples
+ // with seconds-since-epoch timestamps) into what chart.js
+ // can handle (list of objects with millisecs-since-epoch
+ // timestamps)
+ convert_timeseries(sourceSeries) {
+ const data = [];
+ _.each(sourceSeries, dp => {
+ data.push({
+ x: dp[0] * 1000,
+ y: dp[1]
+ });
+ });
+
+ return data;
+ }
+
+ delta_timeseries(sourceSeries) {
+ let i;
+ let prev = sourceSeries[0];
+ const result = [];
+ for (i = 1; i < sourceSeries.length; i++) {
+ const cur = sourceSeries[i];
+ const tdelta = cur[0] - prev[0];
+ const vdelta = cur[1] - prev[1];
+ const rate = vdelta / tdelta;
+
+ result.push({
+ x: cur[0] * 1000,
+ y: rate
+ });
+
+ prev = cur;
+ }
+ return result;
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { ChartsModule } from 'ng2-charts/ng2-charts';
+import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
+
+import { AppRoutingModule } from '../../app-routing.module';
+import { SharedModule } from '../../shared/shared.module';
+import { CephfsChartComponent } from './cephfs-chart/cephfs-chart.component';
+import { CephfsService } from './cephfs.service';
+import { CephfsComponent } from './cephfs/cephfs.component';
+import { ClientsComponent } from './clients/clients.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ SharedModule,
+ AppRoutingModule,
+ ChartsModule,
+ ProgressbarModule.forRoot()
+ ],
+ declarations: [CephfsComponent, ClientsComponent, CephfsChartComponent],
+ providers: [CephfsService]
+})
+export class CephfsModule {}
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { CephfsService } from './cephfs.service';
+
+describe('CephfsService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [HttpClientModule],
+ providers: [CephfsService]
+ });
+ });
+
+ it(
+ 'should be created',
+ inject([CephfsService], (service: CephfsService) => {
+ expect(service).toBeTruthy();
+ })
+ );
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class CephfsService {
+ baseURL = 'api/cephfs';
+
+ constructor(private http: HttpClient) {}
+
+ getCephfs(id) {
+ return this.http.get(`${this.baseURL}/data/${id}`);
+ }
+
+ getClients(id) {
+ return this.http.get(`${this.baseURL}/clients/${id}`);
+ }
+
+ getMdsCounters(id) {
+ return this.http.get(`${this.baseURL}/mds_counters/${id}`);
+ }
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li i18n
+ class="breadcrumb-item">Filesystem</li>
+ <li class="breadcrumb-item active"
+ aria-current="page">{{ name }}</li>
+ </ol>
+</nav>
+
+<div class="row">
+ <div class="col-md-12">
+ <i class="fa fa-desktop"></i>
+ <a i18n
+ [routerLink]="['/cephfs/' + id + '/clients']">
+ <span style="font-weight:bold;">{{ clientCount }}</span>
+ Clients
+ </a>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col-sm-6">
+ <fieldset>
+ <legend i18n>Ranks</legend>
+
+ <cd-table [data]="ranks.data"
+ [columns]="ranks.columns"
+ (fetchData)="refresh()"
+ [toolHeader]="false">
+ </cd-table>
+ </fieldset>
+
+ <cd-table-key-value [data]="standbys">
+ </cd-table-key-value>
+ </div>
+
+ <div class="col-sm-6">
+ <fieldset>
+ <legend i18n>Pools</legend>
+
+ <cd-table [data]="pools.data"
+ [columns]="pools.columns"
+ [toolHeader]="false">
+ </cd-table>
+
+ </fieldset>
+ </div>
+</div>
+
+<div class="row"
+ *ngFor="let mdsCounter of objectValues(mdsCounters); trackBy: trackByFn">
+ <div class="cold-md-12">
+ <cd-cephfs-chart [mdsCounter]="mdsCounter"></cd-cephfs-chart>
+ </div>
+</div>
+
+<!-- templates -->
+<ng-template #poolProgressTmpl
+ let-row="row">
+ <progressbar type="danger"
+ [value]="row.used * 100.0 / row.avail">
+ </progressbar>
+</ng-template>
+
+<ng-template #activityTmpl
+ let-row="row"
+ let-value="value">
+ {{ row.state === 'standby-replay' ? 'Evts' : 'Reqs' }}: {{ value | dimless }} /s
+</ng-template>
--- /dev/null
+.progress {
+ margin-bottom: 0px;
+}
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ChartsModule } from 'ng2-charts/ng2-charts';
+import { BsDropdownModule, ProgressbarModule } from 'ngx-bootstrap';
+import { Observable } from 'rxjs/Observable';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { CephfsChartComponent } from '../cephfs-chart/cephfs-chart.component';
+import { CephfsService } from '../cephfs.service';
+import { CephfsComponent } from './cephfs.component';
+
+describe('CephfsComponent', () => {
+ let component: CephfsComponent;
+ let fixture: ComponentFixture<CephfsComponent>;
+
+ const fakeFilesystemService = {
+ getCephfs: id => {
+ return Observable.create(observer => {
+ return () => console.log('disposed');
+ });
+ },
+ getMdsCounters: id => {
+ return Observable.create(observer => {
+ return () => console.log('disposed');
+ });
+ }
+ };
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SharedModule,
+ ChartsModule,
+ RouterTestingModule,
+ BsDropdownModule.forRoot(),
+ ProgressbarModule.forRoot()
+ ],
+ declarations: [CephfsComponent, CephfsChartComponent],
+ providers: [
+ { provide: CephfsService, useValue: fakeFilesystemService }
+ ]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+
+import * as _ from 'lodash';
+import { Subscription } from 'rxjs/Subscription';
+
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
+import { CephfsService } from '../cephfs.service';
+
+@Component({
+ selector: 'cd-cephfs',
+ templateUrl: './cephfs.component.html',
+ styleUrls: ['./cephfs.component.scss']
+})
+export class CephfsComponent implements OnInit, OnDestroy {
+ @ViewChild('poolProgressTmpl') poolProgressTmpl: TemplateRef<any>;
+ @ViewChild('activityTmpl') activityTmpl: TemplateRef<any>;
+
+ routeParamsSubscribe: Subscription;
+
+ objectValues = Object.values;
+
+ id: number;
+ name: string;
+ ranks: any;
+ pools: any;
+ standbys = [];
+ clientCount: number;
+
+ mdsCounters = {};
+
+ constructor(
+ private route: ActivatedRoute,
+ private cephfsService: CephfsService,
+ private dimlessBinary: DimlessBinaryPipe,
+ private dimless: DimlessPipe
+ ) {}
+
+ ngOnInit() {
+ this.ranks = {
+ columns: [
+ { prop: 'rank' },
+ { prop: 'state' },
+ { prop: 'mds', name: 'Daemon' },
+ { prop: 'activity', cellTemplate: this.activityTmpl },
+ { prop: 'dns', name: 'Dentries', pipe: this.dimless },
+ { prop: 'inos', name: 'Inodes', pipe: this.dimless }
+ ],
+ data: []
+ };
+
+ this.pools = {
+ columns: [
+ { prop: 'pool' },
+ { prop: 'type' },
+ { prop: 'used', pipe: this.dimlessBinary },
+ { prop: 'avail', pipe: this.dimlessBinary },
+ {
+ name: 'Usage',
+ cellTemplate: this.poolProgressTmpl,
+ comparator: (valueA, valueB, rowA, rowB, sortDirection) => {
+ const valA = rowA.used / rowA.avail;
+ const valB = rowB.used / rowB.avail;
+
+ if (valA === valB) {
+ return 0;
+ }
+
+ if (valA > valB) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+ }
+ ],
+ data: []
+ };
+
+ this.routeParamsSubscribe = this.route.params.subscribe((params: { id: number }) => {
+ this.id = params.id;
+
+ this.ranks.data = [];
+ this.pools.data = [];
+ this.standbys = [];
+ this.mdsCounters = {};
+ });
+ }
+
+ ngOnDestroy() {
+ this.routeParamsSubscribe.unsubscribe();
+ }
+
+ refresh() {
+ this.cephfsService.getCephfs(this.id).subscribe((data: any) => {
+ this.ranks.data = data.cephfs.ranks;
+ this.pools.data = data.cephfs.pools;
+ this.standbys = [
+ {
+ key: 'Standby daemons',
+ value: data.standbys.map(value => value.name).join(', ')
+ }
+ ];
+ this.name = data.cephfs.name;
+ this.clientCount = data.cephfs.client_count;
+ });
+
+ this.cephfsService.getMdsCounters(this.id).subscribe(data => {
+ _.each(this.mdsCounters, (value, key) => {
+ if (data[key] === undefined) {
+ delete this.mdsCounters[key];
+ }
+ });
+
+ _.each(data, (mdsData: any, mdsName) => {
+ mdsData.name = mdsName;
+ this.mdsCounters[mdsName] = mdsData;
+ });
+ });
+ }
+
+ trackByFn(index, item) {
+ return item.name;
+ }
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li i18n
+ class="breadcrumb-item">Filesystem</li>
+ <li class="breadcrumb-item">
+ <a [routerLink]="['/cephfs/' + id]">{{ name }}</a>
+ </li>
+ <li i18n
+ class="breadcrumb-item active"
+ aria-current="page">Clients</li>
+ </ol>
+</nav>
+
+<fieldset>
+ <cd-view-cache [status]="viewCacheStatus"></cd-view-cache>
+
+ <cd-table [data]="clients.data"
+ [columns]="clients.columns"
+ (fetchData)="refresh()"
+ [header]="false">
+ </cd-table>
+</fieldset>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+import { Observable } from 'rxjs/Observable';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { CephfsService } from '../cephfs.service';
+import { ClientsComponent } from './clients.component';
+
+describe('ClientsComponent', () => {
+ let component: ClientsComponent;
+ let fixture: ComponentFixture<ClientsComponent>;
+
+ const fakeFilesystemService = {
+ getCephfs: id => {
+ return Observable.create(observer => {
+ return () => console.log('disposed');
+ });
+ },
+ getClients: id => {
+ return Observable.create(observer => {
+ return () => console.log('disposed');
+ });
+ }
+ };
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ RouterTestingModule,
+ BsDropdownModule.forRoot(),
+ SharedModule
+ ],
+ declarations: [ClientsComponent],
+ providers: [{ provide: CephfsService, useValue: fakeFilesystemService }]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ClientsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+
+import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
+import { CephfsService } from '../cephfs.service';
+
+@Component({
+ selector: 'cd-clients',
+ templateUrl: './clients.component.html',
+ styleUrls: ['./clients.component.scss']
+})
+export class ClientsComponent implements OnInit, OnDestroy {
+ routeParamsSubscribe: any;
+
+ id: number;
+ name: string;
+ clients: any;
+ viewCacheStatus: ViewCacheStatus;
+
+ constructor(private route: ActivatedRoute, private cephfsService: CephfsService) {}
+
+ ngOnInit() {
+ this.clients = {
+ columns: [
+ { prop: 'id' },
+ { prop: 'type' },
+ { prop: 'state' },
+ { prop: 'version' },
+ { prop: 'hostname', name: 'Host' },
+ { prop: 'root' }
+ ],
+ data: []
+ };
+
+ this.routeParamsSubscribe = this.route.params.subscribe((params: { id: number }) => {
+ this.id = params.id;
+ this.clients.data = [];
+ this.viewCacheStatus = ViewCacheStatus.ValueNone;
+
+ this.cephfsService.getCephfs(this.id).subscribe((data: any) => {
+ this.name = data.cephfs.name;
+ });
+ });
+ }
+
+ ngOnDestroy() {
+ this.routeParamsSubscribe.unsubscribe();
+ }
+
+ refresh() {
+ this.cephfsService.getClients(this.id).subscribe((data: any) => {
+ this.viewCacheStatus = data.status;
+ this.clients.data = data.data;
+ });
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { ComponentsModule } from '../../shared/components/components.module';
+import { SharedModule } from '../../shared/shared.module';
+import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
+import { ConfigurationComponent } from './configuration/configuration.component';
+import { HostsComponent } from './hosts/hosts.component';
+import { MonitorService } from './monitor.service';
+import { MonitorComponent } from './monitor/monitor.component';
+import { OsdDetailsComponent } from './osd/osd-details/osd-details.component';
+import { OsdListComponent } from './osd/osd-list/osd-list.component';
+import {
+ OsdPerformanceHistogramComponent
+} from './osd/osd-performance-histogram/osd-performance-histogram.component';
+import { OsdService } from './osd/osd.service';
+
+@NgModule({
+ entryComponents: [
+ OsdDetailsComponent
+ ],
+ imports: [
+ CommonModule,
+ PerformanceCounterModule,
+ ComponentsModule,
+ TabsModule.forRoot(),
+ SharedModule,
+ RouterModule,
+ FormsModule
+ ],
+ declarations: [
+ HostsComponent,
+ MonitorComponent,
+ ConfigurationComponent,
+ OsdListComponent,
+ OsdDetailsComponent,
+ OsdPerformanceHistogramComponent
+ ],
+ providers: [
+ MonitorService,
+ OsdService
+ ]
+})
+export class ClusterModule {}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item">Cluster</li>
+ <li class="breadcrumb-item active"
+ aria-current="page">Configuration Documentation</li>
+ </ol>
+</nav>
+
+<div class="dataTables_wrapper">
+ <div class="dataTables_header clearfix form-inline">
+ <!-- filters -->
+ <div class="form-group pull-right filter"
+ *ngFor="let filter of filters">
+ <label>{{ filter.label }}: </label>
+ <select class="form-control input-sm"
+ [(ngModel)]="filter.value"
+ (ngModelChange)="updateFilter()">
+ <option *ngFor="let opt of filter.options">{{ opt }}</option>
+ </select>
+ </div>
+ <!-- end filters -->
+ </div>
+
+ <table class="oadatatable table table-striped table-condensed table-bordered table-hover">
+ <thead class="datatable-header">
+ <tr>
+ <th >Name</th>
+ <th style="width:400px;">Description</th>
+ <th>Type</th>
+ <th>Level</th>
+ <th style="width: 200px">Default</th>
+ <th>Tags</th>
+ <th>Services</th>
+ <th>See_also</th>
+ <th>Max</th>
+ <th>Min</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr *ngFor="let row of data | filter:filters">
+ <td >{{ row.name }}</td>
+ <td>
+ <p>
+ {{ row.desc }}</p>
+ <p *ngIf="row.long_desc"
+ class=text-muted>{{ row.long_desc }}</p>
+ </td>
+ <td>{{ row.type }}</td>
+ <td>{{ row.level }}</td>
+ <td class="wrap">
+ {{ row.default }} {{ row.daemon_default }}
+ </td>
+ <td>
+ <p *ngFor="let item of row.tags">{{ item }}</p>
+ </td>
+ <td>
+ <p *ngFor="let item of row.services">{{ item }}</p>
+ </td>
+ <td class="wrap">
+ <p *ngFor="let item of row.see_also">{{ item }}</p>
+ </td>
+ <td>{{ row.max }}</td>
+ <td>{{ row.min }}</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
--- /dev/null
+@import '../../../shared/datatable/table/table.component.scss';
+
+td.wrap {
+ word-break: break-all;
+}
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+
+import { Observable } from 'rxjs/Observable';
+
+import { ConfigurationService } from '../../../shared/services/configuration.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { ConfigurationComponent } from './configuration.component';
+
+describe('ConfigurationComponent', () => {
+ let component: ConfigurationComponent;
+ let fixture: ComponentFixture<ConfigurationComponent>;
+
+ const fakeService = {
+ getConfigData: () => {
+ return Observable.create(observer => {
+ return () => console.log('disposed');
+ });
+ }
+ };
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ConfigurationComponent],
+ providers: [{ provide: ConfigurationService, useValue: fakeService }],
+ imports: [SharedModule, FormsModule]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigurationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { ConfigurationService } from '../../../shared/services/configuration.service';
+
+@Component({
+ selector: 'cd-configuration',
+ templateUrl: './configuration.component.html',
+ styleUrls: ['./configuration.component.scss']
+})
+export class ConfigurationComponent implements OnInit {
+ @ViewChild('arrayTmpl') arrayTmpl: TemplateRef<any>;
+
+ data = [];
+ columns: any;
+
+ filters = [
+ {
+ label: 'Level',
+ prop: 'level',
+ value: 'basic',
+ options: ['basic', 'advanced', 'dev'],
+ applyFilter: (row, value) => {
+ enum Level {
+ basic = 0,
+ advanced = 1,
+ dev = 2
+ }
+
+ const levelVal = Level[value];
+
+ return Level[row.level] <= levelVal;
+ }
+ },
+ {
+ label: 'Service',
+ prop: 'services',
+ value: 'any',
+ options: ['mon', 'mgr', 'osd', 'mds', 'common', 'mds_client', 'rgw', 'any'],
+ applyFilter: (row, value) => {
+ if (value === 'any') {
+ return true;
+ }
+
+ return row.services.includes(value);
+ }
+ }
+ ];
+
+ constructor(private configurationService: ConfigurationService) {}
+
+ ngOnInit() {
+ this.columns = [
+ { flexGrow: 2, canAutoResize: true, prop: 'name' },
+ { flexGrow: 2, prop: 'desc', name: 'Description' },
+ { flexGrow: 2, prop: 'long_desc', name: 'Long description' },
+ { flexGrow: 1, prop: 'type' },
+ { flexGrow: 1, prop: 'level' },
+ { flexGrow: 1, prop: 'default' },
+ { flexGrow: 2, prop: 'daemon_default', name: 'Daemon default' },
+ { flexGrow: 1, prop: 'tags', name: 'Tags', cellTemplate: this.arrayTmpl },
+ { flexGrow: 1, prop: 'services', name: 'Services', cellTemplate: this.arrayTmpl },
+ { flexGrow: 1, prop: 'see_also', name: 'See_also', cellTemplate: this.arrayTmpl },
+ { flexGrow: 1, prop: 'max', name: 'Max' },
+ { flexGrow: 1, prop: 'min', name: 'Min' }
+ ];
+
+ this.fetchData();
+ }
+
+ fetchData() {
+ this.configurationService.getConfigData().subscribe((data: any) => {
+ this.data = data;
+ });
+ }
+
+ updateFilter() {
+ this.data = [...this.data];
+ }
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li i18n
+ class="breadcrumb-item">Cluster</li>
+ <li i18n
+ class="breadcrumb-item active"
+ aria-current="page">Hosts</li>
+ </ol>
+</nav>
+<cd-table [data]="hosts"
+ [columns]="columns"
+ columnMode="flex"
+ (fetchData)="getHosts()">
+ <ng-template #servicesTpl let-value="value">
+ <span *ngFor="let service of value; last as isLast">
+ <a [routerLink]="[service.cdLink]">{{ service.type }}.{{ service.id }}</a>{{ !isLast ? ", " : "" }}
+ </span>
+ </ng-template>
+</cd-table>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+
+import { ComponentsModule } from '../../../shared/components/components.module';
+import { SharedModule } from '../../../shared/shared.module';
+import { HostsComponent } from './hosts.component';
+
+describe('HostsComponent', () => {
+ let component: HostsComponent;
+ let fixture: ComponentFixture<HostsComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SharedModule,
+ HttpClientTestingModule,
+ ComponentsModule,
+ BsDropdownModule.forRoot(),
+ RouterTestingModule
+ ],
+ declarations: [
+ HostsComponent
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HostsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
+import { HostService } from '../../../shared/services/host.service';
+
+@Component({
+ selector: 'cd-hosts',
+ templateUrl: './hosts.component.html',
+ styleUrls: ['./hosts.component.scss']
+})
+export class HostsComponent implements OnInit {
+
+ columns: Array<CdTableColumn> = [];
+ hosts: Array<object> = [];
+ isLoadingHosts = false;
+
+ @ViewChild('servicesTpl') public servicesTpl: TemplateRef<any>;
+
+ constructor(private hostService: HostService,
+ private cephShortVersionPipe: CephShortVersionPipe) { }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: 'Hostname',
+ prop: 'hostname',
+ flexGrow: 1
+ },
+ {
+ name: 'Services',
+ prop: 'services',
+ flexGrow: 3,
+ cellTemplate: this.servicesTpl
+ },
+ {
+ name: 'Version',
+ prop: 'ceph_version',
+ flexGrow: 1,
+ pipe: this.cephShortVersionPipe
+ }
+ ];
+ }
+
+ getHosts() {
+ if (this.isLoadingHosts) {
+ return;
+ }
+ this.isLoadingHosts = true;
+ this.hostService.list().then((resp) => {
+ resp.map((host) => {
+ host.services.map((service) => {
+ service.cdLink = `/perf_counters/${service.type}/${service.id}`;
+ return service;
+ });
+ return host;
+ });
+ this.hosts = resp;
+ this.isLoadingHosts = false;
+ }).catch(() => {
+ this.isLoadingHosts = false;
+ });
+ }
+}
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import {
+ HttpClientTestingModule,
+ HttpTestingController
+} from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { MonitorService } from './monitor.service';
+
+describe('MonitorService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [MonitorService],
+ imports: [HttpClientTestingModule, HttpClientModule]
+ });
+ });
+
+ it('should be created', inject([MonitorService], (service: MonitorService) => {
+ expect(service).toBeTruthy();
+ }));
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class MonitorService {
+ constructor(private http: HttpClient) {}
+
+ getMonitor() {
+ return this.http.get('api/monitor');
+ }
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li i18n
+ class="breadcrumb-item">Cluster</li>
+ <li i18n
+ class="breadcrumb-item active"
+ aria-current="page">Monitors</li>
+ </ol>
+</nav>
+
+<div class="row">
+ <div class="col-md-4">
+ <fieldset>
+ <legend i18n>Status</legend>
+ <table class="table table-striped"
+ *ngIf="mon_status">
+ <tr>
+ <td i18n
+ class="bold">Cluster ID</td>
+ <td>{{ mon_status.monmap.fsid }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">monmap modified</td>
+ <td>{{ mon_status.monmap.modified }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">monmap epoch</td>
+ <td>{{ mon_status.monmap.epoch }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">quorum con</td>
+ <td>{{ mon_status.features.quorum_con }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">quorum mon</td>
+ <td>{{ mon_status.features.quorum_mon }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">required con</td>
+ <td>{{ mon_status.features.required_con }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">required mon</td>
+ <td>{{ mon_status.features.required_mon }}</td>
+ </tr>
+ </table>
+ </fieldset>
+ </div>
+
+ <div class="col-md-8">
+ <fieldset>
+ <legend i18n
+ class="in-quorum">In Quorum</legend>
+ <cd-table [data]="inQuorum.data"
+ [columns]="inQuorum.columns">
+ </cd-table>
+
+ <legend i18n
+ class="in-quorum">Not In Quorum</legend>
+ <cd-table [data]="notInQuorum.data"
+ (fetchData)="refresh()"
+ [columns]="notInQuorum.columns">
+ </cd-table>
+ </fieldset>
+ </div>
+</div>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AppModule } from '../../../app.module';
+import { MonitorComponent } from './monitor.component';
+
+describe('MonitorComponent', () => {
+ let component: MonitorComponent;
+ let fixture: ComponentFixture<MonitorComponent>;
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [AppModule]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MonitorComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+
+import { CellTemplate } from '../../../shared/enum/cell-template.enum';
+import { MonitorService } from '../monitor.service';
+
+@Component({
+ selector: 'cd-monitor',
+ templateUrl: './monitor.component.html',
+ styleUrls: ['./monitor.component.scss']
+})
+export class MonitorComponent {
+
+ mon_status: any;
+ inQuorum: any;
+ notInQuorum: any;
+
+ interval: any;
+ sparklineStyle = {
+ height: '30px',
+ width: '50%'
+ };
+
+ constructor(private monitorService: MonitorService) {
+ this.inQuorum = {
+ columns: [
+ { prop: 'name', name: 'Name', cellTransformation: CellTemplate.routerLink },
+ { prop: 'rank', name: 'Rank' },
+ { prop: 'public_addr', name: 'Public Address' },
+ {
+ prop: 'cdOpenSessions',
+ name: 'Open Sessions',
+ cellTransformation: CellTemplate.sparkline
+ }
+ ],
+ data: []
+ };
+
+ this.notInQuorum = {
+ columns: [
+ { prop: 'name', name: 'Name', cellTransformation: CellTemplate.routerLink },
+ { prop: 'rank', name: 'Rank' },
+ { prop: 'public_addr', name: 'Public Address' }
+ ],
+ data: []
+ };
+ }
+
+ refresh() {
+ this.monitorService.getMonitor().subscribe((data: any) => {
+ data.in_quorum.map((row) => {
+ row.cdOpenSessions = row.stats.num_sessions.map(i => i[1]);
+ row.cdLink = '/perf_counters/mon/' + row.name;
+ return row;
+ });
+
+ data.out_quorum.map((row) => {
+ row.cdLink = '/perf_counters/mon/' + row.name;
+ return row;
+ });
+
+ this.inQuorum.data = [...data.in_quorum];
+ this.notInQuorum.data = [...data.out_quorum];
+ this.mon_status = data.mon_status;
+ });
+ }
+}
--- /dev/null
+<tabset *ngIf="selection.hasSingleSelection">
+ <tab heading="Attributes (OSD map)">
+ <cd-table-key-value *ngIf="osd.loaded"
+ [data]="osd.details.osd_map">
+ </cd-table-key-value>
+ </tab>
+ <tab heading="Metadata">
+ <cd-table-key-value *ngIf="osd.loaded"
+ (fetchData)="osd.autoRefresh()"
+ [data]="osd.details.osd_metadata">
+ </cd-table-key-value>
+ </tab>
+ <tab heading="Performance counter">
+ <cd-table-performance-counter *ngIf="osd.loaded"
+ serviceType="osd"
+ [serviceId]="osd.id">
+ </cd-table-performance-counter>
+ </tab>
+ <tab heading="Histogram">
+ <h3 *ngIf="osd.loaded && osd.histogram_failed">
+ Histogram not available -> <span class="text-warning">{{ osd.histogram_failed }}</span>
+ </h3>
+ <div class="row" *ngIf="osd.loaded && osd.details.histogram">
+ <div class="col-md-6">
+ <h4>Writes</h4>
+ <cd-osd-performance-histogram [histogram]="osd.details.histogram.osd.op_w_latency_in_bytes_histogram">
+ </cd-osd-performance-histogram>
+ </div>
+ <div class="col-md-6">
+ <h4>Reads</h4>
+ <cd-osd-performance-histogram [histogram]="osd.details.histogram.osd.op_r_latency_out_bytes_histogram">
+ </cd-osd-performance-histogram>
+ </div>
+ </div>
+ </tab>
+</tabset>
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsModule } from 'ngx-bootstrap';
+
+import { DataTableModule } from '../../../../shared/datatable/datatable.module';
+import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
+import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
+import {
+ OsdPerformanceHistogramComponent
+} from '../osd-performance-histogram/osd-performance-histogram.component';
+import { OsdService } from '../osd.service';
+import { OsdDetailsComponent } from './osd-details.component';
+
+describe('OsdDetailsComponent', () => {
+ let component: OsdDetailsComponent;
+ let fixture: ComponentFixture<OsdDetailsComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ HttpClientModule,
+ TabsModule.forRoot(),
+ PerformanceCounterModule,
+ DataTableModule
+ ],
+ declarations: [
+ OsdDetailsComponent,
+ OsdPerformanceHistogramComponent
+ ],
+ providers: [OsdService]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdDetailsComponent);
+ component = fixture.componentInstance;
+
+ component.selection = new CdTableSelection();
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input, OnChanges } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
+import { OsdService } from '../osd.service';
+
+@Component({
+ selector: 'cd-osd-details',
+ templateUrl: './osd-details.component.html',
+ styleUrls: ['./osd-details.component.scss']
+})
+export class OsdDetailsComponent implements OnChanges {
+ @Input() selection: CdTableSelection;
+
+ osd: any;
+
+ constructor(private osdService: OsdService) {}
+
+ ngOnChanges() {
+ this.osd = {
+ loaded: false
+ };
+ if (this.selection.hasSelection) {
+ this.osd = this.selection.first();
+ this.osd.autoRefresh = () => {
+ this.refresh();
+ };
+ this.refresh();
+ }
+ }
+
+ refresh() {
+ this.osdService.getDetails(this.osd.tree.id)
+ .subscribe((data: any) => {
+ this.osd.details = data;
+ if (!_.isObject(data.histogram)) {
+ this.osd.histogram_failed = data.histogram;
+ this.osd.details.histogram = undefined;
+ }
+ this.osd.loaded = true;
+ });
+ }
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item">Cluster</li>
+ <li class="breadcrumb-item active">OSDs</li>
+ </ol>
+</nav>
+<cd-table [data]="osds"
+ (fetchData)="getOsdList()"
+ [columns]="columns"
+ selectionType="single"
+ (updateSelection)="updateSelection($event)">
+ <cd-osd-details cdTableDetail
+ [selection]="selection">
+ </cd-osd-details>
+</cd-table>
+
+<ng-template #statusColor
+ let-value="value">
+ <span *ngFor="let state of value; last as last">
+ <span [class.text-success]="'up' === state || 'in' === state"
+ [class.text-warning]="'down' === state || 'out' === state">
+ {{ state }}</span><span *ngIf="!last">, </span>
+ <!-- Has to be on the same line to prevent a space between state and comma. -->
+ </span>
+</ng-template>
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { DataTableModule } from '../../../../shared/datatable/datatable.module';
+import { DimlessPipe } from '../../../../shared/pipes/dimless.pipe';
+import { FormatterService } from '../../../../shared/services/formatter.service';
+import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
+import { OsdDetailsComponent } from '../osd-details/osd-details.component';
+import {
+ OsdPerformanceHistogramComponent
+} from '../osd-performance-histogram/osd-performance-histogram.component';
+import { OsdService } from '../osd.service';
+import { OsdListComponent } from './osd-list.component';
+
+describe('OsdListComponent', () => {
+ let component: OsdListComponent;
+ let fixture: ComponentFixture<OsdListComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ HttpClientModule,
+ PerformanceCounterModule,
+ TabsModule.forRoot(),
+ DataTableModule
+ ],
+ declarations: [
+ OsdListComponent,
+ OsdDetailsComponent,
+ OsdPerformanceHistogramComponent
+ ],
+ providers: [OsdService, DimlessPipe, FormatterService]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdListComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { CellTemplate } from '../../../../shared/enum/cell-template.enum';
+import { CdTableColumn } from '../../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
+import { DimlessPipe } from '../../../../shared/pipes/dimless.pipe';
+import { OsdService } from '../osd.service';
+
+@Component({
+ selector: 'cd-osd-list',
+ templateUrl: './osd-list.component.html',
+ styleUrls: ['./osd-list.component.scss']
+})
+
+export class OsdListComponent implements OnInit {
+ @ViewChild('statusColor') statusColor: TemplateRef<any>;
+
+ osds = [];
+ columns: CdTableColumn[];
+ selection = new CdTableSelection();
+
+ constructor(
+ private osdService: OsdService,
+ private dimlessPipe: DimlessPipe
+ ) { }
+
+ ngOnInit() {
+ this.columns = [
+ {prop: 'host.name', name: 'Host'},
+ {prop: 'id', name: 'ID', cellTransformation: CellTemplate.bold},
+ {prop: 'collectedStates', name: 'Status', cellTemplate: this.statusColor},
+ {prop: 'stats.numpg', name: 'PGs'},
+ {prop: 'usedPercent', name: 'Usage'},
+ {
+ prop: 'stats_history.out_bytes',
+ name: 'Read bytes',
+ cellTransformation: CellTemplate.sparkline
+ },
+ {
+ prop: 'stats_history.in_bytes',
+ name: 'Writes bytes',
+ cellTransformation: CellTemplate.sparkline
+ },
+ {prop: 'stats.op_r', name: 'Read ops', cellTransformation: CellTemplate.perSecond},
+ {prop: 'stats.op_w', name: 'Write ops', cellTransformation: CellTemplate.perSecond}
+ ];
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ getOsdList() {
+ this.osdService.getList().subscribe((data: any[]) => {
+ this.osds = data;
+ data.map((osd) => {
+ osd.collectedStates = this.collectStates(osd);
+ osd.stats_history.out_bytes = osd.stats_history.op_out_bytes.map(i => i[1]);
+ osd.stats_history.in_bytes = osd.stats_history.op_in_bytes.map(i => i[1]);
+ osd.usedPercent = this.dimlessPipe.transform(osd.stats.stat_bytes_used) + ' / ' +
+ this.dimlessPipe.transform(osd.stats.stat_bytes);
+ return osd;
+ });
+ });
+ }
+
+ collectStates(osd) {
+ const select = (onState, offState) => osd[onState] ? onState : offState;
+ return [select('up', 'down'), select('in', 'out')];
+ }
+
+ beforeShowDetails(selection: CdTableSelection) {
+ return selection.hasSingleSelection;
+ }
+}
--- /dev/null
+<table>
+ <tr style="height: 10px;"
+ *ngFor="let row of valuesStyle">
+ <td style="width: 10px; height: 10px;"
+ *ngFor="let col of row"
+ [ngStyle]="col">
+ </td>
+ </tr>
+</table>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { OsdPerformanceHistogramComponent } from './osd-performance-histogram.component';
+
+describe('OsdPerformanceHistogramComponent', () => {
+ let component: OsdPerformanceHistogramComponent;
+ let fixture: ComponentFixture<OsdPerformanceHistogramComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ OsdPerformanceHistogramComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdPerformanceHistogramComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input, OnChanges } from '@angular/core';
+
+import * as _ from 'lodash';
+
+@Component({
+ selector: 'cd-osd-performance-histogram',
+ templateUrl: './osd-performance-histogram.component.html',
+ styleUrls: ['./osd-performance-histogram.component.scss']
+})
+export class OsdPerformanceHistogramComponent implements OnChanges {
+ @Input() histogram: any;
+ valuesStyle: any;
+ last = {};
+
+ constructor() { }
+
+ ngOnChanges() {
+ this.render();
+ }
+
+ hexdigits(v): string {
+ const i = Math.floor(v * 255).toString(16);
+ return i.length === 1 ? '0' + i : i;
+ }
+
+ hexcolor(r, g, b) {
+ return '#' + this.hexdigits(r) + this.hexdigits(g) + this.hexdigits(b);
+ }
+
+ render() {
+ if (!this.histogram) {
+ return;
+ }
+ let sum = 0;
+ let max = 0;
+
+ _.each(this.histogram.values, (row, i) => {
+ _.each(row, (col, j) => {
+ let val;
+ if (this.last && this.last[i] && this.last[i][j]) {
+ val = col - this.last[i][j];
+ } else {
+ val = col;
+ }
+ sum += val;
+ max = Math.max(max, val);
+ });
+ });
+
+ this.valuesStyle = this.histogram.values.map((row, i) => {
+ return row.map((col, j) => {
+ const val = this.last && this.last[i] && this.last[i][j] ? col - this.last[i][j] : col;
+ const g = max ? val / max : 0;
+ const r = 1 - g;
+ return {backgroundColor: this.hexcolor(r, g, 0)};
+ });
+ });
+
+ this.last = this.histogram.values;
+ }
+}
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { OsdService } from './osd.service';
+
+describe('OsdService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [OsdService],
+ imports: [
+ HttpClientModule,
+ ],
+ });
+ });
+
+ it('should be created', inject([OsdService], (service: OsdService) => {
+ expect(service).toBeTruthy();
+ }));
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class OsdService {
+ private path = 'api/osd';
+
+ constructor (private http: HttpClient) {}
+
+ getList () {
+ return this.http.get(`${this.path}`);
+ }
+
+ getDetails(id: number) {
+ return this.http.get(`${this.path}/${id}`);
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { ChartsModule } from 'ng2-charts';
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { SharedModule } from '../../shared/shared.module';
+import { DashboardService } from './dashboard.service';
+import { DashboardComponent } from './dashboard/dashboard.component';
+import { HealthPieComponent } from './health-pie/health-pie.component';
+import { HealthComponent } from './health/health.component';
+import { LogColorPipe } from './log-color.pipe';
+import { MdsSummaryPipe } from './mds-summary.pipe';
+import { MgrSummaryPipe } from './mgr-summary.pipe';
+import { MonSummaryPipe } from './mon-summary.pipe';
+import { OsdSummaryPipe } from './osd-summary.pipe';
+import { PgStatusStylePipe } from './pg-status-style.pipe';
+import { PgStatusPipe } from './pg-status.pipe';
+
+@NgModule({
+ imports: [CommonModule, TabsModule.forRoot(), SharedModule, ChartsModule, RouterModule],
+ declarations: [
+ HealthComponent,
+ DashboardComponent,
+ MonSummaryPipe,
+ OsdSummaryPipe,
+ LogColorPipe,
+ MgrSummaryPipe,
+ PgStatusPipe,
+ MdsSummaryPipe,
+ PgStatusStylePipe,
+ HealthPieComponent
+ ],
+ providers: [DashboardService]
+})
+export class DashboardModule {}
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { appendFile } from 'fs';
+
+import { DashboardService } from './dashboard.service';
+
+describe('DashboardService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [DashboardService],
+ imports: [HttpClientTestingModule, HttpClientModule]
+ });
+ });
+
+ it(
+ 'should be created',
+ inject([DashboardService], (service: DashboardService) => {
+ expect(service).toBeTruthy();
+ })
+ );
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class DashboardService {
+ constructor(private http: HttpClient) {}
+
+ getHealth() {
+ return this.http.get('api/dashboard/health');
+ }
+}
--- /dev/null
+<div>
+ <tabset *ngIf="hasGrafana">
+ <tab i18n-heading
+ heading="Health">
+ <cd-health></cd-health>
+ </tab>
+ <tab i18n-heading
+ heading="Statistics">
+ </tab>
+ </tabset>
+ <cd-health *ngIf="!hasGrafana"></cd-health>
+</div>
--- /dev/null
+div {
+ padding-top: 20px;
+}
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DashboardComponent } from './dashboard.component';
+
+describe('DashboardComponent', () => {
+ let component: DashboardComponent;
+ let fixture: ComponentFixture<DashboardComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ DashboardComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DashboardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ // it('should create', () => {
+ // expect(component).toBeTruthy();
+ // });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'cd-dashboard',
+ templateUrl: './dashboard.component.html',
+ styleUrls: ['./dashboard.component.scss']
+})
+export class DashboardComponent implements OnInit {
+ hasGrafana = false; // TODO: Temporary var, remove when grafana is implemented
+
+ constructor() { }
+
+ ngOnInit() {
+ }
+
+}
--- /dev/null
+<div class="chart-container">
+ <canvas baseChart
+ #chartCanvas
+ [datasets]="chart.dataset"
+ [chartType]="chart.chartType"
+ [options]="chart.options"
+ [labels]="chart.labels"
+ [colors]="chart.colors"
+ width="120"
+ height="120"></canvas>
+ <div class="chartjs-tooltip"
+ #chartTooltip>
+ <table></table>
+ </div>
+</div>
--- /dev/null
+@import '../../../../styles/chart-tooltip.scss';
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ChartsModule } from 'ng2-charts/ng2-charts';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { HealthPieComponent } from './health-pie.component';
+
+describe('HealthPieComponent', () => {
+ let component: HealthPieComponent;
+ let fixture: ComponentFixture<HealthPieComponent>;
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [ChartsModule, SharedModule],
+ declarations: [HealthPieComponent]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HealthPieComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import {
+ Component,
+ ElementRef,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnInit,
+ Output,
+ ViewChild
+} from '@angular/core';
+
+import * as Chart from 'chart.js';
+import * as _ from 'lodash';
+
+import { ChartTooltip } from '../../../shared/models/chart-tooltip';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+
+@Component({
+ selector: 'cd-health-pie',
+ templateUrl: './health-pie.component.html',
+ styleUrls: ['./health-pie.component.scss']
+})
+export class HealthPieComponent implements OnChanges, OnInit {
+ @ViewChild('chartCanvas') chartCanvasRef: ElementRef;
+ @ViewChild('chartTooltip') chartTooltipRef: ElementRef;
+
+ @Input() data: any;
+ @Input() tooltipFn: any;
+ @Output() prepareFn = new EventEmitter();
+
+ chart: any = {
+ chartType: 'doughnut',
+ dataset: [
+ {
+ label: null,
+ borderWidth: 0
+ }
+ ],
+ options: {
+ responsive: true,
+ legend: { display: false },
+ animation: { duration: 0 },
+
+ tooltips: {
+ enabled: false
+ }
+ },
+ colors: [
+ {
+ borderColor: 'transparent'
+ }
+ ]
+ };
+
+ constructor(private dimlessBinary: DimlessBinaryPipe) {}
+
+ ngOnInit() {
+ // An extension to Chart.js to enable rendering some
+ // text in the middle of a doughnut
+ Chart.pluginService.register({
+ beforeDraw: function(chart) {
+ if (!chart.options.center_text) {
+ return;
+ }
+
+ const width = chart.chart.width,
+ height = chart.chart.height,
+ ctx = chart.chart.ctx;
+
+ ctx.restore();
+ const fontSize = (height / 114).toFixed(2);
+ ctx.font = fontSize + 'em sans-serif';
+ ctx.textBaseline = 'middle';
+
+ const text = chart.options.center_text,
+ textX = Math.round((width - ctx.measureText(text).width) / 2),
+ textY = height / 2;
+
+ ctx.fillText(text, textX, textY);
+ ctx.save();
+ }
+ });
+
+ const getStyleTop = (tooltip, positionY) => {
+ return positionY + tooltip.caretY - tooltip.height - 10 + 'px';
+ };
+
+ const getStyleLeft = (tooltip, positionX) => {
+ return positionX + tooltip.caretX + 'px';
+ };
+
+ const getBody = (body) => {
+ const bodySplit = body[0].split(': ');
+ bodySplit[1] = this.dimlessBinary.transform(bodySplit[1]);
+ return bodySplit.join(': ');
+ };
+
+ const chartTooltip = new ChartTooltip(
+ this.chartCanvasRef,
+ this.chartTooltipRef,
+ getStyleLeft,
+ getStyleTop,
+ );
+ chartTooltip.getBody = getBody;
+
+ const self = this;
+ this.chart.options.tooltips.custom = (tooltip) => {
+ chartTooltip.customTooltips(tooltip);
+ };
+
+ this.prepareFn.emit([this.chart, this.data]);
+ }
+
+ ngOnChanges() {
+ this.prepareFn.emit([this.chart, this.data]);
+ }
+}
--- /dev/null
+<div *ngIf="contentData">
+ <div class="row">
+ <!-- HEALTH -->
+ <div class="col-md-6">
+ <div class="well">
+ <fieldset>
+ <legend i18n>Health</legend>
+ <ng-container i18n>Overall status:</ng-container>
+ <span [ngStyle]="contentData.health.status | healthColor">{{ contentData.health.status }}</span>
+ <ul>
+ <li *ngFor="let check of contentData.health.checks">
+ <span [ngStyle]="check.severity | healthColor">{{ check.type }}</span>: {{ check.summary.message }}
+ </li>
+ </ul>
+ </fieldset>
+ </div>
+ </div>
+
+ <div class="col-md-6">
+ <!--STATS -->
+ <div class="row">
+ <div class="col-md-6">
+ <div class="well">
+ <div class="media">
+ <div class="media-left">
+ <i class="fa fa-database fa-fw"></i>
+ </div>
+ <div class="media-body">
+ <span class="media-heading"
+ i18n="ceph monitors">
+ <a routerLink="/monitor/">Monitors</a>
+ </span>
+ <span class="media-text">{{ contentData.mon_status | monSummary }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-6">
+ <div class="well">
+ <div class="media">
+ <div class="media-left">
+ <i class="fa fa-hdd-o fa-fw"></i>
+ </div>
+ <div class="media-body">
+ <span class="media-heading"
+ i18n="ceph OSDs">
+ <a routerLink="/osd/">OSDs</a>
+ </span>
+ <span class="media-text">{{ contentData.osd_map | osdSummary }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-6">
+ <div class="well">
+ <div class="media">
+ <div class="media-left">
+ <i class="fa fa-folder fa-fw"></i>
+ </div>
+ <div class="media-body">
+ <span class="media-heading"
+ i18n>Metadata servers</span>
+ <span class="media-text">{{ contentData.fs_map | mdsSummary }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-6">
+ <div class="well">
+ <div class="media">
+ <div class="media-left">
+ <i class="fa fa-cog fa-fw"></i>
+ </div>
+ <div class="media-body">
+ <span class="media-heading"
+ i18n>Manager daemons</span>
+ <span class="media-text">{{ contentData.mgr_map | mgrSummary }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <!-- USAGE -->
+ <div class="col-md-6">
+ <div class="well">
+ <fieldset class="usage">
+ <legend i18n>Usage</legend>
+
+ <table class="ceph-chartbox">
+ <tr>
+ <td>
+ <span style="font-size: 45px;">{{ contentData.df.stats.total_objects | dimless }}</span>
+ </td>
+ <td>
+ <div class="center-block pie">
+ <cd-health-pie [data]="contentData"
+ (prepareFn)="prepareRawUsage($event[0], $event[1])"></cd-health-pie>
+ </div>
+ </td>
+ <td>
+ <div class="center-block pie">
+ <cd-health-pie [data]="contentData"
+ (prepareFn)="preparePoolUsage($event[0], $event[1])"></cd-health-pie>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td i18n>Objects</td>
+ <td>
+ <ng-container i18n>Raw capacity</ng-container>
+ <br>
+ <ng-container i18n="disk used">({{ contentData.df.stats.total_used_bytes | dimlessBinary }} used)</ng-container>
+ </td>
+ <td i18n>Usage by pool</td>
+ </tr>
+ </table>
+ </fieldset>
+ </div>
+ </div>
+
+ <div class="col-md-6">
+ <div class="well">
+ <fieldset>
+ <legend i18n>Pools</legend>
+ <table class="table table-condensed">
+ <thead>
+ <tr>
+ <th i18n>Name</th>
+ <th i18n>PG status</th>
+ <th i18n>Usage</th>
+ <th colspan="2"
+ i18n>Read</th>
+ <th colspan="2"
+ i18n>Write</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr *ngFor="let pool of contentData.pools">
+ <td>{{ pool.pool_name }}</td>
+ <td [ngStyle]="pool.pg_status | pgStatusStyle">
+ {{ pool.pg_status | pgStatus }}
+ </td>
+ <td>
+ {{ pool.stats.bytes_used.latest | dimlessBinary }} / {{ pool.stats.max_avail.latest | dimlessBinary }}
+ </td>
+ <td>
+ {{ pool.stats.rd_bytes.rate | dimless }}
+ </td>
+ <td>
+ {{ pool.stats.rd.rate | dimless }} ops
+ </td>
+ <td>
+ {{ pool.stats.wr_bytes.rate | dimless }}
+ </td>
+ <td>
+ {{ pool.stats.wr.rate | dimless }} ops
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </fieldset>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-md-12">
+ <!-- LOGS -->
+ <div class="well">
+ <fieldset>
+ <legend i18n>Logs</legend>
+
+ <tabset>
+ <tab heading="Cluster log"
+ class="text-monospace"
+ i18n-heading>
+ <span *ngFor="let line of contentData.clog">
+ {{ line.stamp }} {{ line.priority }}
+ <span [ngStyle]="line | logColor">
+ {{ line.message }}
+ <br>
+ </span>
+ </span>
+ </tab>
+ <tab heading="Audit log"
+ class="text-monospace"
+ i18n-heading>
+ <span *ngFor="let line of contentData.audit_log">
+ {{ line.stamp }} {{ line.priority }}
+ <span [ngStyle]="line | logColor">
+ <span style="font-weight: bold;">
+ {{ line.message }}
+ </span>
+ <br>
+ </span>
+ </span>
+ </tab>
+ </tabset>
+ </fieldset>
+ </div>
+ </div>
+ </div>
+</div>
--- /dev/null
+table.ceph-chartbox {
+ width: 100%;
+
+ td {
+ text-align: center;
+ font-weight: bold;
+ }
+}
+
+.center-block {
+ width: 120px;
+}
+
+.pie {
+ height: 120px;
+ width: 120px;
+}
+
+.media {
+ display: block;
+ min-height: 60px;
+ width: 100%;
+
+ .media-left {
+ border-top-left-radius: 2px;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 2px;
+ display: block;
+ float: left;
+ height: 60px;
+ width: 60px;
+ text-align: center;
+ font-size: 40px;
+ line-height: 60px;
+ padding-right: 0;
+
+ .fa {
+ font-size: 45px;
+ }
+ }
+
+ .media-body {
+ padding: 5px 10px;
+ margin-left: 60px;
+
+ .media-heading {
+ text-transform: uppercase;
+ display: block;
+ font-size: 14px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .media-text {
+ display: block;
+ font-weight: bold;
+ font-size: 18px;
+ }
+ }
+}
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { DashboardService } from '../dashboard.service';
+import { HealthComponent } from './health.component';
+
+describe('HealthComponent', () => {
+ let component: HealthComponent;
+ let fixture: ComponentFixture<HealthComponent>;
+
+ const fakeService = {
+ getHealth() {
+ return {};
+ }
+ };
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ providers: [{ provide: DashboardService, useValue: fakeService }],
+ imports: [SharedModule],
+ declarations: [HealthComponent]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HealthComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+});
--- /dev/null
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { DashboardService } from '../dashboard.service';
+
+@Component({
+ selector: 'cd-health',
+ templateUrl: './health.component.html',
+ styleUrls: ['./health.component.scss']
+})
+export class HealthComponent implements OnInit, OnDestroy {
+ contentData: any;
+ interval: number;
+
+ constructor(private dashboardService: DashboardService) {}
+
+ ngOnInit() {
+ this.getInfo();
+ this.interval = window.setInterval(() => {
+ this.getInfo();
+ }, 5000);
+ }
+
+ ngOnDestroy() {
+ clearInterval(this.interval);
+ }
+
+ getInfo() {
+ this.dashboardService.getHealth().subscribe((data: any) => {
+ this.contentData = data;
+ });
+ }
+
+ prepareRawUsage(chart, data) {
+ let rawUsageChartColor;
+
+ const rawUsageText =
+ Math.round(100 * (data.df.stats.total_used_bytes / data.df.stats.total_bytes)) + '%';
+
+ if (data.df.stats.total_used_bytes / data.df.stats.total_bytes >= data.osd_map.full_ratio) {
+ rawUsageChartColor = '#ff0000';
+ } else if (
+ data.df.stats.total_used_bytes / data.df.stats.total_bytes >=
+ data.osd_map.backfillfull_ratio
+ ) {
+ rawUsageChartColor = '#ff6600';
+ } else if (
+ data.df.stats.total_used_bytes / data.df.stats.total_bytes >=
+ data.osd_map.nearfull_ratio
+ ) {
+ rawUsageChartColor = '#ffc200';
+ } else {
+ rawUsageChartColor = '#00bb00';
+ }
+
+ chart.dataset[0].data = [data.df.stats.total_used_bytes, data.df.stats.total_avail_bytes];
+ chart.options.center_text = rawUsageText;
+ chart.colors = [{ backgroundColor: [rawUsageChartColor, '#424d52'] }];
+ chart.labels = ['Raw Used', 'Raw Available'];
+ }
+
+ preparePoolUsage(chart, data) {
+ const colors = [
+ '#3366CC',
+ '#109618',
+ '#990099',
+ '#3B3EAC',
+ '#0099C6',
+ '#DD4477',
+ '#66AA00',
+ '#B82E2E',
+ '#316395',
+ '#994499',
+ '#22AA99',
+ '#AAAA11',
+ '#6633CC',
+ '#E67300',
+ '#8B0707',
+ '#329262',
+ '#5574A6',
+ '#FF9900',
+ '#DC3912',
+ '#3B3EAC'
+ ];
+
+ const poolLabels = [];
+ const poolData = [];
+
+ _.each(data.df.pools, (pool, i) => {
+ poolLabels.push(pool['name']);
+ poolData.push(pool['stats']['bytes_used']);
+ });
+
+ chart.dataset[0].data = poolData;
+ chart.colors = [{ backgroundColor: colors }];
+ chart.labels = poolLabels;
+ }
+}
--- /dev/null
+import { LogColorPipe } from './log-color.pipe';
+
+describe('LogColorPipe', () => {
+ it('create an instance', () => {
+ const pipe = new LogColorPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'logColor'
+})
+export class LogColorPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ if (value.priority === '[INF]') {
+ return ''; // Inherit
+ } else if (value.priority === '[WRN]') {
+ return {
+ color: '#ffa500',
+ 'font-weight': 'bold'
+ };
+ } else if (value.priority === '[ERR]') {
+ return { color: '#FF2222' };
+ } else {
+ return '';
+ }
+ }
+}
--- /dev/null
+import { MdsSummaryPipe } from './mds-summary.pipe';
+
+describe('MdsSummaryPipe', () => {
+ it('create an instance', () => {
+ const pipe = new MdsSummaryPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+import * as _ from 'lodash';
+
+@Pipe({
+ name: 'mdsSummary'
+})
+export class MdsSummaryPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ if (!value) {
+ return '';
+ }
+
+ let standbys = 0;
+ let active = 0;
+ let standbyReplay = 0;
+ _.each(value.standbys, (s, i) => {
+ standbys += 1;
+ });
+
+ if (value.standbys && !value.filesystems) {
+ return standbys + ', no filesystems';
+ } else if (value.filesystems.length === 0) {
+ return 'no filesystems';
+ } else {
+ _.each(value.filesystems, (fs, i) => {
+ _.each(fs.mdsmap.info, (mds, j) => {
+ if (mds.state === 'up:standby-replay') {
+ standbyReplay += 1;
+ } else {
+ active += 1;
+ }
+ });
+ });
+
+ return active + ' active, ' + (standbys + standbyReplay) + ' standby';
+ }
+ }
+}
--- /dev/null
+import { MgrSummaryPipe } from './mgr-summary.pipe';
+
+describe('MgrSummaryPipe', () => {
+ it('create an instance', () => {
+ const pipe = new MgrSummaryPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+import * as _ from 'lodash';
+
+@Pipe({
+ name: 'mgrSummary'
+})
+export class MgrSummaryPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ if (!value) {
+ return '';
+ }
+
+ let result = 'active: ';
+ result += _.isUndefined(value.active_name) ? 'n/a' : value.active_name;
+
+ if (value.standbys.length) {
+ result += ', ' + value.standbys.length + ' standbys';
+ }
+
+ return result;
+ }
+}
--- /dev/null
+import { MonSummaryPipe } from './mon-summary.pipe';
+
+describe('MonSummaryPipe', () => {
+ it('create an instance', () => {
+ const pipe = new MonSummaryPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'monSummary'
+})
+export class MonSummaryPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ if (!value) {
+ return '';
+ }
+
+ let result = value.monmap.mons.length.toString() + ' (quorum ';
+ result += value.quorum.join(', ');
+ result += ')';
+
+ return result;
+ }
+}
--- /dev/null
+import { OsdSummaryPipe } from './osd-summary.pipe';
+
+describe('OsdSummaryPipe', () => {
+ it('create an instance', () => {
+ const pipe = new OsdSummaryPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+import * as _ from 'lodash';
+
+@Pipe({
+ name: 'osdSummary'
+})
+export class OsdSummaryPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ if (!value) {
+ return '';
+ }
+
+ let inCount = 0;
+ let upCount = 0;
+ _.each(value.osds, (osd, i) => {
+ if (osd.in) {
+ inCount++;
+ }
+ if (osd.up) {
+ upCount++;
+ }
+ });
+
+ return value.osds.length + ' (' + upCount + ' up, ' + inCount + ' in)';
+ }
+}
--- /dev/null
+import { PgStatusStylePipe } from './pg-status-style.pipe';
+
+describe('PgStatusStylePipe', () => {
+ it('create an instance', () => {
+ const pipe = new PgStatusStylePipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+import * as _ from 'lodash';
+
+@Pipe({
+ name: 'pgStatusStyle'
+})
+export class PgStatusStylePipe implements PipeTransform {
+ transform(pgStatus: any, args?: any): any {
+ let warning = false;
+ let error = false;
+
+ _.each(pgStatus, (value, state) => {
+ if (
+ state.includes('inconsistent') ||
+ state.includes('incomplete') ||
+ !state.includes('active')
+ ) {
+ error = true;
+ }
+
+ if (
+ state !== 'active+clean' &&
+ state !== 'active+clean+scrubbing' &&
+ state !== 'active+clean+scrubbing+deep'
+ ) {
+ warning = true;
+ }
+ });
+
+ if (error) {
+ return { color: '#FF0000' };
+ }
+
+ if (warning) {
+ return { color: '#FFC200' };
+ }
+
+ return { color: '#00BB00' };
+ }
+}
--- /dev/null
+import { PgStatusPipe } from './pg-status.pipe';
+
+describe('PgStatusPipe', () => {
+ it('create an instance', () => {
+ const pipe = new PgStatusPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+import * as _ from 'lodash';
+
+@Pipe({
+ name: 'pgStatus'
+})
+export class PgStatusPipe implements PipeTransform {
+ transform(pgStatus: any, args?: any): any {
+ const strings = [];
+ _.each(pgStatus, (count, state) => {
+ strings.push(count + ' ' + state);
+ });
+
+ return strings.join(', ');
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { SharedModule } from '../../shared/shared.module';
+import {
+ PerformanceCounterComponent
+} from './performance-counter/performance-counter.component';
+import { TablePerformanceCounterService } from './services/table-performance-counter.service';
+import {
+ TablePerformanceCounterComponent
+} from './table-performance-counter/table-performance-counter.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ SharedModule,
+ RouterModule
+ ],
+ declarations: [
+ TablePerformanceCounterComponent,
+ PerformanceCounterComponent
+ ],
+ providers: [
+ TablePerformanceCounterService
+ ],
+ exports: [
+ TablePerformanceCounterComponent
+ ]
+})
+export class PerformanceCounterModule { }
--- /dev/null
+<fieldset>
+ <legend i18n>Performance Counters</legend>
+ <h3>{{ serviceType }}.{{ serviceId }}</h3>
+ <cd-table-performance-counter [serviceType]="serviceType"
+ [serviceId]="serviceId">
+ </cd-table-performance-counter>
+</fieldset>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+
+import { PerformanceCounterModule } from '../performance-counter.module';
+import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
+import { PerformanceCounterComponent } from './performance-counter.component';
+
+describe('PerformanceCounterComponent', () => {
+ let component: PerformanceCounterComponent;
+ let fixture: ComponentFixture<PerformanceCounterComponent>;
+
+ const fakeService = {
+ get: (service_type: string, service_id: string) => {
+ return new Promise(function(resolve, reject) {
+ return [];
+ });
+ },
+ list: () => {
+ return new Promise(function(resolve, reject) {
+ return {};
+ });
+ }
+ };
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ PerformanceCounterModule,
+ BsDropdownModule.forRoot(),
+ RouterTestingModule
+ ],
+ providers: [{ provide: TablePerformanceCounterService, useValue: fakeService }]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PerformanceCounterComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnDestroy } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+
+@Component({
+ selector: 'cd-performance-counter',
+ templateUrl: './performance-counter.component.html',
+ styleUrls: ['./performance-counter.component.scss']
+})
+export class PerformanceCounterComponent implements OnDestroy {
+ serviceId: string;
+ serviceType: string;
+ routeParamsSubscribe: any;
+
+ constructor(private route: ActivatedRoute) {
+ this.routeParamsSubscribe = this.route.params.subscribe(
+ (params: { type: string; id: string }) => {
+ this.serviceId = params.id;
+ this.serviceType = params.type;
+ }
+ );
+ }
+
+ ngOnDestroy() {
+ this.routeParamsSubscribe.unsubscribe();
+ }
+}
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+
+import { TablePerformanceCounterService } from './table-performance-counter.service';
+
+describe('TablePerformanceCounterService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [TablePerformanceCounterService],
+ imports: [
+ HttpClientTestingModule,
+ BsDropdownModule.forRoot(),
+ HttpClientModule
+ ]
+ });
+ });
+
+ it(
+ 'should be created',
+ inject([TablePerformanceCounterService], (service: TablePerformanceCounterService) => {
+ expect(service).toBeTruthy();
+ })
+ );
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class TablePerformanceCounterService {
+
+ private url = 'api/perf_counters';
+
+ constructor(private http: HttpClient) { }
+
+ list() {
+ return this.http.get(this.url)
+ .toPromise()
+ .then((resp: object): object => {
+ return resp;
+ });
+ }
+
+ get(service_type: string, service_id: string) {
+ const serviceType = service_type.replace('-', '_');
+
+ return this.http.get(`${this.url}/${serviceType}/${service_id}`)
+ .toPromise()
+ .then((resp: object): Array<object> => {
+ return resp['counters'];
+ });
+ }
+}
--- /dev/null
+<cd-table [data]="counters"
+ [columns]="columns"
+ columnMode="flex"
+ (fetchData)="getCounters()">
+ <ng-template #valueTpl let-row="row">
+ {{ row.value | dimless }} {{ row.unit }}
+ </ng-template>
+</cd-table>
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BsDropdownModule } from 'ngx-bootstrap';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
+import { TablePerformanceCounterComponent } from './table-performance-counter.component';
+
+describe('TablePerformanceCounterComponent', () => {
+ let component: TablePerformanceCounterComponent;
+ let fixture: ComponentFixture<TablePerformanceCounterComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ TablePerformanceCounterComponent ],
+ imports: [
+ HttpClientTestingModule,
+ HttpClientModule,
+ BsDropdownModule.forRoot(),
+ SharedModule
+ ],
+ providers: [ TablePerformanceCounterService ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TablePerformanceCounterComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
+
+/**
+ * Display the specified performance counters in a datatable.
+ */
+@Component({
+ selector: 'cd-table-performance-counter',
+ templateUrl: './table-performance-counter.component.html',
+ styleUrls: ['./table-performance-counter.component.scss']
+})
+export class TablePerformanceCounterComponent implements OnInit {
+
+ columns: Array<CdTableColumn> = [];
+ counters: Array<object> = [];
+
+ @ViewChild('valueTpl') public valueTpl: TemplateRef<any>;
+
+ /**
+ * The service type, e.g. 'rgw', 'mds', 'mon', 'osd', ...
+ */
+ @Input() serviceType: string;
+
+ /**
+ * The service identifier.
+ */
+ @Input() serviceId: string;
+
+ constructor(private performanceCounterService: TablePerformanceCounterService) { }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: 'Name',
+ prop: 'name',
+ flexGrow: 1
+ },
+ {
+ name: 'Description',
+ prop: 'description',
+ flexGrow: 1
+ },
+ {
+ name: 'Value',
+ cellTemplate: this.valueTpl,
+ flexGrow: 1
+ }
+ ];
+ }
+
+ getCounters() {
+ this.performanceCounterService.get(this.serviceType, this.serviceId)
+ .then((resp) => {
+ this.counters = resp;
+ });
+ }
+}
--- /dev/null
+<tabset *ngIf="selection.hasSingleSelection">
+ <tab i18n-heading
+ heading="Details">
+ <cd-table-key-value [data]="metadata"
+ (fetchData)="getMetaData()">
+ </cd-table-key-value>
+ </tab>
+ <tab i18n-heading
+ heading="Performance Counters">
+ <cd-table-performance-counter serviceType="rgw"
+ [serviceId]="serviceId">
+ </cd-table-performance-counter>
+ </tab>
+</tabset>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { SharedModule } from '../../../shared/shared.module';
+import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module';
+import { RgwDaemonService } from '../services/rgw-daemon.service';
+import { RgwDaemonDetailsComponent } from './rgw-daemon-details.component';
+
+describe('RgwDaemonDetailsComponent', () => {
+ let component: RgwDaemonDetailsComponent;
+ let fixture: ComponentFixture<RgwDaemonDetailsComponent>;
+
+ const fakeService = {
+ get: (id: string) => {
+ return new Promise(function(resolve, reject) {
+ return [];
+ });
+ }
+ };
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ RgwDaemonDetailsComponent ],
+ imports: [
+ SharedModule,
+ PerformanceCounterModule,
+ TabsModule.forRoot()
+ ],
+ providers: [{ provide: RgwDaemonService, useValue: fakeService }]
+ });
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwDaemonDetailsComponent);
+ component = fixture.componentInstance;
+
+ component.selection = new CdTableSelection();
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input, OnChanges } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { RgwDaemonService } from '../services/rgw-daemon.service';
+
+@Component({
+ selector: 'cd-rgw-daemon-details',
+ templateUrl: './rgw-daemon-details.component.html',
+ styleUrls: ['./rgw-daemon-details.component.scss']
+})
+export class RgwDaemonDetailsComponent implements OnChanges {
+ metadata: any;
+ serviceId = '';
+
+ @Input() selection: CdTableSelection;
+
+ constructor(private rgwDaemonService: RgwDaemonService) {}
+
+ ngOnChanges() {
+ // Get the service id of the first selected row.
+ if (this.selection.hasSelection) {
+ this.serviceId = this.selection.first().id;
+ }
+ }
+
+ getMetaData() {
+ if (_.isEmpty(this.serviceId)) {
+ return;
+ }
+ this.rgwDaemonService.get(this.serviceId).then(resp => {
+ this.metadata = resp['rgw_metadata'];
+ });
+ }
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li i18n
+ class="breadcrumb-item active"
+ aria-current="page">Object Gateway</li>
+ </ol>
+</nav>
+
+<cd-table [data]="daemons"
+ [columns]="columns"
+ columnMode="flex"
+ selectionType="single"
+ (updateSelection)="updateSelection($event)"
+ (fetchData)="getDaemonList()">
+ <cd-rgw-daemon-details cdTableDetail
+ [selection]="selection">
+ </cd-rgw-daemon-details>
+</cd-table>
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { DataTableModule } from '../../../shared/datatable/datatable.module';
+import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module';
+import { RgwDaemonDetailsComponent } from '../rgw-daemon-details/rgw-daemon-details.component';
+import { RgwDaemonService } from '../services/rgw-daemon.service';
+import { RgwDaemonListComponent } from './rgw-daemon-list.component';
+
+describe('RgwDaemonListComponent', () => {
+ let component: RgwDaemonListComponent;
+ let fixture: ComponentFixture<RgwDaemonListComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ RgwDaemonListComponent, RgwDaemonDetailsComponent ],
+ imports: [
+ DataTableModule,
+ HttpClientTestingModule,
+ HttpClientModule,
+ TabsModule.forRoot(),
+ PerformanceCounterModule
+ ],
+ providers: [ RgwDaemonService ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwDaemonListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
+import { RgwDaemonService } from '../services/rgw-daemon.service';
+
+@Component({
+ selector: 'cd-rgw-daemon-list',
+ templateUrl: './rgw-daemon-list.component.html',
+ styleUrls: ['./rgw-daemon-list.component.scss']
+})
+export class RgwDaemonListComponent {
+
+ columns: Array<CdTableColumn> = [];
+ daemons: Array<object> = [];
+ selection = new CdTableSelection();
+
+ constructor(private rgwDaemonService: RgwDaemonService,
+ cephShortVersionPipe: CephShortVersionPipe) {
+ this.columns = [
+ {
+ name: 'ID',
+ prop: 'id',
+ flexGrow: 2
+ },
+ {
+ name: 'Hostname',
+ prop: 'server_hostname',
+ flexGrow: 2
+ },
+ {
+ name: 'Version',
+ prop: 'version',
+ flexGrow: 1,
+ pipe: cephShortVersionPipe
+ }
+ ];
+ }
+
+ getDaemonList() {
+ this.rgwDaemonService.list()
+ .then((resp) => {
+ this.daemons = resp;
+ });
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { TabsModule } from 'ngx-bootstrap/tabs';
+
+import { SharedModule } from '../../shared/shared.module';
+import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
+import { RgwDaemonDetailsComponent } from './rgw-daemon-details/rgw-daemon-details.component';
+import { RgwDaemonListComponent } from './rgw-daemon-list/rgw-daemon-list.component';
+import { RgwDaemonService } from './services/rgw-daemon.service';
+
+@NgModule({
+ entryComponents: [
+ RgwDaemonDetailsComponent
+ ],
+ imports: [
+ CommonModule,
+ SharedModule,
+ PerformanceCounterModule,
+ TabsModule.forRoot()
+ ],
+ exports: [
+ RgwDaemonListComponent,
+ RgwDaemonDetailsComponent
+ ],
+ declarations: [
+ RgwDaemonListComponent,
+ RgwDaemonDetailsComponent
+ ],
+ providers: [
+ RgwDaemonService
+ ]
+})
+export class RgwModule { }
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { RgwDaemonService } from './rgw-daemon.service';
+
+describe('RgwDaemonService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [RgwDaemonService],
+ imports: [HttpClientTestingModule, HttpClientModule]
+ });
+ });
+
+ it(
+ 'should be created',
+ inject([RgwDaemonService], (service: RgwDaemonService) => {
+ expect(service).toBeTruthy();
+ })
+ );
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class RgwDaemonService {
+
+ private url = 'api/rgw/daemon';
+
+ constructor(private http: HttpClient) { }
+
+ list() {
+ return this.http.get(this.url)
+ .toPromise()
+ .then((resp: any) => {
+ return resp;
+ });
+ }
+
+ get(id: string) {
+ return this.http.get(`${this.url}/${id}`)
+ .toPromise()
+ .then((resp: any) => {
+ return resp;
+ });
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { SharedModule } from '../../shared/shared.module';
+
+import { LoginComponent } from './login/login.component';
+import { LogoutComponent } from './logout/logout.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ SharedModule
+ ],
+ declarations: [LoginComponent, LogoutComponent],
+ exports: [LogoutComponent]
+})
+export class AuthModule { }
--- /dev/null
+<div class="login">
+ <div class="row full-height vertical-align">
+ <div class="col-sm-6 hidden-xs">
+ <img src="assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png"
+ alt="Ceph"
+ class="pull-right">
+ </div>
+ <div class="col-xs-10 col-sm-4 col-lg-3 col-xs-offset-1 col-sm-offset-0 col-md-offset-0 col-lg-offset-0">
+ <h1 i18n="The welcome message on the login page">Welcome to Ceph!</h1>
+ <form name="loginForm"
+ (ngSubmit)="login()"
+ #loginForm="ngForm"
+ novalidate>
+
+ <!-- Username -->
+ <div class="form-group has-feedback"
+ [ngClass]="{'has-error': (loginForm.submitted || username.dirty) && username.invalid}">
+ <input name="username"
+ [(ngModel)]="model.username"
+ #username="ngModel"
+ type="text"
+ placeholder="Enter your username..."
+ class="form-control"
+ required
+ autofocus>
+ <div class="help-block"
+ *ngIf="(loginForm.submitted || username.dirty) && username.invalid">Username is required</div>
+ </div>
+
+ <!-- Password -->
+ <div class="form-group has-feedback"
+ [ngClass]="{'has-error': (loginForm.submitted || password.dirty) && password.invalid}">
+ <div class="input-group">
+ <input id="password"
+ name="password"
+ [(ngModel)]="model.password"
+ #password="ngModel"
+ type="password"
+ placeholder="Enter your password..."
+ class="form-control"
+ required>
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default btn-password"
+ cdPasswordButton="password">
+ </button>
+ </span>
+ </div>
+ <div class="help-block"
+ *ngIf="(loginForm.submitted || password.dirty) && password.invalid">Password is required
+ </div>
+ </div>
+
+ <!-- Stay signed in -->
+ <div class="checkbox checkbox-primary">
+ <input id="stay_signed_in"
+ name="stay_signed_in"
+ type="checkbox"
+ [(ngModel)]="model.stay_signed_in">
+ <label for="stay_signed_in"
+ i18n="A checkbox on the login page to do not expire session on browser close">
+ Keep me logged in
+ </label>
+ </div>
+
+ <input type="submit"
+ class="btn btn-openattic btn-block"
+ [disabled]="loginForm.invalid"
+ value="Login">
+ </form>
+ </div>
+ </div>
+</div>
--- /dev/null
+@import '../../../../defaults';
+
+.login {
+ height: 100%;
+
+ .row {
+ color: #ececec;
+ background-color: #474544;
+ }
+
+ h1 {
+ margin-top: 0;
+ margin-bottom: 30px;
+ }
+
+ .btn-password,
+ .form-control {
+ color: #ececec;
+ background-color: #555555;
+ }
+
+ .btn-password:focus {
+ outline-color: #66afe9;
+ }
+
+ .checkbox-primary input[type="checkbox"]:checked + label::before,
+ .checkbox-primary input[type="radio"]:checked + label::before {
+ background-color: $oa-color-blue;
+ border-color: $oa-color-blue;
+ }
+}
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastModule } from 'ng2-toastr';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { LoginComponent } from './login.component';
+
+describe('LoginComponent', () => {
+ let component: LoginComponent;
+ let fixture: ComponentFixture<LoginComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ SharedModule,
+ RouterTestingModule,
+ HttpClientTestingModule,
+ ToastModule.forRoot()
+ ],
+ declarations: [
+ LoginComponent
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LoginComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit, ViewContainerRef } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { ToastsManager } from 'ng2-toastr';
+
+import { Credentials } from '../../../shared/models/credentials';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { AuthService } from '../../../shared/services/auth.service';
+
+@Component({
+ selector: 'cd-login',
+ templateUrl: './login.component.html',
+ styleUrls: ['./login.component.scss']
+})
+export class LoginComponent implements OnInit {
+
+ model = new Credentials();
+
+ constructor(private authService: AuthService,
+ private authStorageService: AuthStorageService,
+ private router: Router,
+ public toastr: ToastsManager,
+ private vcr: ViewContainerRef) {
+ this.toastr.setRootViewContainerRef(vcr);
+ }
+
+ ngOnInit() {
+ if (this.authStorageService.isLoggedIn()) {
+ this.router.navigate(['']);
+ }
+ }
+
+ login() {
+ this.authService.login(this.model).then(() => {
+ this.router.navigate(['']);
+ });
+ }
+}
--- /dev/null
+<a i18n-title
+ title="Sign Out"
+ (click)="logout()">
+ <i class="fa fa-sign-out"></i>
+ <ng-container i18n>Logout</ng-container>
+</a>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { LogoutComponent } from './logout.component';
+
+describe('LogoutComponent', () => {
+ let component: LogoutComponent;
+ let fixture: ComponentFixture<LogoutComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SharedModule,
+ RouterTestingModule,
+ HttpClientTestingModule
+ ],
+ declarations: [
+ LogoutComponent
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LogoutComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { AuthService } from '../../../shared/services/auth.service';
+
+@Component({
+ selector: 'cd-logout',
+ templateUrl: './logout.component.html',
+ styleUrls: ['./logout.component.scss']
+})
+export class LogoutComponent implements OnInit {
+
+ constructor(private authService: AuthService,
+ private router: Router) { }
+
+ ngOnInit() {
+ }
+
+ logout() {
+ this.authService.logout().then(() => {
+ this.router.navigate(['/login']);
+ });
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { AuthModule } from './auth/auth.module';
+import { NavigationModule } from './navigation/navigation.module';
+import { NotFoundComponent } from './not-found/not-found.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ NavigationModule,
+ AuthModule
+ ],
+ exports: [NavigationModule],
+ declarations: [NotFoundComponent]
+})
+export class CoreModule { }
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
+
+import { AppRoutingModule } from '../../app-routing.module';
+import { SharedModule } from '../../shared/shared.module';
+import { AuthModule } from '../auth/auth.module';
+import { NavigationComponent } from './navigation/navigation.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ AuthModule,
+ BsDropdownModule.forRoot(),
+ AppRoutingModule,
+ SharedModule,
+ RouterModule
+ ],
+ declarations: [NavigationComponent],
+ exports: [NavigationComponent]
+})
+export class NavigationModule {}
--- /dev/null
+<nav class="navbar navbar-default navbar-openattic">
+ <!-- Brand and toggle get grouped for better mobile display -->
+
+ <div class="navbar-header tc_logo_component">
+ <a class="navbar-brand"
+ href="#">
+ <img src="assets/Ceph_Logo_Standard_RGB_White_120411_fa.png"
+ alt="Ceph">
+ </a>
+
+ <button type="button"
+ class="navbar-toggle collapsed"
+ data-toggle="collapse"
+ data-target="#bs-example-navbar-collapse-1">
+ <span i18n
+ class="sr-only">Toggle navigation
+ </span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ </div>
+
+ <!-- Collect the nav links, forms, and other content for toggling -->
+ <div class="collapse navbar-collapse"
+ id="bs-example-navbar-collapse-1">
+ <ul class="nav navbar-nav navbar-primary">
+
+ <!-- Dashboard -->
+ <li routerLinkActive="active"
+ class="tc_menuitem tc_menuitem_dashboard">
+ <a i18n
+ routerLink="/dashboard">
+ <i class="fa fa-heartbeat fa-fw"
+ [ngStyle]="summaryData?.health_status | healthColor"></i>
+ <span>Dashboard</span>
+ </a>
+ </li>
+
+ <!-- Cluster -->
+ <li dropdown
+ routerLinkActive="active"
+ class="dropdown tc_menuitem tc_menuitem_cluster">
+ <a dropdownToggle
+ class="dropdown-toggle"
+ data-toggle="dropdown">
+ <ng-container i18n>Cluster</ng-container>
+ <span class="caret"></span>
+ </a>
+ <ul *dropdownMenu
+ class="dropdown-menu">
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_hosts">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/hosts">Hosts
+ </a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_cluster_monitor">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/monitor/"> Monitors
+ </a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_hosts">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/osd">OSDs
+ </a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_configuration">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/configuration">Configuration Doc.
+ </a>
+ </li>
+ </ul>
+ </li>
+
+ <!-- Block -->
+ <li dropdown
+ routerLinkActive="active"
+ class="dropdown tc_menuitem tc_menuitem_block">
+ <a dropdownToggle
+ class="dropdown-toggle"
+ data-toggle="dropdown"
+ [ngStyle]="blockHealthColor()">
+ <ng-container i18n>Block</ng-container>
+ <span class="caret"></span>
+ </a>
+
+ <ul class="dropdown-menu">
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_block_mirroring">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/mirroring/"> Mirroring
+ <small *ngIf="summaryData?.rbd_mirroring?.warnings !== 0"
+ class="label label-warning">{{ summaryData?.rbd_mirroring?.warnings }}</small>
+ <small *ngIf="summaryData?.rbd_mirroring?.errors !== 0"
+ class="label label-danger">{{ summaryData?.rbd_mirroring?.errors }}</small>
+ </a>
+ </li>
+
+ <li routerLinkActive="active">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/block/iscsi">iSCSI</a>
+ </li>
+
+ <li class="dropdown-submenu">
+ <a class="dropdown-toggle"
+ data-toggle="dropdown">Pools</a>
+ <ul *dropdownMenu
+ class="dropdown-menu">
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_pools"
+ *ngFor="let rbdPool of rbdPools">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/block/pool/{{ rbdPool }}">{{ rbdPool }}
+ </a>
+ </li>
+ <li class="tc_submenuitem tc_submenuitem_cephfs_nofs"
+ *ngIf="rbdPools.length === 0">
+ <a class="dropdown-item disabled"
+ i18n>There are no pools</a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+
+ <!-- Filesystem -->
+ <li dropdown
+ routerLinkActive="active"
+ class="dropdown tc_menuitem tc_menuitem_cephs">
+ <a dropdownToggle
+ class="dropdown-toggle"
+ data-toggle="dropdown">
+ <ng-container i18n>Filesystems</ng-container>
+ <span class="caret"></span>
+ </a>
+ <ul *dropdownMenu
+ class="dropdown-menu">
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_cephfs_fs"
+ *ngFor="let fs of summaryData?.filesystems">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/cephfs/{{fs.id}}">{{ fs.name }}
+ </a>
+ </li>
+ <li class="tc_submenuitem tc_submenuitem_cephfs_nofs"
+ *ngIf="summaryData.filesystems.length === 0">
+ <span i18n>There are no filesystems</span>
+ </li>
+ </ul>
+ </li>
+ <!--
+ <li routerLinkActive="active"
+ class="tc_menuitem tc_menuitem_ceph_osds">
+ <a i18n
+ routerLink="/cephOsds">OSDs
+ </a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_menuitem tc_menuitem_ceph_pools">
+ <a i18n
+ routerLink="/cephPools">Pools
+ </a>
+ </li>
+ -->
+
+ <!-- Object Gateway -->
+ <li routerLinkActive="active"
+ class="tc_menuitem tc_menuitem_rgw">
+ <a i18n
+ routerLink="/rgw">Object Gateway
+ </a>
+ </li>
+
+ <!--<li class="dropdown tc_menuitem tc_menuitem_ceph_rgw">
+ <a href=""
+ class="dropdown-toggle"
+ data-toggle="dropdown">
+ <ng-container i18n>Object Gateway</ng-container>
+ <span class="caret"></span>
+ </a>
+ <ul *dropdownMenu
+ class="dropdown-menu">
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_rgw_users">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/rgw-users">Users
+ </a>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_rgw_buckets">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/rgw-buckets">Buckets
+ </a>
+ </li>
+ </ul>
+ </li>
+ <li routerLinkActive="active"
+ class="tc_menuitem tc_submenuitem_settings">
+ <a i18n
+ routerLink="/settings">Settings
+ </a>
+ </li> -->
+ </ul>
+ <!-- /.navbar-primary -->
+
+ <ul class="nav navbar-nav navbar-utility">
+ <li class="tc_logout">
+ <cd-logout class="oa-navbar"></cd-logout>
+ </li>
+ </ul>
+ <!-- /.navbar-utility -->
+ </div>
+ <!-- /.navbar-collapse -->
+</nav>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { LogoutComponent } from '../../auth/logout/logout.component';
+import { NavigationComponent } from './navigation.component';
+
+describe('NavigationComponent', () => {
+ let component: NavigationComponent;
+ let fixture: ComponentFixture<NavigationComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SharedModule,
+ RouterTestingModule,
+ HttpClientTestingModule
+ ],
+ declarations: [
+ NavigationComponent,
+ LogoutComponent
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NavigationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { SummaryService } from '../../../shared/services/summary.service';
+
+@Component({
+ selector: 'cd-navigation',
+ templateUrl: './navigation.component.html',
+ styleUrls: ['./navigation.component.scss']
+})
+export class NavigationComponent implements OnInit {
+ summaryData: any;
+ rbdPools: Array<any> = [];
+
+ constructor(private summaryService: SummaryService) {}
+
+ ngOnInit() {
+ this.summaryService.summaryData$.subscribe((data: any) => {
+ this.summaryData = data;
+ this.rbdPools = data.rbd_pools;
+ });
+ }
+
+ blockHealthColor() {
+ if (this.summaryData && this.summaryData.rbd_mirroring) {
+ if (this.summaryData.rbd_mirroring.errors > 0) {
+ return { color: '#d9534f' };
+ } else if (this.summaryData.rbd_mirroring.warnings > 0) {
+ return { color: '#f0ad4e' };
+ }
+ }
+ }
+}
--- /dev/null
+<div class="row">
+ <div class="col-md-12 text-center">
+ <h1 i18n>Sorry, we could not find what you were looking for</h1>
+
+ <img class="img-responsive center-block img-rounded"
+ src="/assets/1280px-Mimic_Octopus2.jpg">
+ <span>
+ "<a href="https://www.flickr.com/photos/37707866@N00/4838953223">Mimic Octopus</a>" by prilfish is licensed under
+ <a rel="nofollow"
+ class="external text"
+ href="https://creativecommons.org/licenses/by/2.0/">CC BY 2.0</a>
+ </span>
+ </div>
+</div>
--- /dev/null
+h1 {
+ font-size: -webkit-xxx-large;
+}
+
+h2 {
+ font-size: xx-large;
+}
+
+*{
+ font-family: monospace;
+}
+
+img{
+ width: 50vw;
+}
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NotFoundComponent } from './not-found.component';
+
+describe('NotFoundComponent', () => {
+ let component: NotFoundComponent;
+ let fixture: ComponentFixture<NotFoundComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ NotFoundComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NotFoundComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'cd-not-found',
+ templateUrl: './not-found.component.html',
+ styleUrls: ['./not-found.component.scss']
+})
+export class NotFoundComponent {
+ constructor() {}
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { ChartsModule } from 'ng2-charts/ng2-charts';
+import { AlertModule } from 'ngx-bootstrap';
+
+import { SparklineComponent } from './sparkline/sparkline.component';
+import { ViewCacheComponent } from './view-cache/view-cache.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ AlertModule.forRoot(),
+ ChartsModule
+ ],
+ declarations: [
+ ViewCacheComponent,
+ SparklineComponent
+ ],
+ providers: [],
+ exports: [
+ ViewCacheComponent,
+ SparklineComponent
+ ]
+})
+export class ComponentsModule { }
--- /dev/null
+<div class="chart-container"
+ [ngStyle]="style">
+ <canvas baseChart #sparkCanvas
+ [labels]="labels"
+ [datasets]="datasets"
+ [options]="options"
+ [colors]="colors"
+ [chartType]="'line'">
+ </canvas>
+ <div class="chartjs-tooltip" #sparkTooltip>
+ <table></table>
+ </div>
+</div>
--- /dev/null
+@import '../../../../styles/chart-tooltip.scss';
+
+.chart-container {
+ position: static !important;
+}
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AppModule } from '../../../app.module';
+import { SparklineComponent } from './sparkline.component';
+
+describe('SparklineComponent', () => {
+ let component: SparklineComponent;
+ let fixture: ComponentFixture<SparklineComponent>;
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [AppModule]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SparklineComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, ElementRef, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
+import { Input } from '@angular/core';
+
+import { ChartTooltip } from '../../../shared/models/chart-tooltip';
+
+@Component({
+ selector: 'cd-sparkline',
+ templateUrl: './sparkline.component.html',
+ styleUrls: ['./sparkline.component.scss']
+})
+export class SparklineComponent implements OnInit, OnChanges {
+ @ViewChild('sparkCanvas') chartCanvasRef: ElementRef;
+ @ViewChild('sparkTooltip') chartTooltipRef: ElementRef;
+
+ @Input() data: any;
+ @Input()
+ style = {
+ height: '30px',
+ width: '100px'
+ };
+
+ public colors: Array<any> = [
+ {
+ backgroundColor: 'rgba(40,140,234,0.2)',
+ borderColor: 'rgba(40,140,234,1)',
+ pointBackgroundColor: 'rgba(40,140,234,1)',
+ pointBorderColor: '#fff',
+ pointHoverBackgroundColor: '#fff',
+ pointHoverBorderColor: 'rgba(40,140,234,0.8)'
+ }
+ ];
+
+ options = {
+ animation: {
+ duration: 0
+ },
+ responsive: true,
+ maintainAspectRatio: false,
+ legend: {
+ display: false
+ },
+ elements: {
+ line: {
+ borderWidth: 1
+ }
+ },
+ tooltips: {
+ enabled: false,
+ mode: 'index',
+ intersect: false,
+ custom: undefined
+ },
+ scales: {
+ yAxes: [
+ {
+ display: false
+ }
+ ],
+ xAxes: [
+ {
+ display: false
+ }
+ ]
+ }
+ };
+
+ public datasets: Array<any> = [
+ {
+ data: []
+ }
+ ];
+
+ public labels: Array<any> = [];
+
+ constructor() {}
+
+ ngOnInit() {
+ const getStyleTop = (tooltip, positionY) => {
+ return (tooltip.caretY - tooltip.height - tooltip.yPadding - 5) + 'px';
+ };
+
+ const getStyleLeft = (tooltip, positionX) => {
+ return positionX + tooltip.caretX + 'px';
+ };
+
+ const chartTooltip = new ChartTooltip(
+ this.chartCanvasRef,
+ this.chartTooltipRef,
+ getStyleLeft,
+ getStyleTop
+ );
+
+ chartTooltip.customColors = {
+ backgroundColor: this.colors[0].pointBackgroundColor,
+ borderColor: this.colors[0].pointBorderColor
+ };
+
+ this.options.tooltips.custom = tooltip => {
+ chartTooltip.customTooltips(tooltip);
+ };
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ this.datasets[0].data = changes['data'].currentValue;
+ this.datasets = [...this.datasets];
+ this.labels = [...Array(changes['data'].currentValue.length)];
+ }
+}
--- /dev/null
+<alert i18n
+ type="info"
+ *ngIf="status === vcs.ValueNone">
+ Retrieving data, please wait.
+</alert>
+
+<alert i18n
+ type="warning"
+ *ngIf="status === vcs.ValueStale">
+ Displaying previously cached data.
+</alert>
+
+<alert i18n
+ type="danger"
+ *ngIf="status === vcs.ValueException">
+ Could not load data. Please check the cluster health.
+</alert>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AlertModule } from 'ngx-bootstrap';
+
+import { ViewCacheComponent } from './view-cache.component';
+
+describe('ViewCacheComponent', () => {
+ let component: ViewCacheComponent;
+ let fixture: ComponentFixture<ViewCacheComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ViewCacheComponent ],
+ imports: [AlertModule.forRoot()]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ViewCacheComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input, OnInit } from '@angular/core';
+
+import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
+
+@Component({
+ selector: 'cd-view-cache',
+ templateUrl: './view-cache.component.html',
+ styleUrls: ['./view-cache.component.scss']
+})
+export class ViewCacheComponent implements OnInit {
+ @Input() status: ViewCacheStatus;
+ vcs = ViewCacheStatus;
+
+ constructor() {}
+
+ ngOnInit() {}
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
+
+import { ComponentsModule } from '../components/components.module';
+import { PipesModule } from '../pipes/pipes.module';
+import { TableKeyValueComponent } from './table-key-value/table-key-value.component';
+import { TableComponent } from './table/table.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ NgxDatatableModule,
+ FormsModule,
+ BsDropdownModule.forRoot(),
+ PipesModule,
+ ComponentsModule,
+ RouterModule
+ ],
+ declarations: [
+ TableComponent,
+ TableKeyValueComponent
+ ],
+ exports: [
+ TableComponent,
+ NgxDatatableModule,
+ TableKeyValueComponent
+ ]
+})
+export class DataTableModule { }
--- /dev/null
+<cd-table [data]="tableData"
+ [columns]="columns"
+ columnMode="flex"
+ [toolHeader]="false"
+ [header]="false"
+ [footer]="false"
+ [limit]="0"
+ (fetchData)="reloadData()">
+</cd-table>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+
+import { ComponentsModule } from '../../components/components.module';
+import { TableComponent } from '../table/table.component';
+import { TableKeyValueComponent } from './table-key-value.component';
+
+describe('TableKeyValueComponent', () => {
+ let component: TableKeyValueComponent;
+ let fixture: ComponentFixture<TableKeyValueComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ TableComponent, TableKeyValueComponent ],
+ imports: [ FormsModule, NgxDatatableModule, ComponentsModule, RouterTestingModule ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TableKeyValueComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should make key value object pairs out of arrays with length two', () => {
+ component.data = [
+ ['someKey', 0],
+ [3, 'something'],
+ ];
+ component.ngOnInit();
+ expect(component.tableData.length).toBe(2);
+ expect(component.tableData[0].key).toBe('someKey');
+ expect(component.tableData[1].value).toBe('something');
+ });
+
+ it('should transform arrays', () => {
+ component.data = [
+ ['someKey', [1, 2, 3]],
+ [3, 'something']
+ ];
+ component.ngOnInit();
+ expect(component.tableData.length).toBe(2);
+ expect(component.tableData[0].key).toBe('someKey');
+ expect(component.tableData[0].value).toBe('1, 2, 3');
+ expect(component.tableData[1].value).toBe('something');
+ });
+
+ it('should remove pure object values', () => {
+ component.data = [
+ [3, 'something'],
+ ['will be removed', { a: 3, b: 4, c: 5}]
+ ];
+ component.ngOnInit();
+ expect(component.tableData.length).toBe(1);
+ expect(component.tableData[0].value).toBe('something');
+ });
+
+ it('should make key value object pairs out of an object', () => {
+ component.data = {
+ 3: 'something',
+ someKey: 0
+ };
+ component.ngOnInit();
+ expect(component.tableData.length).toBe(2);
+ expect(component.tableData[0].value).toBe('something');
+ expect(component.tableData[1].key).toBe('someKey');
+ });
+
+ it('should make do nothing if data is correct', () => {
+ component.data = [
+ {
+ key: 3,
+ value: 'something'
+ },
+ {
+ key: 'someKey',
+ value: 0
+ }
+ ];
+ component.ngOnInit();
+ expect(component.tableData.length).toBe(2);
+ expect(component.tableData[0].value).toBe('something');
+ expect(component.tableData[1].key).toBe('someKey');
+ });
+
+ it('should throw error if miss match', () => {
+ component.data = 38;
+ expect(() => component.ngOnInit()).toThrowError('Wrong data format');
+ component.data = [['someKey', 0, 3]];
+ expect(() => component.ngOnInit()).toThrowError('Wrong array format');
+ });
+});
--- /dev/null
+import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
+
+import * as _ from 'lodash';
+
+import { CellTemplate } from '../../enum/cell-template.enum';
+import { CdTableColumn } from '../../models/cd-table-column';
+
+/**
+ * Display the given data in a 2 column data table. The left column
+ * shows the 'key' attribute, the right column the 'value' attribute.
+ * The data table has the following characteristics:
+ * - No header and footer is displayed
+ * - The relation of the width for the columns 'key' and 'value' is 1:3
+ * - The 'key' column is displayed in bold text
+ */
+@Component({
+ selector: 'cd-table-key-value',
+ templateUrl: './table-key-value.component.html',
+ styleUrls: ['./table-key-value.component.scss']
+})
+export class TableKeyValueComponent implements OnInit, OnChanges {
+
+ columns: Array<CdTableColumn> = [];
+
+ @Input() data: any;
+
+ tableData: {
+ key: string,
+ value: any
+ }[];
+
+ /**
+ * The function that will be called to update the input data.
+ */
+ @Output() fetchData = new EventEmitter();
+
+ constructor() { }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ prop: 'key',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.bold
+ },
+ {
+ prop: 'value',
+ flexGrow: 3
+ }
+ ];
+ this.useData();
+ }
+
+ ngOnChanges(changes) {
+ this.useData();
+ }
+
+ useData() {
+ let temp = [];
+ if (!this.data) {
+ return; // Wait for data
+ } else if (_.isArray(this.data)) {
+ const first = this.data[0];
+ if (_.isPlainObject(first) && _.has(first, 'key') && _.has(first, 'value')) {
+ temp = [...this.data];
+ } else {
+ if (_.isArray(first)) {
+ if (first.length === 2) {
+ temp = this.data.map(a => ({
+ key: a[0],
+ value: a[1]
+ }));
+ } else {
+ throw new Error('Wrong array format');
+ }
+ }
+ }
+ } else if (_.isPlainObject(this.data)) {
+ temp = Object.keys(this.data).map(k => ({
+ key: k,
+ value: this.data[k]
+ }));
+ } else {
+ throw new Error('Wrong data format');
+ }
+ this.tableData = temp.map(o => {
+ if (_.isArray(o.value)) {
+ o.value = o.value.join(', ');
+ } else if (_.isObject(o.value)) {
+ return;
+ }
+ return o;
+ }).filter(o => o); // Filters out undefined
+ }
+
+ reloadData() {
+ // Forward event triggered by the 'cd-table' datatable.
+ this.fetchData.emit();
+ }
+}
--- /dev/null
+<div class="dataTables_wrapper">
+ <div class="dataTables_header clearfix"
+ *ngIf="toolHeader">
+ <!-- actions -->
+ <div class="oadatatableactions">
+ <ng-content select="table-actions"></ng-content>
+ </div>
+ <!-- end actions -->
+
+ <!-- search -->
+ <div class="input-group">
+ <span class="input-group-addon">
+ <i class="glyphicon glyphicon-search"></i>
+ </span>
+ <input class="form-control"
+ type="text"
+ [(ngModel)]="search"
+ (keyup)='updateFilter($event)'>
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default clear-input tc_clearInputBtn"
+ (click)="updateFilter()">
+ <i class="icon-prepend fa fa-remove"></i>
+ </button>
+ </span>
+ </div>
+ <!-- end search -->
+
+ <!-- pagination limit -->
+ <div class="input-group dataTables_paginate">
+ <input class="form-control"
+ type="number"
+ min="1"
+ max="9999"
+ [value]="limit"
+ (click)="setLimit($event)"
+ (keyup)="setLimit($event)"
+ (blur)="setLimit($event)">
+ </div>
+ <!-- end pagination limit-->
+
+ <!-- show hide columns -->
+ <div class="widget-toolbar">
+ <div dropdown
+ class="dropdown tc_menuitem tc_menuitem_cluster">
+ <a dropdownToggle
+ class="btn btn-sm btn-default dropdown-toggle tc_columnBtn"
+ data-toggle="dropdown">
+ <i class="fa fa-lg fa-table"></i>
+ </a>
+ <ul *dropdownMenu
+ class="dropdown-menu">
+ <li *ngFor="let column of columns">
+ <label>
+ <input type="checkbox"
+ (change)="toggleColumn($event)"
+ [name]="column.prop"
+ [checked]="!column.isHidden">
+ <span>{{ column.name }}</span>
+ </label>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <!-- end show hide columns -->
+
+ <!-- refresh button -->
+ <div class="widget-toolbar tc_refreshBtn">
+ <a (click)="refreshBtn()">
+ <i class="fa fa-lg fa-refresh"
+ [class.fa-spin]="updating || loadingIndicator"></i>
+ </a>
+ </div>
+ <!-- end refresh button -->
+ </div>
+ <ngx-datatable #table
+ class="bootstrap oadatatable"
+ [cssClasses]="paginationClasses"
+ [selectionType]="selectionType"
+ [selected]="selection.selected"
+ (select)="onSelect()"
+ [sorts]="sorts"
+ [columns]="tableColumns"
+ [columnMode]="columnMode"
+ [rows]="rows"
+ [rowClass]="getRowClass()"
+ [headerHeight]="header ? 'auto' : 0"
+ [footerHeight]="footer ? 'auto' : 0"
+ [limit]="limit > 0 ? limit : undefined"
+ [loadingIndicator]="loadingIndicator"
+ [rowIdentity]="rowIdentity()"
+ [rowHeight]="'auto'">
+ </ngx-datatable>
+</div>
+
+<!-- Table Details -->
+<ng-content select="[cdTableDetail]"></ng-content>
+
+<!-- cell templates that can be accessed from outside -->
+<ng-template #tableCellBoldTpl
+ let-value="value">
+ <strong>{{ value }}</strong>
+</ng-template>
+
+<ng-template #sparklineTpl
+ let-value="value">
+ <cd-sparkline [data]="value"></cd-sparkline>
+</ng-template>
+
+<ng-template #routerLinkTpl
+ let-row="row"
+ let-value="value">
+ <a [routerLink]="[row.cdLink]">{{ value }}</a>
+</ng-template>
+
+<ng-template #perSecondTpl
+ let-row="row"
+ let-value="value">
+ {{ value }} /s
+</ng-template>
--- /dev/null
+@import '../../../../defaults';
+
+.dataTables_wrapper {
+ margin-bottom: 25px;
+ .separator {
+ height: 30px;
+ border-left: 1px solid rgba(0,0,0,.09);
+ padding-left: 5px;
+ margin-left: 5px;
+ display: inline-block;
+ vertical-align: middle;
+ }
+ .widget-toolbar {
+ display: inline-block;
+ float: right;
+ width: auto;
+ height: 30px;
+ line-height: 28px;
+ position: relative;
+ border-left: 1px solid rgba(0,0,0,.09);
+ cursor: pointer;
+ padding: 0 8px;
+ text-align: center;
+ }
+ .dropdown-menu {
+ white-space: nowrap;
+ & > li {
+ cursor: pointer;
+ & > label {
+ width: 100%;
+ margin-bottom: 0;
+ padding-left: 20px;
+ padding-right: 20px;
+ cursor: pointer;
+ &:hover {
+ background-color: #f5f5f5;
+ }
+ & > input {
+ cursor: pointer;
+ }
+ }
+ }
+ }
+ th.oadatatablecheckbox {
+ width: 16px;
+ }
+ .dataTables_length>input {
+ line-height: 25px;
+ text-align: right;
+ }
+}
+.dataTables_header {
+ background-color: #f6f6f6;
+ border: 1px solid #d1d1d1;
+ border-bottom: none;
+ padding: 5px;
+ position: relative;
+ .oadatatableactions {
+ display: inline-block;
+ }
+ .form-group {
+ padding-left: 8px;
+ }
+ .input-group {
+ float: right;
+ border-left: 1px solid rgba(0,0,0,.09);
+ padding-left: 8px;
+ width: 40%;
+ max-width: 350px;
+ .form-control {
+ height: 30px;
+ }
+ .clear-input {
+ height: 30px;
+ i {
+ vertical-align: text-top;
+ }
+ }
+ }
+ .input-group.dataTables_paginate {
+ width: 8%;
+ min-width: 85px;
+ }
+}
+
+::ng-deep .oadatatable {
+ border: $border-color;
+ margin-bottom: 0;
+ max-width: none!important;
+ .progress-linear {
+ display: block;
+ position: relative;
+ width: 100%;
+ height: 5px;
+ padding: 0;
+ margin: 0;
+ .container {
+ background-color: $oa-color-light-blue;
+ .bar {
+ left: 0;
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ overflow: hidden;
+ background-color: $oa-color-light-blue;
+ }
+ .bar:before{
+ display: block;
+ position: absolute;
+ content: "";
+ left: -200px;
+ width: 200px;
+ height: 100%;
+ background-color: $oa-color-blue;
+ animation: progress-loading 3s linear infinite;
+ }
+ }
+ }
+ .datatable-header {
+ background-clip: padding-box;
+ background-color: #f9f9f9;
+ background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
+ background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
+ background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0);
+ .sort-asc, .sort-desc {
+ color: $oa-color-blue;
+ }
+ .datatable-header-cell{
+ @include table-cell;
+ text-align: left;
+ font-weight: bold;
+ .datatable-header-cell-label {
+ &:after {
+ font-family: FontAwesome;
+ font-weight: 400;
+ height: 9px;
+ left: 10px;
+ line-height: 12px;
+ position: relative;
+ vertical-align: baseline;
+ width: 12px;
+ }
+ }
+ &.sortable {
+ .datatable-header-cell-label:after {
+ content: " \f0dc";
+ }
+ &.sort-active {
+ &.sort-asc .datatable-header-cell-label:after {
+ content: " \f160";
+ }
+ &.sort-desc .datatable-header-cell-label:after {
+ content: " \f161";
+ }
+ }
+ }
+ &:first-child {
+ border-left: none;
+ }
+ }
+ }
+ .datatable-body {
+ .empty-row {
+ background-color: $warning-background-color;
+ text-align: center;
+ font-weight: bold;
+ font-style: italic;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ }
+ .datatable-body-row {
+ &.clickable:hover .datatable-row-group {
+ background-color: #eee;
+ transition-property: background;
+ transition-duration: .3s;
+ transition-timing-function: linear;
+ }
+ &.datatable-row-even {
+ background-color: #ffffff;
+ }
+ &.datatable-row-odd {
+ background-color: #f6f6f6;
+ }
+ &.active, &.active:hover {
+ background-color: $bg-color-light-blue;
+ }
+ .datatable-body-cell{
+ @include table-cell;
+ &:first-child {
+ border-left: none;
+ }
+ .datatable-body-cell-label {
+ display: block;
+ }
+ }
+ }
+ }
+ .datatable-footer {
+ .selected-count, .page-count {
+ font-style: italic;
+ padding-left: 5px;
+ }
+ .datatable-pager .pager {
+ margin-right: 5px;
+ .pages {
+ & > a, & > span {
+ display: inline-block;
+ padding: 5px 10px;
+ margin-bottom: 5px;
+ border: none;
+ }
+ a:hover {
+ background-color: $oa-color-light-blue;
+ }
+ &.active > a {
+ background-color: $bg-color-light-blue;
+ }
+ }
+ }
+ }
+}
+
+@keyframes progress-loading {
+ from {
+ left: -200px;
+ width: 15%;
+ }
+ 50% {
+ width: 30%;
+ }
+ 70% {
+ width: 70%;
+ }
+ 80% {
+ left: 50%;
+ }
+ 95% {
+ left: 120%;
+ }
+ to {
+ left: 100%;
+ }
+}
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgxDatatableModule, TableColumn } from '@swimlane/ngx-datatable';
+
+import { ComponentsModule } from '../../components/components.module';
+import { TableComponent } from './table.component';
+
+describe('TableComponent', () => {
+ let component: TableComponent;
+ let fixture: ComponentFixture<TableComponent>;
+ const columns: TableColumn[] = [];
+ const createFakeData = (n) => {
+ const data = [];
+ for (let i = 0; i < n; i++) {
+ data.push({
+ a: i,
+ b: i * i,
+ c: -(i % 10)
+ });
+ }
+ return data;
+ };
+
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ declarations: [TableComponent],
+ imports: [NgxDatatableModule, FormsModule, ComponentsModule, RouterTestingModule]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TableComponent);
+ component = fixture.componentInstance;
+ });
+
+ beforeEach(() => {
+ component.data = createFakeData(100);
+ component.useData();
+ component.columns = [
+ {prop: 'a'},
+ {prop: 'b'},
+ {prop: 'c'}
+ ];
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should have rows', () => {
+ expect(component.data.length).toBe(100);
+ expect(component.rows.length).toBe(component.data.length);
+ });
+
+ it('should have an int in setLimit parsing a string', () => {
+ expect(component.limit).toBe(10);
+ expect(component.limit).toEqual(jasmine.any(Number));
+
+ const e = {target: {value: '1'}};
+ component.setLimit(e);
+ expect(component.limit).toBe(1);
+ expect(component.limit).toEqual(jasmine.any(Number));
+ e.target.value = '-20';
+ component.setLimit(e);
+ expect(component.limit).toBe(1);
+ });
+
+ it('should search for 13', () => {
+ component.search = '13';
+ expect(component.rows.length).toBe(100);
+ component.updateFilter(true);
+ expect(component.rows[0].a).toBe(13);
+ expect(component.rows[1].b).toBe(1369);
+ expect(component.rows[2].b).toBe(3136);
+ expect(component.rows.length).toBe(3);
+ });
+
+ it('should restore full table after search', () => {
+ component.search = '13';
+ expect(component.rows.length).toBe(100);
+ component.updateFilter(true);
+ expect(component.rows.length).toBe(3);
+ component.updateFilter();
+ expect(component.rows.length).toBe(100);
+ });
+
+ describe('after ngInit', () => {
+ const toggleColumn = (prop, checked) => {
+ component.toggleColumn({
+ target: {
+ name: prop,
+ checked: checked
+ }
+ });
+ };
+
+ beforeEach(() => {
+ component.ngOnInit();
+ component.table.sorts = component.sorts;
+ });
+
+ it('should have updated the column definitions', () => {
+ expect(component.columns[0].flexGrow).toBe(1);
+ expect(component.columns[1].flexGrow).toBe(2);
+ expect(component.columns[2].flexGrow).toBe(2);
+ expect(component.columns[2].resizeable).toBe(false);
+ });
+
+ it('should have table columns', () => {
+ expect(component.tableColumns.length).toBe(3);
+ expect(component.tableColumns).toEqual(component.columns);
+ });
+
+ it('should have a unique identifier which is search for', () => {
+ expect(component.identifier).toBe('a');
+ expect(component.sorts[0].prop).toBe('a');
+ expect(component.sorts).toEqual(component.createSortingDefinition('a'));
+ });
+
+ it('should remove column "a"', () => {
+ toggleColumn('a', false);
+ expect(component.table.sorts[0].prop).toBe('b');
+ expect(component.tableColumns.length).toBe(2);
+ });
+
+ it('should not be able to remove all columns', () => {
+ toggleColumn('a', false);
+ toggleColumn('b', false);
+ toggleColumn('c', false);
+ expect(component.table.sorts[0].prop).toBe('c');
+ expect(component.tableColumns.length).toBe(1);
+ });
+
+ it('should enable column "a" again', () => {
+ toggleColumn('a', false);
+ toggleColumn('a', true);
+ expect(component.table.sorts[0].prop).toBe('b');
+ expect(component.tableColumns.length).toBe(3);
+ });
+ });
+});
--- /dev/null
+import {
+ AfterContentChecked,
+ Component,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ Output,
+ TemplateRef,
+ Type,
+ ViewChild
+} from '@angular/core';
+import {
+ DatatableComponent,
+ SortDirection,
+ SortPropDir,
+ TableColumnProp
+} from '@swimlane/ngx-datatable';
+
+import * as _ from 'lodash';
+import 'rxjs/add/observable/timer';
+import { Observable } from 'rxjs/Observable';
+
+import { CdTableColumn } from '../../models/cd-table-column';
+import { CdTableSelection } from '../../models/cd-table-selection';
+
+@Component({
+ selector: 'cd-table',
+ templateUrl: './table.component.html',
+ styleUrls: ['./table.component.scss']
+})
+export class TableComponent implements AfterContentChecked, OnInit, OnChanges, OnDestroy {
+ @ViewChild(DatatableComponent) table: DatatableComponent;
+ @ViewChild('tableCellBoldTpl') tableCellBoldTpl: TemplateRef<any>;
+ @ViewChild('sparklineTpl') sparklineTpl: TemplateRef<any>;
+ @ViewChild('routerLinkTpl') routerLinkTpl: TemplateRef<any>;
+ @ViewChild('perSecondTpl') perSecondTpl: TemplateRef<any>;
+
+ // This is the array with the items to be shown.
+ @Input() data: any[];
+ // Each item -> { prop: 'attribute name', name: 'display name' }
+ @Input() columns: CdTableColumn[];
+ // Each item -> { prop: 'attribute name', dir: 'asc'||'desc'}
+ @Input() sorts?: SortPropDir[];
+ // Method used for setting column widths.
+ @Input() columnMode ?= 'flex';
+ // Display the tool header, including reload button, pagination and search fields?
+ @Input() toolHeader ?= true;
+ // Display the table header?
+ @Input() header ?= true;
+ // Display the table footer?
+ @Input() footer ?= true;
+ // Page size to show. Set to 0 to show unlimited number of rows.
+ @Input() limit ?= 10;
+
+ /**
+ * Auto reload time in ms - per default every 5s
+ * You can set it to 0, undefined or false to disable the auto reload feature in order to
+ * trigger 'fetchData' if the reload button is clicked.
+ */
+ @Input() autoReload: any = 5000;
+
+ // Which row property is unique for a row
+ @Input() identifier = 'id';
+ // Allows other components to specify which type of selection they want,
+ // e.g. 'single' or 'multi'.
+ @Input() selectionType: string = undefined;
+
+ /**
+ * Should be a function to update the input data if undefined nothing will be triggered
+ *
+ * Sometimes it's useful to only define fetchData once.
+ * Example:
+ * Usage of multiple tables with data which is updated by the same function
+ * What happens:
+ * The function is triggered through one table and all tables will update
+ */
+ @Output() fetchData = new EventEmitter();
+
+ /**
+ * This should be defined if you need access to the selection object.
+ *
+ * Each time the table selection changes, this will be triggered and
+ * the new selection object will be sent.
+ *
+ * @memberof TableComponent
+ */
+ @Output() updateSelection = new EventEmitter();
+
+ /**
+ * Use this variable to access the selected row(s).
+ */
+ selection = new CdTableSelection();
+
+ tableColumns: CdTableColumn[];
+ cellTemplates: {
+ [key: string]: TemplateRef<any>
+ } = {};
+ search = '';
+ rows = [];
+ loadingIndicator = true;
+ paginationClasses = {
+ pagerLeftArrow: 'i fa fa-angle-double-left',
+ pagerRightArrow: 'i fa fa-angle-double-right',
+ pagerPrevious: 'i fa fa-angle-left',
+ pagerNext: 'i fa fa-angle-right'
+ };
+ private subscriber;
+ private updating = false;
+
+ // Internal variable to check if it is necessary to recalculate the
+ // table columns after the browser window has been resized.
+ private currentWidth: number;
+
+ constructor() {}
+
+ ngOnInit() {
+ this._addTemplates();
+ if (!this.sorts) {
+ this.identifier = this.columns.some(c => c.prop === this.identifier) ?
+ this.identifier :
+ this.columns[0].prop + '';
+ this.sorts = this.createSortingDefinition(this.identifier);
+ }
+ this.columns.map(c => {
+ if (c.cellTransformation) {
+ c.cellTemplate = this.cellTemplates[c.cellTransformation];
+ }
+ if (!c.flexGrow) {
+ c.flexGrow = c.prop + '' === this.identifier ? 1 : 2;
+ }
+ if (!c.resizeable) {
+ c.resizeable = false;
+ }
+ return c;
+ });
+ this.tableColumns = this.columns.filter(c => !c.isHidden);
+ if (this.autoReload) { // Also if nothing is bound to fetchData nothing will be triggered
+ // Force showing the loading indicator because it has been set to False in
+ // useData() when this method was triggered by ngOnChanges().
+ this.loadingIndicator = true;
+ this.subscriber = Observable.timer(0, this.autoReload).subscribe(x => {
+ return this.reloadData();
+ });
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.subscriber) {
+ this.subscriber.unsubscribe();
+ }
+ }
+
+ ngAfterContentChecked() {
+ // If the data table is not visible, e.g. another tab is active, and the
+ // browser window gets resized, the table and its columns won't get resized
+ // automatically if the tab gets visible again.
+ // https://github.com/swimlane/ngx-datatable/issues/193
+ // https://github.com/swimlane/ngx-datatable/issues/193#issuecomment-329144543
+ if (this.table && this.table.element.clientWidth !== this.currentWidth) {
+ this.currentWidth = this.table.element.clientWidth;
+ this.table.recalculate();
+ }
+ }
+
+ _addTemplates() {
+ this.cellTemplates.bold = this.tableCellBoldTpl;
+ this.cellTemplates.sparkline = this.sparklineTpl;
+ this.cellTemplates.routerLink = this.routerLinkTpl;
+ this.cellTemplates.perSecond = this.perSecondTpl;
+ }
+
+ ngOnChanges(changes) {
+ this.useData();
+ }
+
+ setLimit(e) {
+ const value = parseInt(e.target.value, 10);
+ if (value > 0) {
+ this.limit = value;
+ }
+ }
+
+ reloadData() {
+ if (!this.updating) {
+ this.fetchData.emit();
+ this.updating = true;
+ }
+ }
+
+ refreshBtn () {
+ this.loadingIndicator = true;
+ this.reloadData();
+ }
+
+ rowIdentity() {
+ return (row) => {
+ const id = row[this.identifier];
+ if (_.isUndefined(id)) {
+ throw new Error(`Wrong identifier "${this.identifier}" -> "${id}"`);
+ }
+ return id;
+ };
+ }
+
+ useData() {
+ if (!this.data) {
+ return; // Wait for data
+ }
+ this.rows = [...this.data];
+ if (this.search.length > 0) {
+ this.updateFilter(true);
+ }
+ this.loadingIndicator = false;
+ this.updating = false;
+ }
+
+ onSelect() {
+ this.selection.update();
+ this.updateSelection.emit(_.clone(this.selection));
+ }
+
+ toggleColumn($event: any) {
+ const prop: TableColumnProp = $event.target.name;
+ const hide = !$event.target.checked;
+ if (hide && this.tableColumns.length === 1) {
+ $event.target.checked = true;
+ return;
+ }
+ _.find(this.columns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
+ this.updateColumns();
+ }
+
+ updateColumns () {
+ this.tableColumns = this.columns.filter(c => !c.isHidden);
+ const sortProp = this.table.sorts[0].prop;
+ if (!_.find(this.tableColumns, (c: CdTableColumn) => c.prop === sortProp)) {
+ this.table.onColumnSort({sorts: this.createSortingDefinition(this.tableColumns[0].prop)});
+ }
+ this.table.recalculate();
+ }
+
+ createSortingDefinition (prop: TableColumnProp): SortPropDir[] {
+ return [
+ {
+ prop: prop,
+ dir: SortDirection.asc
+ }
+ ];
+ }
+
+ updateFilter(event?) {
+ if (!event) {
+ this.search = '';
+ }
+ const val = this.search.toLowerCase();
+ const columns = this.columns;
+ // update the rows
+ this.rows = this.data.filter((d) => {
+ return (
+ columns.filter(c => {
+ return (
+ (_.isString(d[c.prop]) || _.isNumber(d[c.prop])) &&
+ (d[c.prop] + '').toLowerCase().indexOf(val) !== -1
+ );
+ }).length > 0
+ );
+ });
+ // Whenever the filter changes, always go back to the first page
+ this.table.offset = 0;
+ }
+
+ getRowClass() {
+ // Return the function used to populate a row's CSS classes.
+ return () => {
+ return {
+ clickable: !_.isUndefined(this.selectionType)
+ };
+ };
+ }
+}
--- /dev/null
+import { PasswordButtonDirective } from './password-button.directive';
+
+describe('PasswordButtonDirective', () => {
+ it('should create an instance', () => {
+ const directive = new PasswordButtonDirective(null, null);
+ expect(directive).toBeTruthy();
+ });
+});
--- /dev/null
+import { Directive, ElementRef, HostListener, Input, OnInit, Renderer2 } from '@angular/core';
+
+@Directive({
+ selector: '[cdPasswordButton]'
+})
+export class PasswordButtonDirective implements OnInit {
+ private inputElement: any;
+ private iElement: any;
+
+ @Input('cdPasswordButton') private cdPasswordButton: string;
+
+ constructor(private el: ElementRef, private renderer: Renderer2) { }
+
+ ngOnInit() {
+ this.inputElement = document.getElementById(this.cdPasswordButton);
+ this.iElement = this.renderer.createElement('i');
+ this.renderer.addClass(this.iElement, 'icon-prepend');
+ this.renderer.addClass(this.iElement, 'fa');
+ this.renderer.appendChild(this.el.nativeElement, this.iElement);
+ this.update();
+ }
+
+ private update() {
+ if (this.inputElement.type === 'text') {
+ this.renderer.removeClass(this.iElement, 'fa-eye');
+ this.renderer.addClass(this.iElement, 'fa-eye-slash');
+ } else {
+ this.renderer.removeClass(this.iElement, 'fa-eye-slash');
+ this.renderer.addClass(this.iElement, 'fa-eye');
+ }
+ }
+
+ @HostListener('click')
+ onClick() {
+ // Modify the type of the input field.
+ this.inputElement.type = (this.inputElement.type === 'password') ? 'text' : 'password';
+ // Update the button icon/tooltip.
+ this.update();
+ }
+}
--- /dev/null
+export enum CellTemplate {
+ bold = 'bold',
+ sparkline = 'sparkline',
+ perSecond = 'perSecond',
+ routerLink = 'routerLink'
+}
--- /dev/null
+export enum ViewCacheStatus {
+ ValueOk = 0,
+ ValueStale = 1,
+ ValueNone = 2,
+ ValueException = 3
+}
--- /dev/null
+import { TableColumn } from '@swimlane/ngx-datatable';
+import { CellTemplate } from '../enum/cell-template.enum';
+
+export interface CdTableColumn extends TableColumn {
+ cellTransformation?: CellTemplate;
+ isHidden?: boolean;
+}
--- /dev/null
+export class CdTableSelection {
+ selected: any[] = [];
+ hasMultiSelection: boolean;
+ hasSingleSelection: boolean;
+ hasSelection: boolean;
+
+ constructor() {
+ this.update();
+ }
+
+ /**
+ * Recalculate the variables based on the current number
+ * of selected rows.
+ */
+ update() {
+ this.hasSelection = this.selected.length > 0;
+ this.hasSingleSelection = this.selected.length === 1;
+ this.hasMultiSelection = this.selected.length > 1;
+ }
+
+ /**
+ * Get the first selected row.
+ * @return {any | null}
+ */
+ first() {
+ return this.hasSelection ? this.selected[0] : null;
+ }
+}
--- /dev/null
+import { ElementRef } from '@angular/core';
+
+import * as _ from 'lodash';
+
+export class ChartTooltip {
+ tooltipEl: any;
+ chartEl: any;
+ getStyleLeft: Function;
+ getStyleTop: Function;
+ customColors = {
+ backgroundColor: undefined,
+ borderColor: undefined
+ };
+ checkOffset = false;
+
+ /**
+ * Creates an instance of ChartTooltip.
+ * @param {ElementRef} chartCanvas Canvas Element
+ * @param {ElementRef} chartTooltip Tooltip Element
+ * @param {Function} getStyleLeft Function that calculates the value of Left
+ * @param {Function} getStyleTop Function that calculates the value of Top
+ * @memberof ChartTooltip
+ */
+ constructor(
+ chartCanvas: ElementRef,
+ chartTooltip: ElementRef,
+ getStyleLeft: Function,
+ getStyleTop: Function
+ ) {
+ this.chartEl = chartCanvas.nativeElement;
+ this.getStyleLeft = getStyleLeft;
+ this.getStyleTop = getStyleTop;
+ this.tooltipEl = chartTooltip.nativeElement;
+ }
+
+ /**
+ * Implementation of a ChartJS custom tooltip function.
+ *
+ * @param {any} tooltip
+ * @memberof ChartTooltip
+ */
+ customTooltips(tooltip) {
+ // Hide if no tooltip
+ if (tooltip.opacity === 0) {
+ this.tooltipEl.style.opacity = 0;
+ return;
+ }
+
+ // Set caret Position
+ this.tooltipEl.classList.remove('above', 'below', 'no-transform');
+ if (tooltip.yAlign) {
+ this.tooltipEl.classList.add(tooltip.yAlign);
+ } else {
+ this.tooltipEl.classList.add('no-transform');
+ }
+
+ // Set Text
+ if (tooltip.body) {
+ const titleLines = tooltip.title || [];
+ const bodyLines = tooltip.body.map(bodyItem => {
+ return bodyItem.lines;
+ });
+
+ let innerHtml = '<thead>';
+
+ titleLines.forEach(title => {
+ innerHtml += '<tr><th>' + this.getTitle(title) + '</th></tr>';
+ });
+ innerHtml += '</thead><tbody>';
+
+ bodyLines.forEach((body, i) => {
+ const colors = tooltip.labelColors[i];
+ let style = 'background:' + (this.customColors.backgroundColor || colors.backgroundColor);
+ style += '; border-color:' + (this.customColors.borderColor || colors.borderColor);
+ style += '; border-width: 2px';
+ const span = '<span class="chartjs-tooltip-key" style="' + style + '"></span>';
+ innerHtml += '<tr><td nowrap>' + span + this.getBody(body) + '</td></tr>';
+ });
+ innerHtml += '</tbody>';
+
+ const tableRoot = this.tooltipEl.querySelector('table');
+ tableRoot.innerHTML = innerHtml;
+ }
+
+ const positionY = this.chartEl.offsetTop;
+ const positionX = this.chartEl.offsetLeft;
+
+ // Display, position, and set styles for font
+ if (this.checkOffset) {
+ const halfWidth = tooltip.width / 2;
+ this.tooltipEl.classList.remove('transform-left');
+ this.tooltipEl.classList.remove('transform-right');
+ if (tooltip.caretX - halfWidth < 0) {
+ this.tooltipEl.classList.add('transform-left');
+ } else if (tooltip.caretX + halfWidth > this.chartEl.width) {
+ this.tooltipEl.classList.add('transform-right');
+ }
+ }
+
+ this.tooltipEl.style.left = this.getStyleLeft(tooltip, positionX);
+ this.tooltipEl.style.top = this.getStyleTop(tooltip, positionY);
+
+ this.tooltipEl.style.opacity = 1;
+ this.tooltipEl.style.fontFamily = tooltip._fontFamily;
+ this.tooltipEl.style.fontSize = tooltip.fontSize;
+ this.tooltipEl.style.fontStyle = tooltip._fontStyle;
+ this.tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px';
+ }
+
+ getBody(body) {
+ return body;
+ }
+
+ getTitle(title) {
+ return title;
+ }
+}
--- /dev/null
+export class Credentials {
+ username: string;
+ password: string;
+ stay_signed_in = false;
+}
--- /dev/null
+import { CephShortVersionPipe } from './ceph-short-version.pipe';
+
+describe('CephShortVersionPipe', () => {
+ it('create an instance', () => {
+ const pipe = new CephShortVersionPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'cephShortVersion'
+})
+export class CephShortVersionPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ // Expect "ceph version 1.2.3-g9asdasd (as98d7a0s8d7)"
+ const result = /ceph version\s+([^ ]+)\s+\(.+\)/.exec(value);
+ if (result) {
+ // Return the "1.2.3-g9asdasd" part
+ return result[1];
+ } else {
+ // Unexpected format, pass it through
+ return value;
+ }
+ }
+}
--- /dev/null
+import { FormatterService } from '../services/formatter.service';
+import { DimlessBinaryPipe } from './dimless-binary.pipe';
+
+describe('DimlessBinaryPipe', () => {
+ it('create an instance', () => {
+ const formatterService = new FormatterService();
+ const pipe = new DimlessBinaryPipe(formatterService);
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+import { FormatterService } from '../services/formatter.service';
+
+@Pipe({
+ name: 'dimlessBinary'
+})
+export class DimlessBinaryPipe implements PipeTransform {
+ constructor(private formatter: FormatterService) {}
+
+ transform(value: any, args?: any): any {
+ return this.formatter.format_number(value, 1024, [
+ 'B',
+ 'KiB',
+ 'MiB',
+ 'GiB',
+ 'TiB',
+ 'PiB'
+ ]);
+ }
+}
--- /dev/null
+import { FormatterService } from '../services/formatter.service';
+import { DimlessPipe } from './dimless.pipe';
+
+describe('DimlessPipe', () => {
+ it('create an instance', () => {
+ const formatterService = new FormatterService();
+ const pipe = new DimlessPipe(formatterService);
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+import { FormatterService } from '../services/formatter.service';
+
+@Pipe({
+ name: 'dimless'
+})
+export class DimlessPipe implements PipeTransform {
+ constructor(private formatter: FormatterService) {}
+
+ transform(value: any, args?: any): any {
+ return this.formatter.format_number(value, 1000, [
+ ' ',
+ 'k',
+ 'M',
+ 'G',
+ 'T',
+ 'P'
+ ]);
+ }
+}
--- /dev/null
+import { FilterPipe } from './filter.pipe';
+
+describe('FilterPipe', () => {
+ it('create an instance', () => {
+ const pipe = new FilterPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'filter'
+})
+export class FilterPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ return value.filter(row => {
+ let result = true;
+
+ args.forEach(filter => {
+ if (!filter.value) {
+ return;
+ }
+
+ result = result && filter.applyFilter(row, filter.value);
+ if (!result) {
+ return result;
+ }
+ });
+
+ return result;
+ });
+ }
+}
--- /dev/null
+import { HealthColorPipe } from './health-color.pipe';
+
+describe('HealthColorPipe', () => {
+ it('create an instance', () => {
+ const pipe = new HealthColorPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'healthColor'
+})
+export class HealthColorPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ if (value === 'HEALTH_OK') {
+ return { color: '#00bb00' };
+ } else if (value === 'HEALTH_WARN') {
+ return { color: '#ffa500' };
+ } else if (value === 'HEALTH_ERR') {
+ return { color: '#ff0000' };
+ } else {
+ return null;
+ }
+ }
+}
--- /dev/null
+import { ListPipe } from './list.pipe';
+
+describe('ListPipe', () => {
+ it('create an instance', () => {
+ const pipe = new ListPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'list'
+})
+export class ListPipe implements PipeTransform {
+ transform(value: any, args?: any): any {
+ return value.join(', ');
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { CephShortVersionPipe } from './ceph-short-version.pipe';
+import { DimlessBinaryPipe } from './dimless-binary.pipe';
+import { DimlessPipe } from './dimless.pipe';
+import { FilterPipe } from './filter.pipe';
+import { HealthColorPipe } from './health-color.pipe';
+import { ListPipe } from './list.pipe';
+import { RelativeDatePipe } from './relative-date.pipe';
+
+@NgModule({
+ imports: [CommonModule],
+ declarations: [
+ DimlessBinaryPipe,
+ HealthColorPipe,
+ DimlessPipe,
+ CephShortVersionPipe,
+ RelativeDatePipe,
+ ListPipe,
+ FilterPipe
+ ],
+ exports: [
+ DimlessBinaryPipe,
+ HealthColorPipe,
+ DimlessPipe,
+ CephShortVersionPipe,
+ RelativeDatePipe,
+ ListPipe,
+ FilterPipe
+ ],
+ providers: [
+ CephShortVersionPipe,
+ DimlessBinaryPipe,
+ DimlessPipe,
+ RelativeDatePipe,
+ ListPipe
+ ]
+})
+export class PipesModule {}
--- /dev/null
+import { RelativeDatePipe } from './relative-date.pipe';
+
+describe('RelativeDatePipe', () => {
+ it('create an instance', () => {
+ const pipe = new RelativeDatePipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+import * as moment from 'moment';
+
+@Pipe({
+ name: 'relativeDate'
+})
+export class RelativeDatePipe implements PipeTransform {
+ constructor() {}
+
+ transform(value: any, args?: any): any {
+ if (!value) {
+ return 'unknown';
+ }
+ return moment(value * 1000).fromNow();
+ }
+}
--- /dev/null
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
+
+import { AuthStorageService } from './auth-storage.service';
+
+@Injectable()
+export class AuthGuardService implements CanActivate {
+
+ constructor(private router: Router, private authStorageService: AuthStorageService) {
+ }
+
+ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+ if (this.authStorageService.isLoggedIn()) {
+ return true;
+ }
+ this.router.navigate(['/login']);
+ return false;
+ }
+}
--- /dev/null
+import {
+ HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest,
+ HttpResponse
+} from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { ToastsManager } from 'ng2-toastr';
+import 'rxjs/add/operator/do';
+import { Observable } from 'rxjs/Observable';
+
+import { AuthStorageService } from './auth-storage.service';
+
+@Injectable()
+export class AuthInterceptorService implements HttpInterceptor {
+
+ constructor(private router: Router,
+ private authStorageService: AuthStorageService,
+ public toastr: ToastsManager) {
+ }
+
+ intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+ return next.handle(request).do((event: HttpEvent<any>) => {
+ if (event instanceof HttpResponse) {
+ // do nothing
+ }
+ }, (err: any) => {
+ if (err instanceof HttpErrorResponse) {
+ if (err.status === 404) {
+ this.router.navigate(['/404']);
+ return;
+ }
+
+ this.toastr.error(err.error.detail || '', `${err.status} - ${err.statusText}`);
+ if (err.status === 401) {
+ this.authStorageService.remove();
+ this.router.navigate(['/login']);
+ }
+ }
+ });
+ }
+}
--- /dev/null
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class AuthStorageService {
+
+ constructor() {
+ }
+
+ set(username: string) {
+ localStorage.setItem('dashboard_username', username);
+ }
+
+ remove() {
+ localStorage.removeItem('dashboard_username');
+ }
+
+ isLoggedIn() {
+ return localStorage.getItem('dashboard_username') !== null;
+ }
+
+}
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Credentials } from '../models/credentials';
+import { AuthStorageService } from './auth-storage.service';
+
+@Injectable()
+export class AuthService {
+
+ constructor(private authStorageService: AuthStorageService,
+ private http: HttpClient) {
+ }
+
+ login(credentials: Credentials) {
+ return this.http.post('api/auth', credentials).toPromise().then((resp: Credentials) => {
+ this.authStorageService.set(resp.username);
+ });
+ }
+
+ logout() {
+ return this.http.delete('api/auth').toPromise().then(() => {
+ this.authStorageService.remove();
+ });
+ }
+}
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { ConfigurationService } from './configuration.service';
+
+describe('ConfigurationService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [ConfigurationService],
+ imports: [HttpClientTestingModule, HttpClientModule]
+ });
+ });
+
+ it(
+ 'should be created',
+ inject([ConfigurationService], (service: ConfigurationService) => {
+ expect(service).toBeTruthy();
+ })
+ );
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class ConfigurationService {
+ constructor(private http: HttpClient) {}
+
+ getConfigData() {
+ return this.http.get('api/cluster_conf/');
+ }
+}
--- /dev/null
+import { inject, TestBed } from '@angular/core/testing';
+
+import { FormatterService } from './formatter.service';
+
+describe('FormatterService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [FormatterService]
+ });
+ });
+
+ it('should be created', inject([FormatterService], (service: FormatterService) => {
+ expect(service).toBeTruthy();
+ }));
+});
--- /dev/null
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class FormatterService {
+ constructor() {}
+
+ truncate(n, maxWidth) {
+ const stringized = n.toString();
+ const parts = stringized.split('.');
+ if (parts.length === 1) {
+ // Just an int
+ return stringized;
+ } else {
+ const fractionalDigits = maxWidth - parts[0].length - 1;
+ if (fractionalDigits <= 0) {
+ // No width available for the fractional part, drop
+ // it and the decimal point
+ return parts[0];
+ } else {
+ return stringized.substring(0, maxWidth);
+ }
+ }
+ }
+
+ format_number(n, divisor, units) {
+ const width = 4;
+ let unit = 0;
+
+ if (n == null) {
+ // People shouldn't really be passing null, but let's
+ // do something sensible instead of barfing.
+ return '-';
+ }
+
+ while (Math.floor(n / divisor ** unit).toString().length > width - 1) {
+ unit = unit + 1;
+ }
+
+ let truncatedFloat;
+ if (unit > 0) {
+ truncatedFloat = this.truncate(
+ (n / Math.pow(divisor, unit)).toString(),
+ width
+ );
+ } else {
+ truncatedFloat = this.truncate(n, width);
+ }
+
+ return truncatedFloat === '' ? '-' : (truncatedFloat + units[unit]);
+ }
+}
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class HostService {
+
+ constructor(private http: HttpClient) {
+ }
+
+ list() {
+ return this.http.get('api/host').toPromise().then((resp: any) => {
+ return resp;
+ });
+ }
+}
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class PoolService {
+
+ constructor(private http: HttpClient) {
+ }
+
+ rbdPoolImages(pool) {
+ return this.http.get(`api/rbd/${pool}`).toPromise().then((resp: any) => {
+ return resp;
+ });
+ }
+}
--- /dev/null
+import { HttpClientModule } from '@angular/common/http';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { RbdMirroringService } from './rbd-mirroring.service';
+
+describe('RbdMirroringService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [RbdMirroringService],
+ imports: [HttpClientTestingModule, HttpClientModule]
+ });
+ });
+
+ it('should be created', inject([RbdMirroringService], (service: RbdMirroringService) => {
+ expect(service).toBeTruthy();
+ }));
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class RbdMirroringService {
+ constructor(private http: HttpClient) {}
+
+ get() {
+ return this.http.get('api/rbdmirror');
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { ConfigurationService } from './configuration.service';
+import { FormatterService } from './formatter.service';
+import { RbdMirroringService } from './rbd-mirroring.service';
+import { SummaryService } from './summary.service';
+import { TcmuIscsiService } from './tcmu-iscsi.service';
+
+@NgModule({
+ imports: [CommonModule],
+ declarations: [],
+ providers: [
+ FormatterService,
+ SummaryService,
+ TcmuIscsiService,
+ ConfigurationService,
+ RbdMirroringService
+ ]
+})
+export class ServicesModule { }
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { inject, TestBed } from '@angular/core/testing';
+
+import { SharedModule } from '../shared.module';
+import { SummaryService } from './summary.service';
+
+describe('SummaryService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [SummaryService],
+ imports: [HttpClientTestingModule, SharedModule]
+ });
+ });
+
+ it(
+ 'should be created',
+ inject([SummaryService], (service: SummaryService) => {
+ expect(service).toBeTruthy();
+ })
+ );
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Subject } from 'rxjs/Subject';
+
+import { AuthStorageService } from './auth-storage.service';
+
+@Injectable()
+export class SummaryService {
+ // Observable sources
+ private summaryDataSource = new Subject();
+
+ // Observable streams
+ summaryData$ = this.summaryDataSource.asObservable();
+
+ constructor(private http: HttpClient, private authStorageService: AuthStorageService) {
+ this.refresh();
+ }
+
+ refresh() {
+ if (this.authStorageService.isLoggedIn()) {
+ this.http.get('api/summary').subscribe(data => {
+ this.summaryDataSource.next(data);
+ });
+ }
+
+ setTimeout(() => {
+ this.refresh();
+ }, 5000);
+ }
+}
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class TcmuIscsiService {
+
+ constructor(private http: HttpClient) {
+ }
+
+ tcmuiscsi() {
+ return this.http.get('api/tcmuiscsi').toPromise().then((resp: any) => {
+ return resp;
+ });
+ }
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { ComponentsModule } from './components/components.module';
+import { DataTableModule } from './datatable/datatable.module';
+import { PasswordButtonDirective } from './directives/password-button.directive';
+import { PipesModule } from './pipes/pipes.module';
+import { AuthGuardService } from './services/auth-guard.service';
+import { AuthStorageService } from './services/auth-storage.service';
+import { AuthService } from './services/auth.service';
+import { FormatterService } from './services/formatter.service';
+import { HostService } from './services/host.service';
+import { PoolService } from './services/pool.service';
+import { ServicesModule } from './services/services.module';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ PipesModule,
+ ComponentsModule,
+ ServicesModule,
+ DataTableModule
+ ],
+ declarations: [
+ PasswordButtonDirective
+ ],
+ exports: [
+ ComponentsModule,
+ PipesModule,
+ ServicesModule,
+ PasswordButtonDirective,
+ DataTableModule
+ ],
+ providers: [
+ AuthService,
+ AuthStorageService,
+ AuthGuardService,
+ PoolService,
+ FormatterService,
+ HostService
+ ],
+})
+export class SharedModule {}
--- /dev/null
+$warning-background-color: #fff3cd;
+$oa-color-blue: #288cea;
+$oa-color-light-blue: #afd9ee;
+$bg-color-light-blue: #d9edf7;
+$border-color: 1px solid #d1d1d1;
+@mixin table-cell {
+ padding: 5px;
+ border: none;
+ border-left: $border-color;
+ border-bottom: $border-color;
+}
--- /dev/null
+export const environment = {
+ production: true
+};
--- /dev/null
+// The file contents for the current environment will overwrite these during build.
+// The build system defaults to the dev environment which uses `environment.ts`, but if you do
+// `ng build --env=prod` then `environment.prod.ts` will be used instead.
+// The list of which env maps to which file can be found in `.angular-cli.json`.
+
+export const environment = {
+ production: false
+};
--- /dev/null
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Ceph</title>
+
+ <script>
+ document.write('<base href="' + document.location+ '" />');
+ </script>
+
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
+</head>
+<body>
+ <noscript>
+ <div class="noscript container"
+ ng-if="false">
+ <div class="jumbotron alert alert-danger">
+ <h2 i18n>JavaScript required!</h2>
+ <p i18n>A browser with JavaScript enabled is required in order to use this service.</p>
+ <p i18n>When using Internet Explorer, please check your security settings and add this address to your trusted sites.</p>
+ </div>
+ </div>
+ </noscript>
+
+ <cd-root></cd-root>
+</body>
+</html>
--- /dev/null
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule)
+ .catch(err => console.log(err));
--- /dev/null
+/*
+ Basics
+ Branding
+ Breadcrumb
+ Buttons
+ Dropdown
+ Grid
+ Modal
+ Navbar
+ Navs
+ Notifications
+ Pagination
+ Panel
+ Table
+ Typo
+
+ Login
+ Statistics
+
+ ApiRecorder
+ Caret
+ Datatables
+ Feedback
+ FlexElement
+ Grafana
+ Graph
+ Progressbar
+ TagForm
+ Trees
+ CSS Fix
+*/
+
+@import 'defaults';
+
+$fa-font-path: "../node_modules/font-awesome/fonts";
+@import "../node_modules/font-awesome/scss/font-awesome";
+
+/* Basics */
+html {
+ background-color: #ffffff;
+}
+html,
+body {
+ width: 100%;
+ height: 100%;
+ font-size: 12px;
+}
+optgroup {
+ font-weight: bold;
+ font-style: italic;
+}
+option {
+ font-weight: normal;
+ font-style: normal;
+}
+.full-height {
+ height: 100%;
+}
+.vertical-align {
+ display: flex;
+ align-items: center;
+}
+.loading {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+}
+.bg-color-darken {
+ background-color: #404040!important;
+}
+.bg-color-greenLight {
+ background-color: #71843f!important;
+}
+.bg-color-red {
+ background-color: #a90329!important;
+}
+.no-margin {
+ margin: 0;
+}
+.margin-left-md {
+ margin-left: 15px
+}
+.margin-right-md {
+ margin-right: 15px
+}
+.margin-right-sm {
+ margin-right: 10px
+}
+.margin-bottom-md {
+ margin-bottom: 15px
+}
+.no-padding {
+ padding: 0;
+}
+.small-padding {
+ padding: 5px;
+}
+.no-border {
+ border: 0px;
+ box-shadow: 0px 0px 0px !important;
+}
+.no-wrap {
+ white-space: nowrap;
+}
+.strikethrough {
+ text-decoration: line-through;
+}
+.italic {
+ font-style: italic;
+}
+.bold {
+ font-weight: bold;
+}
+.text-right {
+ text-align: right;
+}
+.text-monospace {
+ font-family: monospace;
+}
+
+/* Branding */
+.navbar-openattic .navbar-brand,
+.navbar-openattic .navbar-brand:hover{
+ color: #ececec;
+ height: auto;
+ margin: 15px 0 15px 20px;
+ padding: 0;
+ -webkit-align-self: flex-start;
+ align-self: flex-start;
+}
+.navbar-openattic .navbar-brand>img {
+ height: 25px;
+}
+
+/* Breadcrumb */
+.breadcrumb {
+ padding: 8px 0;
+ background-color: transparent;
+ border-radius: 0;
+}
+.breadcrumb>li+li:before {
+ padding: 0 5px 0 7px;
+ color: #474544;
+ font-family: "FontAwesome";
+ content: "\f101";
+}
+.breadcrumb>li>span {
+ color: #474544;
+}
+
+/* Icons */
+.icon-warning {
+ color: #f0ad4e;
+}
+.icon-danger {
+ color: #c9302c;
+}
+
+/* Buttons */
+.btn-openattic {
+ color: #ececec;
+ background-color: $oa-color-blue;
+ border-color: $oa-color-blue;
+}
+.btn-primary {
+ color: #ececec;
+ background-color: $oa-color-blue;
+ border-color: #2172bf;
+}
+.btn-primary:hover,
+.btn-primary:focus,
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+ color: #ececec;
+ background-color: #2582D9;
+ border-color: #2172bf;
+}
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+ background-image: none;
+}
+.btn-primary.disabled,
+.btn-primary[disabled],
+fieldset[disabled] .btn-primary,
+.btn-primary.disabled:hover,
+.btn-primary[disabled]:hover,
+fieldset[disabled] .btn-primary:hover,
+.btn-primary.disabled:focus,
+.btn-primary[disabled]:focus,
+fieldset[disabled] .btn-primary:focus,
+.btn-primary.disabled:active,
+.btn-primary[disabled]:active,
+fieldset[disabled] .btn-primary:active,
+.btn-primary.disabled.active,
+.btn-primary[disabled].active,
+fieldset[disabled] .btn-primary.active {
+ background-color: $oa-color-blue;
+ border-color: #2172bf;
+}
+.btn-primary .badge {
+ color: $oa-color-blue;
+ background-color: #ececec;
+}
+.btn-primary .caret {
+ color: #ececec;
+}
+.btn-group>.btn>i.fa,
+button.btn.btn-label>i.fa {
+ /** Add space between icon and text */
+ padding-right: 5px;
+}
+
+/* Dropdown */
+.dropdown-menu {
+ min-width: 50px;
+}
+.dropdown-menu>li>a {
+ color: #474544;
+ cursor: pointer;
+}
+.dropdown-menu>li>a>i.fa {
+ /** Add space between icon and text */
+ padding-right: 5px;
+}
+.dropdown-menu>.active>a {
+ color: #ececec;
+ background-color: $oa-color-blue;
+}
+.dataTables_wrapper .dropdown-menu>li.divider {
+ cursor: auto;
+}
+
+/* Grid */
+.container,
+.container-fluid {
+ padding-left: 30px;
+ padding-right: 30px;
+}
+.row {
+ margin-left: -30px;
+ margin-right: -30px;
+}
+.col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9,
+.col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9,
+.col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9,
+.col-xs-1, .col-xs-10, .col-xs-11, .col-xs-12, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9 {
+ padding-left: 30px;
+ padding-right: 30px;
+}
+
+/* Modal */
+.modal-dialog {
+ margin: 30px auto !important;
+}
+.modal .modal-content .openattic-modal-header,
+.modal .modal-content .openattic-modal-content,
+.modal .modal-content .openattic-modal-footer {
+ padding: 10px 20px;
+}
+.modal .modal-content .openattic-modal-header {
+ border-bottom: 1px solid #cccccc;
+ border-radius: 5px 5px 0 0;
+ background-color: #f5f5f5;
+}
+.modal .modal-content .openattic-modal-content {
+ padding: 20px 20px 10px 20px;
+ overflow-x: auto;
+ max-height: 70vh;
+}
+.modal .modal-content .openattic-modal-content p {
+ margin-bottom: 10px;
+}
+.modal .modal-content .openattic-modal-content legend {
+ font-size: 1.833em;
+}
+.modal .modal-content .openattic-modal-footer {
+ border-top: 1px solid #cccccc;
+ border-radius: 0 0 5px 5px;
+ background-color: #f5f5f5;
+}
+.modal .modal-content .openattic-modal-header span {
+ display: block;
+ font-size: 16px; /* Same as .panel-title */
+}
+
+/* Modal Table (Task Queue) */
+table.task-queue-table thead {
+ display: flex;
+ flex-flow: row;
+}
+table.task-queue-table thead tr {
+ display: flex;
+ align-items: stretch;
+ width: 100%;
+}
+table.task-queue-table tbody {
+ display: flex;
+ flex-flow: row wrap;
+}
+table.task-queue-table tbody tr {
+ display: flex;
+ width: 100%
+}
+table.task-queue-table > * > tr > * {
+ flex: 1;
+}
+table.task-queue-table > * > tr > .oadatatablecheckbox {
+ flex: 0;
+}
+div.task-queue-modal-content {
+ height: 40em;
+}
+div.openattic-modal-content div.modal-scroll {
+ max-height: 26em;
+ overflow: auto;
+ border-bottom: 1px solid #e1e1e1;
+}
+div.task-queue-modal-content div.dataTables_wrapper {
+ margin-bottom: 0;
+}
+div.task-queue-modal-content div.dataTables_wrapper th.oadatatablecheckbox {
+ width: 100%;
+}
+div.task-queue-modal-content div.dataTables_wrapper div.widget-toolbar.tc_refreshBtn{
+ width: 36px;
+}
+ul.task-queue-pagination {
+ display: table;
+ margin: auto;
+ padding-top: 10px;
+}
+
+/* Navbar */
+.navbar-openattic {
+ margin-bottom: 0;
+ background: #474544;
+ border: 0;
+ border-radius: 0;
+ border-top: 4px solid $oa-color-blue;
+ font-size: 1.2em;
+}
+.navbar-openattic .navbar-header {
+ display: flex;
+ float: none;
+}
+.navbar-openattic .navbar-toggle {
+ margin-left: auto;
+ border: 0;
+}
+.navbar-openattic .navbar-toggle:focus,
+.navbar-openattic .navbar-toggle:hover {
+ background-color: transparent;
+ outline: 0;
+}
+.navbar-openattic .navbar-toggle .icon-bar {
+ background-color: #ececec;
+}
+.navbar-openattic .navbar-toggle:focus .icon-bar,
+.navbar-openattic .navbar-toggle:hover .icon-bar {
+ -webkit-box-shadow: 0 0 3px #fff;
+ box-shadow: 0 0 3px #fff;
+}
+.navbar-openattic .navbar-collapse {
+ padding: 0;
+}
+.navbar-openattic .navbar-nav>li>a,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a {
+ color: #ececec;
+ line-height: 1;
+ padding: 10px 20px;
+ position: relative;
+ display: block;
+ text-decoration: none;
+}
+.navbar-openattic .navbar-nav>li>a:focus,
+.navbar-openattic .navbar-nav>li>a:hover,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a:focus,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover {
+ color: #ececec;
+}
+.navbar-openattic .navbar-nav>li>a:hover,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover {
+ background-color: #505050;
+}
+.navbar-openattic .navbar-nav>.open>a,
+.navbar-openattic .navbar-nav>.open>a:hover,
+.navbar-openattic .navbar-nav>.open>a:focus,
+.navbar-openattic .navbar-nav>.open>.oa-navbar>a,
+.navbar-openattic .navbar-nav>.open>.oa-navbar>a:hover,
+.navbar-openattic .navbar-nav>.open>.oa-navbar>a:focus {
+ color: #ececec;
+ border-color: transparent;
+ background-color: transparent;
+}
+.navbar-openattic .navbar-primary>li>a {
+ border: 0;
+}
+.navbar-openattic .navbar-primary>.active>a,
+.navbar-openattic .navbar-primary>.active>a:hover,
+.navbar-openattic .navbar-primary>.active>a:focus {
+ color: #ececec;
+ background-color: $oa-color-blue;
+ border: 0;
+}
+.navbar-openattic .navbar-utility a,
+.navbar-openattic .navbar-utility .fa{
+ font-size: 1.0em;
+}
+.navbar-openattic .navbar-utility>.active>a {
+ color: #ececec;
+ background-color: #505050;
+}
+.navbar-openattic .navbar-utility>li>.open>a,
+.navbar-openattic .navbar-utility>li>.open>a:hover,
+.navbar-openattic .navbar-utility>li>.open>a:focus {
+ color: #ececec;
+ border-color: transparent;
+ background-color: transparent;
+}
+@media (min-width: 768px) {
+ .navbar-openattic .navbar-primary>li>a {
+ border-bottom: 4px solid transparent;
+ }
+ .navbar-openattic .navbar-primary>.active>a,
+ .navbar-openattic .navbar-primary>.active>a:hover,
+ .navbar-openattic .navbar-primary>.active>a:focus {
+ background-color: transparent;
+ border-bottom: 4px solid $oa-color-blue;
+ }
+ .navbar-openattic .navbar-utility {
+ border-bottom: 0;
+ font-size: 11px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+}
+@media (max-width: 767px) {
+ .navbar-openattic .navbar-nav {
+ margin: 0;
+ }
+ .navbar-openattic .navbar-collapse,
+ .navbar-openattic .navbar-form {
+ border-color: #ececec;
+ }
+ .navbar-openattic .navbar-collapse {
+ padding: 0;
+ }
+ .navbar-nav .open .dropdown-menu {
+ padding-top: 0;
+ padding-bottom: 0;
+ background-color: #505050;
+ }
+ .navbar-nav .open .dropdown-menu .dropdown-header,
+ .navbar-nav .open .dropdown-menu>li>a {
+ padding: 5px 15px 5px 35px;
+ }
+ .navbar-openattic .navbar-nav .open .dropdown-menu>li>a {
+ color: #ececec;
+ }
+ .navbar-openattic .navbar-nav .open .dropdown-menu>.active>a {
+ color: #ececec;
+ background-color: $oa-color-blue;
+ }
+ .navbar-openattic .navbar-nav>li>a:hover {
+ background-color: $oa-color-blue;
+ }
+ .navbar-openattic .navbar-utility {
+ border-top: 1px solid #ececec;
+ }
+ .navbar-openattic .navbar-primary>.active>a,
+ .navbar-openattic .navbar-primary>.active>a:hover,
+ .navbar-openattic .navbar-primary>.active>a:focus {
+ background-color: $oa-color-blue;
+ }
+}
+
+/* Navs */
+.nav-tabs {
+ margin-bottom: 15px;
+}
+.nav-tabs-openattic {
+ margin-top: -15px;
+ margin-bottom: 15px;
+}
+.nav-tabs-openattic>li>a {
+ padding: 7px 15px 4px 15px;
+}
+.nav-tabs-openattic>li.active>a,
+.nav-tabs-openattic>li.active>a:active,
+.nav-tabs-openattic>li.active>a:focus,
+.nav-tabs-openattic>li.active>a:hover {
+ border: 0!important;
+ border-bottom: 3px solid $oa-color-blue!important;
+}
+
+/* Notifications */
+#toasty .toast.toasty-theme-bootstrap {
+ opacity: 1
+}
+
+/* Pagination */
+.pagination {
+ display: block;
+ margin: 0;
+}
+.pagination>.disabled>a,
+.pagination>.disabled>a:focus,
+.pagination>.disabled>a:hover,
+.pagination>.disabled>span,
+.pagination>.disabled>span:focus,
+.pagination>.disabled>span:hover {
+ -webkit-box-shadow: none;
+ box-shadow: none;
+ cursor: not-allowed;
+ background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
+ background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
+ background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
+}
+.pagination>.active>a,
+.pagination>.active>a:focus,
+.pagination>.active>a:hover,
+.pagination>.active>span,
+.pagination>.active>span:focus,
+.pagination>.active>span:hover,
+.pagination>.disabled>a,
+.pagination>.disabled>a:focus,
+.pagination>.disabled>a:hover,
+.pagination>.disabled>span,
+.pagination>.disabled>span:focus,
+.pagination>.disabled>span:hover,
+.pagination>li>a,
+.pagination>li>span,
+.panel-group
+.panel-heading {
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0);
+}
+.pagination>li>a,
+.pagination>li>span {
+ background-color: #eee;
+ background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
+ background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
+ background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
+ border-color: #b7b7b7;
+ color: #4d5258;
+ cursor: pointer;
+ font-weight: 600;
+ padding: 2px 10px;
+}
+.pagination>.active>span,
+.pagination>.active>span:focus,
+.pagination>.active>span:hover {
+ color: $oa-color-blue;
+ border-color: #fff #e1e1e1 #f4f4f4;
+ border-width: 0 1px;
+}
+
+/* Panel */
+.panel .panel-toolbar {
+ float: right;
+}
+.panel .panel-toolbar div {
+ display: inline-block;
+}
+.panel .panel-toolbar>a,
+.panel .panel-toolbar>.dropdown>a {
+ padding-left: 5px;
+}
+.panel-dashboard {
+ height: 100%;
+ padding-top: 60px;
+}
+.panel-dashboard>.panel-heading {
+ cursor: move;
+ position: relative;
+ margin-top: -60px;
+ width: 100%;
+}
+.panel-dashboard>.panel-heading>.panel-title {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ -o-text-overflow: ellipsis;
+}
+.panel-dashboard>.panel-heading>.toolbar a {
+ text-decoration: none;
+}
+.panel-dashboard>.panel-body {
+ height: 100%;
+ overflow: auto;
+}
+.panel-dashboard>.panel-body .indent {
+ margin-top: 10px;
+ margin-left: 10px;
+}
+.panel-dashboard .overlay {
+ position: absolute;
+ bottom: 5px;
+ right: 5px;
+ z-index: 10;
+}
+.panel-dashboard .max-height {
+ height: 100%;
+}
+.panel-dashboard .max-height.alert-is-shown {
+ height: 85%;
+}
+.panel-dashboard .fa-2x{
+ vertical-align: middle;
+ margin-right: 0.5em;
+}
+.panel-dashboard .alert.bottom-margin-zero {
+ margin-bottom: 0;
+}
+.panel-openattic {
+ border: $border-color;
+ border-top: 0;
+ border-radius: 0;
+}
+.panel-openattic>.panel-heading {
+ border-top: 2px solid $oa-color-blue;
+ border-radius: 0;
+ padding: 20px 15px;
+}
+.panel-openattic>.panel-heading>.panel-title {
+ color: #333333;
+ font-size: 1.333em;
+ margin: 0;
+ padding: 0;
+}
+.panel-openattic>.panel-body {
+ background: #ffffff;
+ border-top: $border-color;
+ padding: 10px 15px;
+}
+.panel-openattic>.panel-footer {
+ background: #ffffff;
+ border-top: $border-color;
+}
+
+/* Typo */
+a {
+ color: $oa-color-blue;
+}
+a:hover,
+a:focus{
+ color: #474544;
+}
+h1 {
+ letter-spacing: -1px;
+ font-size: 2em;
+}
+h2 {
+ letter-spacing: -1px;
+ font-size: 1.833em;
+}
+h3{
+ display: block;
+ font-size: 1.583em;
+ font-weight: 400;
+}
+h3.sub-title {
+ color: #666666;
+ margin-left: 15px;
+}
+h4{
+ font-size: 1.5em;
+ line-height: normal
+}
+h5{
+ font-size: 1.417em;
+ font-weight: 300;
+ line-height: normal;
+}
+h6{
+ font-size: 1.25em;
+ font-weight: 700;
+ line-height: normal;
+}
+
+/*************************************************************/
+
+/* Statistics */
+.statistics-content {
+ margin: 0 -20px;
+}
+
+/*************************************************************/
+
+/* ApiRecorder */
+.apirecorder {
+ resize: none;
+ width:100%;
+}
+.apirecorder-enabled {
+ color: red;
+}
+
+/* Caret */
+.caret {
+ color: $oa-color-blue;
+}
+
+/* Feedback */
+#feedback .feedback-button {
+ position: fixed;
+ top: 50%;
+ right: 0;
+ padding: 2px 16px;
+ cursor: pointer;
+ color: #ffffff;
+ font-size: 1.2em;
+ font-weight: 700;
+ background-color: $oa-color-blue;
+ border-radius: 5px 5px 0 0;
+ z-index: 99999;
+}
+#feedback .feedback-button:hover {
+ background-color: #2172bf;
+}
+#feedback .feedback-button-transform {
+ -webkit-transform: rotate(-90deg) translate(50%, -100%);
+ -moz-transform: rotate(-90deg) translate(50%, -100%);
+ -ms-transform: rotate(-90deg) translate(50%, -100%);
+ -o-transform: rotate(-90deg) translate(50%, -100%);
+ transform: rotate(-90deg) translate(50%, -100%);
+ -webkit-transform-origin: top right;
+ -moz-transform-origin: top right;
+ -ms-transform-origin: top right;
+ -o-transform-origin: top right;
+ transform-origin: top right;
+}
+#feedback .feedback-button-active {
+ right: 299px;
+}
+#feedback .feedback-button .fa,
+#feedback .feedback-button .glyphicon{
+ padding-right: 6px;
+}
+#feedback .feedback-panel {
+ position: fixed;
+ top: 0;
+ right: -300px;
+ padding: 20px;
+ width: 300px;
+ height: 100%;
+ background-color: #ffffff;
+ border-left: 5px solid $oa-color-blue;
+ z-index: 99999;
+ overflow-y: auto;
+}
+#feedback .feedback-panel-active {
+ right: 0;
+}
+#feedback .feedback-transition {
+ transition: right 150ms cubic-bezier(0.0, 0.0, 0.2, 1);
+}
+
+/* FlexElement */
+/* Container */
+.flex-container {
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+}
+.flex-wrap {
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+}
+.flex-nowrap {
+ -webkit-flex-wrap: nowrap;
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+}
+.flex-row {
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+}
+.flex-column {
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+}
+/* Items */
+.flex-item {
+ margin-bottom: 10px;
+ padding: 15px;
+}
+.flex-item-1 { -webkit-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; padding: 0 10px; }
+.flex-item-2 { -webkit-flex: 2; -moz-flex: 2; -ms-flex: 2; flex: 2; padding: 0 10px; }
+.flex-item-3 { -webkit-flex: 3; -moz-flex: 3; -ms-flex: 3; flex: 3; padding: 0 10px; }
+.flex-item-4 { -webkit-flex: 4; -moz-flex: 4; -ms-flex: 4; flex: 4; padding: 0 10px; }
+.flex-item-5 { -webkit-flex: 5; -moz-flex: 5; -ms-flex: 5; flex: 5; padding: 0 10px; }
+.flex-item-6 { -webkit-flex: 6; -moz-flex: 6; -ms-flex: 6; flex: 6; padding: 0 10px; }
+.flex-item-7 { -webkit-flex: 7; -moz-flex: 7; -ms-flex: 7; flex: 7; padding: 0 10px; }
+.flex-item-8 { -webkit-flex: 8; -moz-flex: 8; -ms-flex: 8; flex: 8; padding: 0 10px; }
+.flex-item-9 { -webkit-flex: 9; -moz-flex: 9; -ms-flex: 9; flex: 9; padding: 0 10px; }
+
+/* Grafana */
+.grafana-container {
+ margin-top: 20px;
+ height: 64px;
+ background:url(./assets/loading.gif) center center no-repeat;
+}
+.grafana {
+ width: 100%;
+ min-height: 600px;
+}
+
+/* Progressbar */
+.progress-bar {
+ background-image: none !important;
+}
+.progress-bar-info {
+ background-color: $oa-color-blue;
+}
+.progress-bar-freespace {
+ background-color: #ddd;
+}
+.progress-bar-stolenspace {
+ background-color: #aaa;
+}
+.progress-bar-outer{
+ margin-top: 5px !important;
+}
+.progress-bar-outer div {
+ border-radius: 31px;
+ background-color: #ffffff;
+ border: 1px solid #ccc;
+ box-shadow: 0 0 0 0;
+ -webkit-box-shadow: 0 0 0 0;
+ -moz-box-shadow: 0 0 0 0;
+ margin: 0;
+ height: 16px;
+}
+.progress-bar-outer div div {
+ background-color: #0091d9;
+}
+.progress-bar-outer div div span {
+ position: relative;
+ top: -3px;
+}
+.oaprogress {
+ position: relative;
+ margin-bottom: 0;
+}
+.oaprogress div.progress-bar {
+ position: static;
+}
+.oaprogress span {
+ position: absolute;
+ display: block;
+ width: 100%;
+ color: black;
+ font-weight: normal;
+}
+
+tags-input .tags {
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
+}
+
+/* TagForm */
+.tag-form label {
+ display: block;
+ margin-bottom: 6px;
+ line-height: 19px;
+ font-weight: 400;
+ font-size: 13px;
+ color: #333;
+ text-align: left;
+ white-space: normal;
+}
+
+/* Trees */
+.tree {
+ min-height: 20px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+.tree>ul {
+ padding-left: 0;
+}
+.tree ul ul {
+ padding-left: 34px;
+ padding-top: 10px;
+}
+.tree li {
+ list-style-type: none;
+ margin: 0;
+ padding: 5px;
+ position: relative;
+}
+.tree li span {
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ border: 1px dotted #999;
+ border-radius: 5px;
+ display: inline-block;
+ padding: 3px 8px;
+ text-decoration: none;
+ -webkit-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
+ -moz-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
+ -o-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
+ transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
+}
+.tree>ul>li::after,
+.tree>ul>li:before {
+ border: 0;
+}
+.tree li:after,
+.tree li:before {
+ content: '';
+ left: -20px;
+ position: absolute;
+ right: auto;
+}
+.tree li:before {
+ border-left: 1px solid #999;
+ bottom: 50px;
+ height: 100%;
+ top: -11px;
+ width: 1px;
+ -webkit-transition: "border-color 0.1s ease 0.1s";
+ -moz-transition: "border-color 0.1s ease 0.1s";
+ -o-transition: "border-color 0.1s ease 0.1s";
+ transition: "border-color 0.1s ease 0.1s";
+}
+.tree li:after {
+ border-top: 1px solid #999;
+ height: 20px;
+ top: 18px;
+ width: 25px;
+}
+.tree li:last-child::before {
+ height: 30px;
+}
+
+.scrollable-menu {
+ height: auto;
+ max-height: 200px;
+ overflow-x: hidden;
+}
+
+.toggle, .toggle-on, .toggle-off {
+ border-radius: 20px;
+}
+
+.toggle .toggle-handle {
+ border-radius: 20px;
+}
+
+/* CSS Fix */
+a {
+ cursor: pointer;
+}
+form .input-group-addon {
+ color: #a2a2a2 !important;
+ background-color: transparent;
+}
+uib-accordion .panel-title,
+.panel .accordion-title {
+ font-size: 14px !important;
+}
+.panel-body h2:first-child {
+ margin-top: 0;
+}
+.actions {
+ padding-bottom: 10px;
+}
+.pull-left {
+ float: left;
+}
+.code-clogs {
+ display: block;
+ padding: 9px;
+ margin: 0 0 10px;
+ font-size: 13px;
+ line-height: 1.42857143;
+ color: #333;
+ word-break: break-all;
+ word-wrap: break-word;
+ background-color: #f5f5f5;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
+}
+.degree-sign:after {
+ content: "\00B0 C"!important;
+}
+.formactions.well {
+ overflow: auto;
+ padding: 10px 20px;
+}
+.disabled {
+ pointer-events: none;
+}
+.clickable {
+ cursor: pointer;
+}
+.non-clickable {
+ cursor: initial;
+}
+.locked {
+ cursor: default!important;
+}
+.list-nomargin {
+ margin: 0;
+}
+
+.has-error .has-error-btn {
+ background-color: #f2dede;
+ border-color: #a94442;
+}
+
+.has-error .has-error-btn:disabled:hover {
+ background-color: #f2dede;
+ border-color: #a94442;
+}
+
+/* If javascript is disabled. */
+.noscript {
+ padding-top: 5em;
+}
+.noscript p {
+ color: #777;
+}
+
+/* Notifications */
+
+.notification div.img-circle {
+ width: 50px;
+ height: 50px;
+ position: relative;
+}
+.notification.info div.img-circle {
+ background-color: #5bc0de;
+}
+.notification.error div.img-circle {
+ background-color: #d9534f;
+}
+.notification.success div.img-circle {
+ background-color: #5cb85c;
+}
+.notification.warning div.img-circle {
+ background-color: #f0ad4e;
+}
+
+.notification .icon {
+ background-repeat: no-repeat;
+ background-image: url('./assets/notification-icons.png') !important;
+ height: 36px;
+ width: 36px;
+ position: absolute;
+ margin: 7px;
+}
+.notification.info .icon {
+ background-position: -36px 0;
+}
+.notification.error .icon {
+ background-position: -108px 0;
+}
+.notification.success .icon {
+ background-position: 0 0;
+}
+.notification.warning .icon {
+ background-position: -72px 0;
+}
+
+.required {
+ color: #d04437;
+}
+
+/* oa-helper */
+oa-helper i {
+ color: $oa-color-blue;
+ cursor: pointer;
+}
+
+.page-footer {
+ font-size: 12px;
+ color: #777;
+ text-align: center;
+ margin-left: 150px;
+ margin-right: 150px;
+ margin-top: 50px;
+ margin-bottom: 50px;
+}
+
+hr.oa-hr-small {
+ margin-top: 5px;
+ margin-bottom: 5px;
+}
+
+.table>thead>tr>th.rbd-striping-object{
+ min-width: 60px;
+}
+.table>thead>tr>th.rbd-striping-stripe {
+ min-width: 100px;
+}
+.rbd-striping-column-separator {
+ width: 1px;
+}
+
+.table>tbody>tr>td.rbd-striping-cell-top {
+ border-top: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+}
+.table>tbody>tr>td.rbd-striping-cell-center {
+ border-top: 1px dashed #ccc;
+ border-left: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+}
+.table>tbody>tr>td.rbd-striping-cell-bottom {
+ border-bottom: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+}
+
+.dropdown-submenu {
+ position: relative;
+}
+
+.dropdown-submenu>.dropdown-menu {
+ top: 0;
+ left: 100%;
+ margin-top: -6px;
+ margin-left: -1px;
+ -webkit-border-radius: 0 6px 6px 6px;
+ -moz-border-radius: 0 6px 6px;
+ border-radius: 0 6px 6px 6px;
+}
+
+.dropdown-submenu:hover>.dropdown-menu {
+ display: block;
+}
+
+.dropdown-submenu>a:after {
+ display: block;
+ content: " ";
+ float: right;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ border-width: 5px 0 5px 5px;
+ border-left-color: $oa-color-blue;
+ margin-top: 5px;
+ margin-right: -10px;
+}
+
+.dropdown-submenu:hover>a:after {
+ border-left-color: $oa-color-blue;
+}
+
+.dropdown-submenu.pull-left {
+ float: none;
+}
+
+.dropdown-submenu.pull-left>.dropdown-menu {
+ left: -100%;
+ margin-left: 10px;
+ -webkit-border-radius: 6px 0 6px 6px;
+ -moz-border-radius: 6px 0 6px 6px;
+ border-radius: 6px 0 6px 6px;
+}
+
+/* Forms */
+.form-group>.control-label>span.required {
+ @extend .fa;
+ @extend .fa-asterisk;
+ @extend .required;
+ font-size: 6px;
+ padding-left: 4px;
+ vertical-align: text-top;
+}
--- /dev/null
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE9, IE10 and IE11 requires all of the following polyfills. **/
+import 'core-js/es6/array';
+import 'core-js/es6/date';
+import 'core-js/es6/function';
+import 'core-js/es6/map';
+import 'core-js/es6/math';
+import 'core-js/es6/number';
+import 'core-js/es6/object';
+import 'core-js/es6/parse-float';
+import 'core-js/es6/parse-int';
+import 'core-js/es6/regexp';
+import 'core-js/es6/set';
+import 'core-js/es6/string';
+import 'core-js/es6/symbol';
+import 'core-js/es6/weak-map';
+import 'core-js/es7/object';
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js'; // Run `npm install --save classlist.js`.
+
+/** IE10 and IE11 requires the following for the Reflect API. */
+// import 'core-js/es6/reflect';
+
+/** Evergreen browsers require these. **/
+// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
+import 'core-js/es7/reflect';
+
+/**
+ * Required to support Web Animations `@angular/platform-browser/animations`.
+ * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
+ **/
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+/***************************************************************************************************
+ * Zone JS is required by Angular itself.
+ */
+import 'zone.js/dist/zone'; // Included with Angular CLI.
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
+
+/**
+ * Date, currency, decimal and percent pipes.
+ * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
+ */
+// import 'intl'; // Run `npm install --save intl`.
+/**
+ * Need to import at least one locale-data with intl.
+ */
+// import 'intl/locale-data/jsonp/en';
--- /dev/null
+/* You can add global styles to this file, and also import other style files */
+@import './openattic-theme.scss';
--- /dev/null
+.chart-container {
+ position: absolute;
+ margin: auto;
+ cursor: pointer;
+ overflow: visible;
+}
+
+canvas {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.chartjs-tooltip {
+ opacity: 0;
+ position: absolute;
+ background: rgba(0, 0, 0, 0.7);
+ color: white;
+ border-radius: 3px;
+ -webkit-transition: all 0.1s ease;
+ transition: all 0.1s ease;
+ pointer-events: none;
+ font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif !important;
+
+ -webkit-transform: translate(-50%, 0);
+ transform: translate(-50%, 0);
+
+ &.transform-left {
+ transform: translate(-10%, 0);
+
+ &::after {
+ left: 10%;
+ }
+ }
+
+ &.transform-right {
+ transform: translate(-90%, 0);
+
+ &::after {
+ left: 90%;
+ }
+ }
+}
+
+.chartjs-tooltip::after {
+ content: ' ';
+ position: absolute;
+ top: 100%; /* At the bottom of the tooltip */
+ left: 50%;
+ margin-left: -5px;
+ border-width: 5px;
+ border-style: solid;
+ border-color: black transparent transparent transparent;
+}
+
+::ng-deep .chartjs-tooltip-key {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ margin-right: 10px;
+}
--- /dev/null
+/* tslint:disable:ordered-imports */
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/dist/long-stack-trace-zone';
+import 'zone.js/dist/proxy.js';
+import 'zone.js/dist/sync-test';
+import 'zone.js/dist/jasmine-patch';
+import 'zone.js/dist/async-test';
+import 'zone.js/dist/fake-async-test';
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
+declare const __karma__: any;
+declare const require: any;
+
+// Prevent Karma from running prematurely.
+__karma__.loaded = function () {};
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting()
+);
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
+// Finally, start Karma to run the tests.
+__karma__.start();
--- /dev/null
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/app",
+ "baseUrl": "./",
+ "module": "es2015",
+ "types": []
+ },
+ "exclude": [
+ "test.ts",
+ "**/*.spec.ts"
+ ]
+}
--- /dev/null
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/spec",
+ "baseUrl": "./",
+ "module": "commonjs",
+ "target": "es5",
+ "types": [
+ "jasmine",
+ "node"
+ ]
+ },
+ "files": [
+ "test.ts"
+ ],
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
--- /dev/null
+/* SystemJS module definition */
+declare var module: NodeModule;
+interface NodeModule {
+ id: string;
+}
--- /dev/null
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "outDir": "./dist/out-tsc",
+ "sourceMap": true,
+ "declaration": false,
+ "moduleResolution": "node",
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "target": "es5",
+ "typeRoots": [
+ "node_modules/@types"
+ ],
+ "lib": [
+ "es2017",
+ "dom"
+ ]
+ }
+}
--- /dev/null
+{
+ "rulesDirectory": [
+ "node_modules/codelyzer"
+ ],
+ "extends": [
+ "tslint-eslint-rules"
+ ],
+ "rules": {
+ "no-consecutive-blank-lines": true,
+ "arrow-return-shorthand": true,
+ "callable-types": true,
+ "class-name": true,
+ "comment-format": [
+ true,
+ "check-space"
+ ],
+ "curly": true,
+ "eofline": true,
+ "forin": true,
+ "import-blacklist": [
+ true,
+ "rxjs",
+ "rxjs/Rx"
+ ],
+ "import-spacing": true,
+ "indent": [
+ true,
+ "spaces"
+ ],
+ "interface-over-type-literal": true,
+ "label-position": true,
+ "max-line-length": [
+ true,
+ 100
+ ],
+ "member-access": false,
+ "member-ordering": [
+ true,
+ {
+ "order": [
+ "static-field",
+ "instance-field",
+ "static-method",
+ "instance-method"
+ ]
+ }
+ ],
+ "no-arg": true,
+ "no-bitwise": true,
+ "no-console": [
+ true,
+ "debug",
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-construct": true,
+ "no-debugger": true,
+ "no-duplicate-super": true,
+ "no-empty": false,
+ "no-empty-interface": true,
+ "no-eval": true,
+ "no-inferrable-types": [
+ true,
+ "ignore-params"
+ ],
+ "no-misused-new": true,
+ "no-non-null-assertion": true,
+ "no-shadowed-variable": true,
+ "no-string-literal": false,
+ "no-string-throw": true,
+ "no-switch-case-fall-through": true,
+ "no-trailing-whitespace": true,
+ "no-unnecessary-initializer": true,
+ "no-unused-expression": true,
+ "no-use-before-declare": true,
+ "no-var-keyword": true,
+ "object-literal-sort-keys": false,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "prefer-const": true,
+ "quotemark": [
+ true,
+ "single"
+ ],
+ "radix": true,
+ "semicolon": [
+ true,
+ "always"
+ ],
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "typedef-whitespace": [
+ true,
+ {
+ "call-signature": "nospace",
+ "index-signature": "nospace",
+ "parameter": "nospace",
+ "property-declaration": "nospace",
+ "variable-declaration": "nospace"
+ }
+ ],
+ "unified-signatures": true,
+ "variable-name": [
+ true,
+ "check-format",
+ "allow-snake-case"
+ ],
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator",
+ "check-type",
+ "check-module"
+ ],
+ "directive-selector": [
+ true,
+ "attribute",
+ "cd",
+ "camelCase"
+ ],
+ "component-selector": [
+ true,
+ "element",
+ "cd",
+ "kebab-case"
+ ],
+ "angular-whitespace": [true, "check-interpolation", "check-semicolon"],
+ "no-output-on-prefix": true,
+ "use-input-property-decorator": true,
+ "use-output-property-decorator": true,
+ "use-host-property-decorator": true,
+ "no-attribute-parameter-decorator": true,
+ "no-input-rename": true,
+ "no-output-rename": true,
+ "use-life-cycle-interface": true,
+ "use-pipe-transform-interface": true,
+ "component-class-suffix": true,
+ "directive-class-suffix": true,
+ "no-forward-ref": true,
+ "no-output-named-after-standard-event": true,
+ "ordered-imports": true,
+ "no-extra-semi": true,
+ "ter-no-irregular-whitespace": true,
+ "no-multi-spaces": true,
+ "brace-style": [
+ true,
+ "1tbs",
+ {
+ "allowSingleLine": false
+ }
+ ],
+ "ter-indent": [
+ true,
+ 2,
+ {
+ "SwitchCase": 1,
+ "FunctionDeclaration": {
+ "body": 1,
+ "parameters": "first"
+ },
+ "FunctionExpression": {
+ "body": 1,
+ "parameters": "first"
+ }
+ }
+ ],
+ "space-in-parens": [true, "never"]
+ }
+}
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+openATTIC mgr plugin (based on CherryPy)
+"""
+from __future__ import absolute_import
+
+import errno
+import os
+import socket
+try:
+ from urlparse import urljoin
+except ImportError:
+ from urllib.parse import urljoin
+try:
+ import cherrypy
+except ImportError:
+ # To be picked up and reported by .can_run()
+ cherrypy = None
+
+from mgr_module import MgrModule, MgrStandbyModule
+
+if 'COVERAGE_ENABLED' in os.environ:
+ import coverage
+ _cov = coverage.Coverage(config_file="{}/.coveragerc".format(os.path.dirname(__file__)))
+ _cov.start()
+
+# pylint: disable=wrong-import-position
+from . import logger, mgr
+from .controllers.auth import Auth
+from .tools import load_controllers, json_error_page, SessionExpireAtBrowserCloseTool, \
+ NotificationQueue, RequestLoggingTool
+from .settings import options_command_list, handle_option_command
+
+
+# cherrypy likes to sys.exit on error. don't let it take us down too!
+# pylint: disable=W0613
+def os_exit_noop(*args):
+ pass
+
+
+# pylint: disable=W0212
+os._exit = os_exit_noop
+
+
+def prepare_url_prefix(url_prefix):
+ """
+ return '' if no prefix, or '/prefix' without slash in the end.
+ """
+ url_prefix = urljoin('/', url_prefix)
+ return url_prefix.rstrip('/')
+
+
+class Module(MgrModule):
+ """
+ dashboard module entrypoint
+ """
+
+ COMMANDS = [
+ {
+ 'cmd': 'dashboard set-login-credentials '
+ 'name=username,type=CephString '
+ 'name=password,type=CephString',
+ 'desc': 'Set the login credentials',
+ 'perm': 'w'
+ },
+ {
+ 'cmd': 'dashboard set-session-expire '
+ 'name=seconds,type=CephInt',
+ 'desc': 'Set the session expire timeout',
+ 'perm': 'w'
+ }
+ ]
+ COMMANDS.extend(options_command_list())
+
+ @property
+ def url_prefix(self):
+ return self._url_prefix
+
+ def __init__(self, *args, **kwargs):
+ super(Module, self).__init__(*args, **kwargs)
+ mgr.init(self)
+ self._url_prefix = ''
+
+ @classmethod
+ def can_run(cls):
+ if cherrypy is None:
+ return False, "Missing dependency: cherrypy"
+
+ if not os.path.exists(cls.get_frontend_path()):
+ return False, "Frontend assets not found: incomplete build?"
+
+ return True, ""
+
+ @classmethod
+ def get_frontend_path(cls):
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ return os.path.join(current_dir, 'frontend/dist')
+
+ def configure_cherrypy(self):
+ server_addr = self.get_localized_config('server_addr', '::')
+ server_port = self.get_localized_config('server_port', '8080')
+ if server_addr is None:
+ raise RuntimeError(
+ 'no server_addr configured; '
+ 'try "ceph config-key put mgr/{}/{}/server_addr <ip>"'
+ .format(self.module_name, self.get_mgr_id()))
+ self.log.info('server_addr: %s server_port: %s', server_addr,
+ server_port)
+
+ self._url_prefix = prepare_url_prefix(self.get_config('url_prefix',
+ default=''))
+
+ # Initialize custom handlers.
+ cherrypy.tools.authenticate = cherrypy.Tool('before_handler', Auth.check_auth)
+ cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
+ cherrypy.tools.request_logging = RequestLoggingTool()
+
+ # Apply the 'global' CherryPy configuration.
+ config = {
+ 'engine.autoreload.on': False,
+ 'server.socket_host': server_addr,
+ 'server.socket_port': int(server_port),
+ 'error_page.default': json_error_page,
+ 'tools.request_logging.on': True
+ }
+ cherrypy.config.update(config)
+
+ config = {
+ '/': {
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': self.get_frontend_path(),
+ 'tools.staticdir.index': 'index.html'
+ }
+ }
+
+ # Publish the URI that others may use to access the service we're
+ # about to start serving
+ self.set_uri("http://{0}:{1}{2}/".format(
+ socket.getfqdn() if server_addr == "::" else server_addr,
+ server_port,
+ self.url_prefix
+ ))
+
+ cherrypy.tree.mount(Module.ApiRoot(self), '{}/api'.format(self.url_prefix))
+ cherrypy.tree.mount(Module.StaticRoot(), '{}/'.format(self.url_prefix), config=config)
+
+ def serve(self):
+ if 'COVERAGE_ENABLED' in os.environ:
+ _cov.start()
+ self.configure_cherrypy()
+
+ cherrypy.engine.start()
+ NotificationQueue.start_queue()
+ logger.info('Waiting for engine...')
+ cherrypy.engine.block()
+ if 'COVERAGE_ENABLED' in os.environ:
+ _cov.stop()
+ _cov.save()
+ logger.info('Engine done')
+
+ def shutdown(self):
+ super(Module, self).shutdown()
+ logger.info('Stopping server...')
+ NotificationQueue.stop()
+ cherrypy.engine.exit()
+ logger.info('Stopped server')
+
+ def handle_command(self, cmd):
+ res = handle_option_command(cmd)
+ if res[0] != -errno.ENOSYS:
+ return res
+ if cmd['prefix'] == 'dashboard set-login-credentials':
+ Auth.set_login_credentials(cmd['username'], cmd['password'])
+ return 0, 'Username and password updated', ''
+ elif cmd['prefix'] == 'dashboard set-session-expire':
+ self.set_config('session-expire', str(cmd['seconds']))
+ return 0, 'Session expiration timeout updated', ''
+
+ return (-errno.EINVAL, '', 'Command not found \'{0}\''
+ .format(cmd['prefix']))
+
+ def notify(self, notify_type, notify_id):
+ NotificationQueue.new_notification(notify_type, notify_id)
+
+ class ApiRoot(object):
+
+ _cp_config = {
+ 'tools.sessions.on': True,
+ 'tools.authenticate.on': True
+ }
+
+ def __init__(self, mgrmod):
+ self.ctrls = load_controllers()
+ logger.debug('Loaded controllers: %s', self.ctrls)
+
+ first_level_ctrls = [ctrl for ctrl in self.ctrls
+ if '/' not in ctrl._cp_path_]
+ multi_level_ctrls = set(self.ctrls).difference(first_level_ctrls)
+
+ for ctrl in first_level_ctrls:
+ logger.info('Adding controller: %s -> /api/%s', ctrl.__name__,
+ ctrl._cp_path_)
+ inst = ctrl()
+ setattr(Module.ApiRoot, ctrl._cp_path_, inst)
+
+ for ctrl in multi_level_ctrls:
+ path_parts = ctrl._cp_path_.split('/')
+ path = '/'.join(path_parts[:-1])
+ key = path_parts[-1]
+ parent_ctrl_classes = [c for c in self.ctrls
+ if c._cp_path_ == path]
+ if len(parent_ctrl_classes) != 1:
+ logger.error('No parent controller found for %s! '
+ 'Please check your path in the ApiController '
+ 'decorator!', ctrl)
+ else:
+ inst = ctrl()
+ setattr(parent_ctrl_classes[0], key, inst)
+
+ @cherrypy.expose
+ def index(self):
+ tpl = """API Endpoints:<br>
+ <ul>
+ {lis}
+ </ul>
+ """
+ endpoints = ['<li><a href="{}">{}</a></li>'.format(ctrl._cp_path_, ctrl.__name__) for
+ ctrl in self.ctrls]
+ return tpl.format(lis='\n'.join(endpoints))
+
+ class StaticRoot(object):
+ pass
+
+
+class StandbyModule(MgrStandbyModule):
+ def serve(self):
+ server_addr = self.get_localized_config('server_addr', '::')
+ server_port = self.get_localized_config('server_port', '7000')
+ if server_addr is None:
+ msg = 'no server_addr configured; try "ceph config-key set ' \
+ 'mgr/dashboard/server_addr <ip>"'
+ raise RuntimeError(msg)
+ self.log.info("server_addr: %s server_port: %s",
+ server_addr, server_port)
+ cherrypy.config.update({
+ 'server.socket_host': server_addr,
+ 'server.socket_port': int(server_port),
+ 'engine.autoreload.on': False
+ })
+
+ module = self
+
+ class Root(object):
+ @cherrypy.expose
+ def index(self):
+ active_uri = module.get_active_uri()
+ if active_uri:
+ module.log.info("Redirecting to active '%s'", active_uri)
+ raise cherrypy.HTTPRedirect(active_uri)
+ else:
+ template = """
+ <html>
+ <!-- Note: this is only displayed when the standby
+ does not know an active URI to redirect to, otherwise
+ a simple redirect is returned instead -->
+ <head>
+ <title>Ceph</title>
+ <meta http-equiv="refresh" content="{delay}">
+ </head>
+ <body>
+ No active ceph-mgr instance is currently running
+ the dashboard. A failover may be in progress.
+ Retrying in {delay} seconds...
+ </body>
+ </html>
+ """
+ return template.format(delay=5)
+
+ url_prefix = prepare_url_prefix(self.get_config('url_prefix',
+ default=''))
+ cherrypy.tree.mount(Root(), "{}/".format(url_prefix), {})
+ self.log.info("Starting engine...")
+ cherrypy.engine.start()
+ self.log.info("Waiting for engine...")
+ cherrypy.engine.wait(state=cherrypy.engine.states.STOPPED)
+ self.log.info("Engine done.")
+
+ def shutdown(self):
+ self.log.info("Stopping server...")
+ cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
+ cherrypy.engine.stop()
+ self.log.info("Stopped server")
--- /dev/null
+astroid==1.6.1
+attrs==17.4.0
+backports.functools-lru-cache==1.4
+cheroot==6.0.0
+CherryPy==13.1.0
+configparser==3.5.0
+coverage==4.4.2
+enum34==1.1.6
+funcsigs==1.0.2
+isort==4.2.15
+lazy-object-proxy==1.3.1
+mccabe==0.6.1
+mock==2.0.0
+more-itertools==4.1.0
+pbr==3.1.1
+pluggy==0.6.0
+portend==2.2
+py==1.5.2
+pycodestyle==2.3.1
+pycparser==2.18
+pylint==1.8.2
+pytest==3.3.2
+pytest-cov==2.5.1
+python-bcrypt==0.3.2
+pytz==2017.3
+requests==2.18.4
+singledispatch==3.4.0.3
+six==1.11.0
+tempora==1.10
+tox==2.9.1
+virtualenv==15.1.0
+wrapt==1.10.11
--- /dev/null
+#!/usr/bin/env bash
+
+# run from ./
+
+# creating temp directory to store virtualenv and teuthology
+TEMP_DIR=`mktemp -d`
+
+get_cmake_variable() {
+ local variable=$1
+ grep "$variable" CMakeCache.txt | cut -d "=" -f 2
+}
+
+read -r -d '' TEUTHOLOFY_PY_REQS <<EOF
+apache-libcloud==2.2.1 \
+asn1crypto==0.22.0 \
+bcrypt==3.1.4 \
+certifi==2018.1.18 \
+cffi==1.10.0 \
+chardet==3.0.4 \
+configobj==5.0.6 \
+cryptography==2.1.4 \
+enum34==1.1.6 \
+gevent==1.2.2 \
+greenlet==0.4.13 \
+idna==2.5 \
+ipaddress==1.0.18 \
+Jinja2==2.9.6 \
+manhole==1.5.0 \
+MarkupSafe==1.0 \
+netaddr==0.7.19 \
+packaging==16.8 \
+paramiko==2.4.0 \
+pexpect==4.4.0 \
+psutil==5.4.3 \
+ptyprocess==0.5.2 \
+pyasn1==0.2.3 \
+pycparser==2.17 \
+PyNaCl==1.2.1 \
+pyparsing==2.2.0 \
+python-dateutil==2.6.1 \
+PyYAML==3.12 \
+requests==2.18.4 \
+six==1.10.0 \
+urllib3==1.22
+EOF
+
+
+CURR_DIR=`pwd`
+
+cd $TEMP_DIR
+
+virtualenv --python=/usr/bin/python venv
+source venv/bin/activate
+eval pip install $TEUTHOLOFY_PY_REQS
+pip install -r $CURR_DIR/requirements.txt
+deactivate
+
+git clone https://github.com/ceph/teuthology.git
+
+cd $CURR_DIR
+cd ../../../../build
+
+CEPH_MGR_PY_VERSION_MAJOR=$(get_cmake_variable MGR_PYTHON_VERSION | cut -d '.' -f1)
+if [ -n "$CEPH_MGR_PY_VERSION_MAJOR" ]; then
+ CEPH_PY_VERSION_MAJOR=${CEPH_MGR_PY_VERSION_MAJOR}
+else
+ if [ $(get_cmake_variable WITH_PYTHON2) = ON ]; then
+ CEPH_PY_VERSION_MAJOR=2
+ else
+ CEPH_PY_VERSION_MAJOR=3
+ fi
+fi
+
+export COVERAGE_ENABLED=true
+export COVERAGE_FILE=.coverage.mgr.dashboard
+
+MGR=2 RGW=1 ../src/vstart.sh -n -d
+sleep 10
+
+source $TEMP_DIR/venv/bin/activate
+BUILD_DIR=`pwd`
+
+if [ "$#" -gt 0 ]; then
+ TEST_CASES=""
+ for t in "$@"; do
+ TEST_CASES="$TESTS_CASES $t"
+ done
+else
+ TEST_CASES=`for i in \`ls $BUILD_DIR/../qa/tasks/mgr/dashboard/test_*\`; do F=$(basename $i); M="${F%.*}"; echo -n " tasks.mgr.dashboard.$M"; done`
+ TEST_CASES="tasks.mgr.test_dashboard $TEST_CASES"
+fi
+
+export PATH=$BUILD_DIR/bin:$PATH
+export LD_LIBRARY_PATH=$BUILD_DIR/lib/cython_modules/lib.${CEPH_PY_VERSION_MAJOR}/:$BUILD_DIR/lib
+export PYTHONPATH=$TEMP_DIR/teuthology:$BUILD_DIR/../qa:$BUILD_DIR/lib/cython_modules/lib.${CEPH_PY_VERSION_MAJOR}/
+eval python ../qa/tasks/vstart_runner.py $TEST_CASES
+
+deactivate
+killall ceph-mgr
+sleep 10
+../src/stop.sh
+sleep 5
+
+cd $CURR_DIR
+rm -rf $TEMP_DIR
+
--- /dev/null
+#!/usr/bin/env bash
+
+set -e
+
+cd $CEPH_ROOT/src/pybind/mgr/dashboard/frontend
+
+npm run build -- --prod
+npm run test -- --browsers PhantomJS --watch=false
+npm run lint
--- /dev/null
+#!/usr/bin/env bash
+
+# run from ./ or from ../
+: ${MGR_DASHBOARD_VIRTUALENV:=/tmp/mgr-dashboard-virtualenv}
+: ${WITH_PYTHON3:=ON}
+test -d dashboard && cd dashboard
+
+if [ -e tox.ini ]; then
+ TOX_PATH=`readlink -f tox.ini`
+else
+ TOX_PATH=`readlink -f $(dirname $0)/tox.ini`
+fi
+
+if [ -z $CEPH_BUILD_DIR ]; then
+ export CEPH_BUILD_DIR=$(dirname ${TOX_PATH})
+fi
+
+source ${MGR_DASHBOARD_VIRTUALENV}/bin/activate
+
+if [ "$WITH_PYTHON3" = "ON" ]; then
+ ENV_LIST="cov-init,py27,py3,cov-report,lint"
+else
+ ENV_LIST="cov-init,py27,cov-report,lint"
+fi
+
+tox -c ${TOX_PATH} -e $ENV_LIST
+
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import time
+import collections
+from collections import defaultdict
+
+from .. import mgr
+
+
+class CephService(object):
+ @classmethod
+ def get_service_map(cls, service_name):
+ service_map = {}
+ for server in mgr.list_servers():
+ for service in server['services']:
+ if service['type'] == service_name:
+ if server['hostname'] not in service_map:
+ service_map[server['hostname']] = {
+ 'server': server,
+ 'services': []
+ }
+ inst_id = service['id']
+ metadata = mgr.get_metadata(service_name, inst_id)
+ status = mgr.get_daemon_status(service_name, inst_id)
+ service_map[server['hostname']]['services'].append({
+ 'id': inst_id,
+ 'type': service_name,
+ 'hostname': server['hostname'],
+ 'metadata': metadata,
+ 'status': status
+ })
+ return service_map
+
+ @classmethod
+ def get_service_list(cls, service_name):
+ service_map = cls.get_service_map(service_name)
+ return [svc for _, svcs in service_map.items() for svc in svcs['services']]
+
+ @classmethod
+ def get_service(cls, service_name, service_id):
+ for server in mgr.list_servers():
+ for service in server['services']:
+ if service['type'] == service_name:
+ inst_id = service['id']
+ if inst_id == service_id:
+ metadata = mgr.get_metadata(service_name, inst_id)
+ status = mgr.get_daemon_status(service_name, inst_id)
+ return {
+ 'id': inst_id,
+ 'type': service_name,
+ 'hostname': server['hostname'],
+ 'metadata': metadata,
+ 'status': status
+ }
+ return None
+
+ @classmethod
+ def get_pool_list(cls, application=None):
+ osd_map = mgr.get('osd_map')
+ if not application:
+ return osd_map['pools']
+ return [pool for pool in osd_map['pools']
+ if application in pool.get('application_metadata', {})]
+
+ @classmethod
+ def get_pool_list_with_stats(cls, application=None):
+ # pylint: disable=too-many-locals
+ pools = cls.get_pool_list(application)
+
+ 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))
+
+ for pool in pools:
+ pool['pg_status'] = pg_summary['by_pool'][pool['pool'].__str__()]
+ stats = pool_stats[pool['pool']]
+ s = {}
+
+ def get_rate(series):
+ if len(series) >= 2:
+ return (float(series[0][1]) - float(series[1][1])) / \
+ (float(series[0][0]) - float(series[1][0]))
+ return 0
+
+ for stat_name, stat_series in stats.items():
+ s[stat_name] = {
+ 'latest': stat_series[0][1],
+ 'rate': get_rate(stat_series),
+ 'series': [i for i in stat_series]
+ }
+ pool['stats'] = s
+ pools_w_stats.append(pool)
+ return pools_w_stats
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import errno
+import inspect
+from six import add_metaclass
+
+from . import mgr
+
+
+class Options(object):
+ """
+ If you need to store some configuration value please add the config option
+ name as a class attribute to this class.
+
+ Example::
+
+ GRAFANA_API_HOST = ('localhost', str)
+ GRAFANA_API_PORT = (3000, int)
+ """
+ pass
+
+
+class SettingsMeta(type):
+ def __getattr__(cls, attr):
+ default, stype = getattr(Options, attr)
+ return stype(mgr.get_config(attr, default))
+
+ def __setattr__(cls, attr, value):
+ if not attr.startswith('_') and hasattr(Options, attr):
+ mgr.set_config(attr, str(value))
+ else:
+ setattr(SettingsMeta, attr, value)
+
+
+# pylint: disable=no-init
+@add_metaclass(SettingsMeta)
+class Settings(object):
+ pass
+
+
+def _options_command_map():
+ def filter_attr(member):
+ return not inspect.isroutine(member)
+
+ cmd_map = {}
+ for option, value in inspect.getmembers(Options, filter_attr):
+ if option.startswith('_'):
+ continue
+ key_get = 'dashboard get-{}'.format(option.lower().replace('_', '-'))
+ key_set = 'dashboard set-{}'.format(option.lower().replace('_', '-'))
+ cmd_map[key_get] = {'name': option, 'type': None}
+ cmd_map[key_set] = {'name': option, 'type': value[1]}
+ return cmd_map
+
+
+_OPTIONS_COMMAND_MAP = _options_command_map()
+
+
+def options_command_list():
+ """
+ This function generates a list of ``get`` and ``set`` commands
+ for each declared configuration option in class ``Options``.
+ """
+ def py2ceph(pytype):
+ if pytype == str:
+ return 'CephString'
+ elif pytype == int:
+ return 'CephInt'
+ return 'CephString'
+
+ cmd_list = []
+ for cmd, opt in _OPTIONS_COMMAND_MAP.items():
+ if not opt['type']:
+ cmd_list.append({
+ 'cmd': '{}'.format(cmd),
+ 'desc': 'Get the {} option value'.format(opt['name']),
+ 'perm': 'r'
+ })
+ else:
+ cmd_list.append({
+ 'cmd': '{} name=value,type={}'
+ .format(cmd, py2ceph(opt['type'])),
+ 'desc': 'Set the {} option value'.format(opt['name']),
+ 'perm': 'w'
+ })
+
+ return cmd_list
+
+
+def handle_option_command(cmd):
+ if cmd['prefix'] not in _OPTIONS_COMMAND_MAP:
+ return (-errno.ENOSYS, '', "Command not found '{}'".format(cmd['prefix']))
+
+ opt = _OPTIONS_COMMAND_MAP[cmd['prefix']]
+ if not opt['type']:
+ # get option
+ return 0, str(getattr(Settings, opt['name'])), ''
+
+ # set option
+ setattr(Settings, opt['name'], opt['type'](cmd['value']))
+ return 0, 'Option {} updated'.format(opt['name']), ''
--- /dev/null
+# -*- coding: utf-8 -*-
+# pylint: disable=W0212
+from __future__ import absolute_import
+
+import json
+
+import cherrypy
+from cherrypy.test import helper
+
+from ..controllers.auth import Auth
+from ..tools import json_error_page, SessionExpireAtBrowserCloseTool
+
+
+class ControllerTestCase(helper.CPWebCase):
+ def __init__(self, *args, **kwargs):
+ cherrypy.tools.authenticate = cherrypy.Tool('before_handler', Auth.check_auth)
+ cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
+ cherrypy.config.update({'error_page.default': json_error_page})
+ super(ControllerTestCase, self).__init__(*args, **kwargs)
+
+ def _request(self, url, method, data=None):
+ if not data:
+ b = None
+ h = None
+ else:
+ b = json.dumps(data)
+ h = [('Content-Type', 'application/json'),
+ ('Content-Length', str(len(b)))]
+ self.getPage(url, method=method, body=b, headers=h)
+
+ def _get(self, url):
+ self._request(url, 'GET')
+
+ def _post(self, url, data=None):
+ self._request(url, 'POST', data)
+
+ def _delete(self, url, data=None):
+ self._request(url, 'DELETE', data)
+
+ def _put(self, url, data=None):
+ self._request(url, 'PUT', data)
+
+ def jsonBody(self):
+ body_str = self.body.decode('utf-8') if isinstance(self.body, bytes) else self.body
+ return json.loads(body_str)
+
+ def assertJsonBody(self, data, msg=None):
+ """Fail if value != self.body."""
+ json_body = self.jsonBody()
+ if data != json_body:
+ if msg is None:
+ msg = 'expected body:\n%r\n\nactual body:\n%r' % (
+ data, json_body)
+ self._handlewebError(msg)
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import random
+import time
+import unittest
+
+
+from ..tools import NotificationQueue
+
+
+class Listener(object):
+ def __init__(self):
+ NotificationQueue.register(self.log_type1, 'type1')
+ NotificationQueue.register(self.log_type2, 'type2')
+ NotificationQueue.register(self.log_type1_3, ['type1', 'type3'])
+ NotificationQueue.register(self.log_all)
+ self.type1 = []
+ self.type2 = []
+ self.type1_3 = []
+ self.all = []
+
+ # these should be ignored by the queue
+ NotificationQueue.register(self.log_type1, 'type1')
+ NotificationQueue.register(self.log_type1_3, ['type1', 'type3'])
+ NotificationQueue.register(self.log_all)
+
+ def log_type1(self, val):
+ self.type1.append(val)
+
+ def log_type2(self, val):
+ self.type2.append(val)
+
+ def log_type1_3(self, val):
+ self.type1_3.append(val)
+
+ def log_all(self, val):
+ self.all.append(val)
+
+ def clear(self):
+ self.type1 = []
+ self.type2 = []
+ self.type1_3 = []
+ self.all = []
+
+
+class NotificationQueueTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.listener = Listener()
+
+ def setUp(self):
+ self.listener.clear()
+
+ def test_invalid_register(self):
+ with self.assertRaises(Exception) as ctx:
+ NotificationQueue.register(None, 1)
+ self.assertEqual(str(ctx.exception),
+ "types param is neither a string nor a list")
+
+ def test_notifications(self):
+ NotificationQueue.start_queue()
+ NotificationQueue.new_notification('type1', 1)
+ NotificationQueue.new_notification('type2', 2)
+ NotificationQueue.new_notification('type3', 3)
+ NotificationQueue.stop()
+ self.assertEqual(self.listener.type1, [1])
+ self.assertEqual(self.listener.type2, [2])
+ self.assertEqual(self.listener.type1_3, [1, 3])
+ self.assertEqual(self.listener.all, [1, 2, 3])
+
+ def test_notifications2(self):
+ NotificationQueue.start_queue()
+ for i in range(0, 600):
+ typ = "type{}".format(i % 3 + 1)
+ if random.random() < 0.5:
+ time.sleep(0.002)
+ NotificationQueue.new_notification(typ, i)
+ NotificationQueue.stop()
+ for i in range(0, 500):
+ typ = i % 3 + 1
+ if typ == 1:
+ self.assertIn(i, self.listener.type1)
+ self.assertIn(i, self.listener.type1_3)
+ elif typ == 2:
+ self.assertIn(i, self.listener.type2)
+ elif typ == 3:
+ self.assertIn(i, self.listener.type1_3)
+ self.assertIn(i, self.listener.all)
+
+ self.assertEqual(len(self.listener.type1), 200)
+ self.assertEqual(len(self.listener.type2), 200)
+ self.assertEqual(len(self.listener.type1_3), 400)
+ self.assertEqual(len(self.listener.all), 600)
--- /dev/null
+from __future__ import absolute_import
+
+import json
+import mock
+
+import cherrypy
+
+from .. import mgr
+from ..controllers.summary import Summary
+from ..controllers.rbd_mirroring import RbdMirror
+from .helper import ControllerTestCase
+
+
+mock_list_servers = [{
+ 'hostname': 'ceph-host',
+ 'services': [{'id': 3, 'type': 'rbd-mirror'}]
+}]
+
+mock_get_metadata = {
+ 'id': 1,
+ 'instance_id': 3,
+ 'ceph_version': 'ceph version 13.0.0-5719 mimic (dev)'
+}
+
+_status = {
+ 1: {
+ 'callouts': {},
+ 'image_local_count': 5,
+ 'image_remote_count': 6,
+ 'image_error_count': 7,
+ 'image_warning_count': 8,
+ 'name': 'pool_name'
+ }
+}
+
+mock_get_daemon_status = {
+ 'json': json.dumps(_status)
+}
+
+mock_osd_map = {
+ 'pools': [{
+ 'pool_name': 'rbd',
+ 'application_metadata': {'rbd'}
+ }]
+}
+
+
+class RbdMirroringControllerTest(ControllerTestCase):
+
+ @classmethod
+ def setup_server(cls):
+ mgr.list_servers.return_value = mock_list_servers
+ mgr.get_metadata.return_value = mock_get_metadata
+ mgr.get_daemon_status.return_value = mock_get_daemon_status
+ mgr.get.side_effect = lambda key: {
+ 'osd_map': mock_osd_map,
+ 'health': {'json': '{"status": 1}'},
+ 'fs_map': {'filesystems': []},
+
+ }[key]
+ mgr.url_prefix = ''
+ mgr.get_mgr_id.return_value = 0
+ mgr.have_mon_connection.return_value = True
+
+ RbdMirror._cp_config['tools.authenticate.on'] = False # pylint: disable=protected-access
+
+ Summary._cp_config['tools.authenticate.on'] = False # pylint: disable=protected-access
+
+ cherrypy.tree.mount(RbdMirror(), '/api/test/rbdmirror')
+ cherrypy.tree.mount(Summary(), '/api/test/summary')
+
+ @mock.patch('dashboard.controllers.rbd_mirroring.rbd')
+ def test_default(self, rbd_mock): # pylint: disable=W0613
+ self._get('/api/test/rbdmirror')
+ result = self.jsonBody()
+ self.assertStatus(200)
+ self.assertEqual(result['status'], 0)
+ for k in ['daemons', 'pools', 'image_error', 'image_syncing', 'image_ready']:
+ self.assertIn(k, result['content_data'])
+
+ @mock.patch('dashboard.controllers.rbd_mirroring.rbd')
+ def test_summary(self, rbd_mock): # pylint: disable=W0613
+ """We're also testing `summary`, as it also uses code from `rbd_mirroring.py`"""
+ self._get('/api/test/summary')
+ self.assertStatus(200)
+
+ summary = self.jsonBody()['rbd_mirroring']
+ self.assertEqual(summary, {'errors': 0, 'warnings': 1})
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import errno
+import unittest
+
+from .. import mgr
+from .. import settings
+from ..settings import Settings, handle_option_command
+
+
+class SettingsTest(unittest.TestCase):
+ CONFIG_KEY_DICT = {}
+
+ @classmethod
+ def setUpClass(cls):
+ # pylint: disable=protected-access
+ settings.Options.GRAFANA_API_HOST = ('localhost', str)
+ settings.Options.GRAFANA_API_PORT = (3000, int)
+ settings._OPTIONS_COMMAND_MAP = settings._options_command_map()
+
+ @classmethod
+ def mock_set_config(cls, attr, val):
+ cls.CONFIG_KEY_DICT[attr] = val
+
+ @classmethod
+ def mock_get_config(cls, attr, default):
+ return cls.CONFIG_KEY_DICT.get(attr, default)
+
+ def setUp(self):
+ self.CONFIG_KEY_DICT.clear()
+ mgr.set_config.side_effect = self.mock_set_config
+ mgr.get_config.side_effect = self.mock_get_config
+ if Settings.GRAFANA_API_HOST != 'localhost':
+ Settings.GRAFANA_API_HOST = 'localhost'
+ if Settings.GRAFANA_API_PORT != 3000:
+ Settings.GRAFANA_API_PORT = 3000
+
+ def test_get_setting(self):
+ self.assertEqual(Settings.GRAFANA_API_HOST, 'localhost')
+
+ def test_set_setting(self):
+ Settings.GRAFANA_API_HOST = 'grafanahost'
+ self.assertEqual(Settings.GRAFANA_API_HOST, 'grafanahost')
+
+ def test_get_cmd(self):
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard get-grafana-api-port'})
+ self.assertEqual(r, 0)
+ self.assertEqual(out, '3000')
+ self.assertEqual(err, '')
+
+ def test_set_cmd(self):
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard set-grafana-api-port',
+ 'value': '4000'})
+ self.assertEqual(r, 0)
+ self.assertEqual(out, 'Option GRAFANA_API_PORT updated')
+ self.assertEqual(err, '')
+
+ def test_inv_cmd(self):
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard get-non-existent-option'})
+ self.assertEqual(r, -errno.ENOSYS)
+ self.assertEqual(out, '')
+ self.assertEqual(err, "Command not found "
+ "'dashboard get-non-existent-option'")
+
+ def test_sync(self):
+ Settings.GRAFANA_API_PORT = 5000
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard get-grafana-api-port'})
+ self.assertEqual(r, 0)
+ self.assertEqual(out, '5000')
+ self.assertEqual(err, '')
+ r, out, err = handle_option_command(
+ {'prefix': 'dashboard set-grafana-api-host',
+ 'value': 'new-local-host'})
+ self.assertEqual(r, 0)
+ self.assertEqual(out, 'Option GRAFANA_API_HOST updated')
+ self.assertEqual(err, '')
+ self.assertEqual(Settings.GRAFANA_API_HOST, 'new-local-host')
+
+ def test_attribute_error(self):
+ with self.assertRaises(AttributeError) as ctx:
+ _ = Settings.NON_EXISTENT_OPTION
+
+ self.assertEqual(str(ctx.exception),
+ "type object 'Options' has no attribute 'NON_EXISTENT_OPTION'")
--- /dev/null
+from __future__ import absolute_import
+
+import cherrypy
+
+from .. import mgr
+from ..controllers.tcmu_iscsi import TcmuIscsi
+from .helper import ControllerTestCase
+
+mocked_servers = [{
+ 'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)',
+ 'hostname': 'ceph-dev',
+ 'services': [{'id': 'a:b', 'type': 'tcmu-runner'}]
+}]
+
+mocked_metadata = {
+ 'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)',
+ 'pool_name': 'pool1',
+ 'image_name': 'image1',
+ 'image_id': '42',
+ 'optimized_since': 100.0,
+}
+
+mocked_get_daemon_status = {
+ 'lock_owner': 'true',
+}
+
+mocked_get_counter = {
+ 'librbd-42-pool1-image1.lock_acquired_time': [[10000.0, 10000.0]],
+ 'librbd-42-pool1-image1.rd': 43,
+ 'librbd-42-pool1-image1.wr': 44,
+ 'librbd-42-pool1-image1.rd_bytes': 45,
+ 'librbd-42-pool1-image1.wr_bytes': 46,
+}
+
+mocked_get_rate = 47
+
+
+class TcmuIscsiControllerTest(ControllerTestCase):
+
+ @classmethod
+ def setup_server(cls):
+ mgr.list_servers.return_value = mocked_servers
+ mgr.get_metadata.return_value = mocked_metadata
+ mgr.get_daemon_status.return_value = mocked_get_daemon_status
+ mgr.get_counter.return_value = mocked_get_counter
+ mgr.get_rate.return_value = mocked_get_rate
+ mgr.url_prefix = ''
+ TcmuIscsi._cp_config['tools.authenticate.on'] = False # pylint: disable=protected-access
+
+ cherrypy.tree.mount(TcmuIscsi(), "/api/test/tcmu")
+
+ def test_list(self):
+ self._get('/api/test/tcmu')
+ self.assertStatus(200)
+ self.assertJsonBody({
+ 'daemons': [{
+ 'server_hostname': 'ceph-dev',
+ 'version': 'ceph version 13.0.0-5083- () mimic (dev)',
+ 'optimized_paths': 1, 'non_optimized_paths': 0}],
+ 'images': [{
+ 'device_id': 'b',
+ 'pool_name': 'pool1',
+ 'name': 'image1',
+ 'id': '42', 'optimized_paths': ['ceph-dev'],
+ 'non_optimized_paths': [],
+ 'optimized_since': 1e-05,
+ 'stats': {'rd': 47, 'rd_bytes': 47, 'wr': 47, 'wr_bytes': 47},
+ 'stats_history': {
+ 'rd': 43, 'wr': 44, 'rd_bytes': 45, 'wr_bytes': 46}
+ }]
+ })
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import cherrypy
+from cherrypy.lib.sessions import RamSession
+from mock import patch
+
+from .helper import ControllerTestCase
+from ..tools import RESTController
+
+
+# pylint: disable=W0613
+class FooResource(RESTController):
+ elems = []
+
+ def list(self, *vpath, **params):
+ return FooResource.elems
+
+ def create(self, data, *args, **kwargs):
+ FooResource.elems.append(data)
+ return data
+
+ def get(self, key, *args, **kwargs):
+ if args:
+ return {'detail': (key, args)}
+ return FooResource.elems[int(key)]
+
+ def delete(self, key):
+ del FooResource.elems[int(key)]
+
+ def bulk_delete(self):
+ FooResource.elems = []
+
+ def set(self, data, key):
+ FooResource.elems[int(key)] = data
+ return dict(key=key, **data)
+
+
+class FooArgs(RESTController):
+ @RESTController.args_from_json
+ def set(self, code, name):
+ return {'code': code, 'name': name}
+
+
+# pylint: disable=C0102
+class Root(object):
+ foo = FooResource()
+ fooargs = FooArgs()
+
+
+class RESTControllerTest(ControllerTestCase):
+
+ @classmethod
+ def setup_server(cls):
+ cherrypy.tree.mount(Root())
+
+ def test_empty(self):
+ self._delete("/foo")
+ self.assertStatus(204)
+ self._get("/foo")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'application/json')
+ self.assertBody('[]')
+
+ def test_fill(self):
+ sess_mock = RamSession()
+ with patch('cherrypy.session', sess_mock, create=True):
+ data = {'a': 'b'}
+ for _ in range(5):
+ self._post("/foo", data)
+ self.assertJsonBody(data)
+ self.assertStatus(201)
+ self.assertHeader('Content-Type', 'application/json')
+
+ self._get("/foo")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'application/json')
+ self.assertJsonBody([data] * 5)
+
+ self._put('/foo/0', {'newdata': 'newdata'})
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'application/json')
+ self.assertJsonBody({'newdata': 'newdata', 'key': '0'})
+
+ def test_not_implemented(self):
+ self._put("/foo")
+ self.assertStatus(405)
+ body = self.jsonBody()
+ self.assertIsInstance(body, dict)
+ assert body['detail'] == 'Method not implemented.'
+ assert '405' in body['status']
+ assert 'traceback' in body
+
+ def test_args_from_json(self):
+ self._put("/fooargs/hello", {'name': 'world'})
+ self.assertJsonBody({'code': 'hello', 'name': 'world'})
+
+ def test_detail_route(self):
+ self._get('/foo/1/detail')
+ self.assertJsonBody({'detail': ['1', ['detail']]})
+
+ self._post('/foo/1/detail', 'post-data')
+ self.assertStatus(405)
--- /dev/null
+# -*- coding: utf-8 -*-
+# pylint: disable=W0212
+from __future__ import absolute_import
+
+import collections
+import datetime
+import importlib
+import inspect
+import json
+import os
+import pkgutil
+import sys
+import time
+import threading
+
+import cherrypy
+
+from . import logger
+
+
+def ApiController(path):
+ def decorate(cls):
+ cls._cp_controller_ = True
+ cls._cp_path_ = path
+ config = {
+ 'tools.sessions.on': True,
+ 'tools.sessions.name': Session.NAME,
+ 'tools.session_expire_at_browser_close.on': True
+ }
+ if not hasattr(cls, '_cp_config'):
+ cls._cp_config = {}
+ if 'tools.authenticate.on' not in cls._cp_config:
+ config['tools.authenticate.on'] = False
+ cls._cp_config.update(config)
+ return cls
+ return decorate
+
+
+def AuthRequired(enabled=True):
+ def decorate(cls):
+ if not hasattr(cls, '_cp_config'):
+ cls._cp_config = {
+ 'tools.authenticate.on': enabled
+ }
+ else:
+ cls._cp_config['tools.authenticate.on'] = enabled
+ return cls
+ return decorate
+
+
+def load_controllers():
+ # setting sys.path properly when not running under the mgr
+ dashboard_dir = os.path.dirname(os.path.realpath(__file__))
+ mgr_dir = os.path.dirname(dashboard_dir)
+ if mgr_dir not in sys.path:
+ sys.path.append(mgr_dir)
+
+ controllers = []
+ ctrls_path = '{}/controllers'.format(dashboard_dir)
+ mods = [mod for _, mod, _ in pkgutil.iter_modules([ctrls_path])]
+ for mod_name in mods:
+ mod = importlib.import_module('.controllers.{}'.format(mod_name),
+ package='dashboard')
+ for _, cls in mod.__dict__.items():
+ # Controllers MUST be derived from the class BaseController.
+ if inspect.isclass(cls) and issubclass(cls, BaseController) and \
+ hasattr(cls, '_cp_controller_'):
+ controllers.append(cls)
+
+ return controllers
+
+
+def json_error_page(status, message, traceback, version):
+ cherrypy.response.headers['Content-Type'] = 'application/json'
+ return json.dumps(dict(status=status, detail=message, traceback=traceback,
+ version=version))
+
+
+class BaseController(object):
+ """
+ Base class for all controllers providing API endpoints.
+ """
+
+
+class RequestLoggingTool(cherrypy.Tool):
+ def __init__(self):
+ cherrypy.Tool.__init__(self, 'before_handler', self.request_begin,
+ priority=95)
+
+ def _setup(self):
+ cherrypy.Tool._setup(self)
+ cherrypy.request.hooks.attach('on_end_request', self.request_end,
+ priority=5)
+ cherrypy.request.hooks.attach('after_error_response', self.request_error,
+ priority=5)
+
+ def _get_user(self):
+ if hasattr(cherrypy.serving, 'session'):
+ return cherrypy.session.get(Session.USERNAME)
+ return None
+
+ def request_begin(self):
+ req = cherrypy.request
+ user = self._get_user()
+ if user:
+ logger.debug("[%s:%s] [%s] [%s] %s", req.remote.ip,
+ req.remote.port, req.method, user, req.path_info)
+ else:
+ logger.debug("[%s:%s] [%s] %s", req.remote.ip,
+ req.remote.port, req.method, req.path_info)
+
+ def request_error(self):
+ self._request_log(logger.error)
+ logger.error(cherrypy.response.body)
+
+ def request_end(self):
+ status = cherrypy.response.status[:3]
+ if status in ["401"]:
+ # log unauthorized accesses
+ self._request_log(logger.warning)
+ else:
+ self._request_log(logger.info)
+
+ def _format_bytes(self, num):
+ units = ['B', 'K', 'M', 'G']
+
+ format_str = "{:.0f}{}"
+ for i, unit in enumerate(units):
+ div = 2**(10*i)
+ if num < 2**(10*(i+1)):
+ if num % div == 0:
+ format_str = "{}{}"
+ else:
+ div = float(div)
+ format_str = "{:.1f}{}"
+ return format_str.format(num/div, unit[0])
+
+ # content-length bigger than 1T!! return value in bytes
+ return "{}B".format(num)
+
+ def _request_log(self, logger_fn):
+ req = cherrypy.request
+ res = cherrypy.response
+ lat = time.time() - res.time
+ user = self._get_user()
+ status = res.status[:3] if isinstance(res.status, str) else res.status
+ if 'Content-Length' in res.headers:
+ length = self._format_bytes(res.headers['Content-Length'])
+ else:
+ length = self._format_bytes(0)
+ if user:
+ logger_fn("[%s:%s] [%s] [%s] [%s] [%s] [%s] %s", req.remote.ip,
+ req.remote.port, req.method, status,
+ "{0:.3f}s".format(lat), user, length, req.path_info)
+ else:
+ logger_fn("[%s:%s] [%s] [%s] [%s] [%s] %s", req.remote.ip,
+ req.remote.port, req.method, status,
+ "{0:.3f}s".format(lat), length, req.path_info)
+
+
+# pylint: disable=too-many-instance-attributes
+class ViewCache(object):
+ VALUE_OK = 0
+ VALUE_STALE = 1
+ VALUE_NONE = 2
+ VALUE_EXCEPTION = 3
+
+ class GetterThread(threading.Thread):
+ def __init__(self, view, fn, args, kwargs):
+ super(ViewCache.GetterThread, self).__init__()
+ self._view = view
+ self.event = threading.Event()
+ self.fn = fn
+ self.args = args
+ self.kwargs = kwargs
+
+ # pylint: disable=broad-except
+ def run(self):
+ try:
+ t0 = time.time()
+ val = self.fn(*self.args, **self.kwargs)
+ t1 = time.time()
+ except Exception as ex:
+ logger.exception("Error while calling fn=%s ex=%s", self.fn,
+ str(ex))
+ self._view.value = None
+ self._view.value_when = None
+ self._view.getter_thread = None
+ self._view.exception = ex
+ else:
+ with self._view.lock:
+ self._view.latency = t1 - t0
+ self._view.value = val
+ self._view.value_when = datetime.datetime.now()
+ self._view.getter_thread = None
+ self._view.exception = None
+
+ self.event.set()
+
+ class RemoteViewCache(object):
+ # Return stale data if
+ STALE_PERIOD = 1.0
+
+ def __init__(self, timeout):
+ self.getter_thread = None
+ # Consider data within 1s old to be sufficiently fresh
+ self.timeout = timeout
+ self.event = threading.Event()
+ self.value_when = None
+ self.value = None
+ self.latency = 0
+ self.exception = None
+ self.lock = threading.Lock()
+
+ def run(self, fn, args, kwargs):
+ """
+ If data less than `stale_period` old is available, return it
+ immediately.
+ If an attempt to fetch data does not complete within `timeout`, then
+ return the most recent data available, with a status to indicate that
+ it is stale.
+
+ Initialization does not count towards the timeout, so the first call
+ on one of these objects during the process lifetime may be slower
+ than subsequent calls.
+
+ :return: 2-tuple of value status code, value
+ """
+ with self.lock:
+ now = datetime.datetime.now()
+ if self.value_when and now - self.value_when < datetime.timedelta(
+ seconds=self.STALE_PERIOD):
+ return ViewCache.VALUE_OK, self.value
+
+ if self.getter_thread is None:
+ self.getter_thread = ViewCache.GetterThread(self, fn, args,
+ kwargs)
+ self.getter_thread.start()
+
+ ev = self.getter_thread.event
+
+ success = ev.wait(timeout=self.timeout)
+
+ with self.lock:
+ if success:
+ # We fetched the data within the timeout
+ if self.exception:
+ # execution raised an exception
+ return ViewCache.VALUE_EXCEPTION, self.exception
+ return ViewCache.VALUE_OK, self.value
+ elif self.value_when is not None:
+ # We have some data, but it doesn't meet freshness requirements
+ return ViewCache.VALUE_STALE, self.value
+ # We have no data, not even stale data
+ return ViewCache.VALUE_NONE, None
+
+ def __init__(self, timeout=5):
+ self.timeout = timeout
+ self.cache_by_args = {}
+
+ def __call__(self, fn):
+ def wrapper(*args, **kwargs):
+ rvc = self.cache_by_args.get(args, None)
+ if not rvc:
+ rvc = ViewCache.RemoteViewCache(self.timeout)
+ self.cache_by_args[args] = rvc
+ return rvc.run(fn, args, kwargs)
+ return wrapper
+
+
+class RESTController(BaseController):
+ """
+ Base class for providing a RESTful interface to a resource.
+
+ To use this class, simply derive a class from it and implement the methods
+ you want to support. The list of possible methods are:
+
+ * list()
+ * bulk_set(data)
+ * create(data)
+ * bulk_delete()
+ * get(key)
+ * set(data, key)
+ * delete(key)
+
+ Test with curl:
+
+ curl -H "Content-Type: application/json" -X POST \
+ -d '{"username":"xyz","password":"xyz"}' http://127.0.0.1:8080/foo
+ curl http://127.0.0.1:8080/foo
+ curl http://127.0.0.1:8080/foo/0
+
+ """
+
+ def _not_implemented(self, is_sub_path):
+ methods = [method
+ for ((method, _is_element), (meth, _))
+ in self._method_mapping.items()
+ if _is_element == is_sub_path is not None and hasattr(self, meth)]
+ cherrypy.response.headers['Allow'] = ','.join(methods)
+ raise cherrypy.HTTPError(405, 'Method not implemented.')
+
+ _method_mapping = {
+ ('GET', False): ('list', 200),
+ ('PUT', False): ('bulk_set', 200),
+ ('PATCH', False): ('bulk_set', 200),
+ ('POST', False): ('create', 201),
+ ('DELETE', False): ('bulk_delete', 204),
+ ('GET', True): ('get', 200),
+ ('PUT', True): ('set', 200),
+ ('PATCH', True): ('set', 200),
+ ('DELETE', True): ('delete', 204),
+ }
+
+ def _get_method(self, vpath):
+ is_sub_path = bool(len(vpath))
+ try:
+ method_name, status_code = self._method_mapping[
+ (cherrypy.request.method, is_sub_path)]
+ except KeyError:
+ self._not_implemented(is_sub_path)
+ method = getattr(self, method_name, None)
+ if not method:
+ self._not_implemented(is_sub_path)
+ return method, status_code
+
+ @cherrypy.expose
+ def default(self, *vpath, **params):
+ method, status_code = self._get_method(vpath)
+
+ if cherrypy.request.method not in ['GET', 'DELETE']:
+ method = RESTController._takes_json(method)
+
+ if cherrypy.request.method != 'DELETE':
+ method = RESTController._returns_json(method)
+
+ cherrypy.response.status = status_code
+
+ return method(*vpath, **params)
+
+ @staticmethod
+ def args_from_json(func):
+ func._args_from_json_ = True
+ return func
+
+ # pylint: disable=W1505
+ @staticmethod
+ def _takes_json(func):
+ def inner(*args, **kwargs):
+ content_length = int(cherrypy.request.headers['Content-Length'])
+ body = cherrypy.request.body.read(content_length)
+ if not body:
+ raise cherrypy.HTTPError(400, 'Empty body. Content-Length={}'
+ .format(content_length))
+ try:
+ data = json.loads(body.decode('utf-8'))
+ except Exception as e:
+ raise cherrypy.HTTPError(400, 'Failed to decode JSON: {}'
+ .format(str(e)))
+ if hasattr(func, '_args_from_json_'):
+ if sys.version_info > (3, 0):
+ f_args = list(inspect.signature(func).parameters.keys())
+ else:
+ f_args = inspect.getargspec(func).args[1:]
+ n_args = []
+ for arg in args:
+ n_args.append(arg)
+ for arg in f_args:
+ if arg in data:
+ n_args.append(data[arg])
+ data.pop(arg)
+ kwargs.update(data)
+ return func(*n_args, **kwargs)
+
+ return func(data, *args, **kwargs)
+ return inner
+
+ @staticmethod
+ def _returns_json(func):
+ def inner(*args, **kwargs):
+ cherrypy.response.headers['Content-Type'] = 'application/json'
+ ret = func(*args, **kwargs)
+ return json.dumps(ret).encode('utf8')
+ return inner
+
+ @staticmethod
+ def split_vpath(vpath):
+ if not vpath:
+ return None, None
+ if len(vpath) == 1:
+ return vpath[0], None
+ return vpath[0], vpath[1]
+
+
+class Session(object):
+ """
+ This class contains all relevant settings related to cherrypy.session.
+ """
+ NAME = 'session_id'
+
+ # The keys used to store the information in the cherrypy.session.
+ USERNAME = '_username'
+ TS = '_ts'
+ EXPIRE_AT_BROWSER_CLOSE = '_expire_at_browser_close'
+
+ # The default values.
+ DEFAULT_EXPIRE = 1200.0
+
+
+class SessionExpireAtBrowserCloseTool(cherrypy.Tool):
+ """
+ A CherryPi Tool which takes care that the cookie does not expire
+ at browser close if the 'Keep me logged in' checkbox was selected
+ on the login page.
+ """
+ def __init__(self):
+ cherrypy.Tool.__init__(self, 'before_finalize', self._callback)
+
+ def _callback(self):
+ # Shall the cookie expire at browser close?
+ expire_at_browser_close = cherrypy.session.get(
+ Session.EXPIRE_AT_BROWSER_CLOSE, True)
+ logger.debug("expire at browser close: %s", expire_at_browser_close)
+ if expire_at_browser_close:
+ # Get the cookie and its name.
+ cookie = cherrypy.response.cookie
+ name = cherrypy.request.config.get(
+ 'tools.sessions.name', Session.NAME)
+ # Make the cookie a session cookie by purging the
+ # fields 'expires' and 'max-age'.
+ logger.debug("expire at browser close: removing 'expires' and 'max-age'")
+ if name in cookie:
+ del cookie[name]['expires']
+ del cookie[name]['max-age']
+
+
+class NotificationQueue(threading.Thread):
+ _ALL_TYPES_ = '__ALL__'
+ _listeners = collections.defaultdict(set)
+ _lock = threading.Lock()
+ _cond = threading.Condition()
+ _queue = collections.deque()
+ _running = False
+ _instance = None
+
+ def __init__(self):
+ super(NotificationQueue, self).__init__()
+
+ @classmethod
+ def start_queue(cls):
+ with cls._lock:
+ if cls._instance:
+ # the queue thread is already running
+ return
+ cls._running = True
+ cls._instance = NotificationQueue()
+ logger.debug("starting notification queue")
+ cls._instance.start()
+
+ @classmethod
+ def stop(cls):
+ with cls._lock:
+ if not cls._instance:
+ # the queue thread was not started
+ return
+ instance = cls._instance
+ cls._instance = None
+ cls._running = False
+ with cls._cond:
+ cls._cond.notify()
+ logger.debug("waiting for notification queue to finish")
+ instance.join()
+ logger.debug("notification queue stopped")
+
+ @classmethod
+ def register(cls, func, types=None):
+ """Registers function to listen for notifications
+
+ If the second parameter `types` is omitted, the function in `func`
+ parameter will be called for any type of notifications.
+
+ Args:
+ func (function): python function ex: def foo(val)
+ types (str|list): the single type to listen, or a list of types
+ """
+ with cls._lock:
+ if not types:
+ cls._listeners[cls._ALL_TYPES_].add(func)
+ return
+ if isinstance(types, str):
+ cls._listeners[types].add(func)
+ elif isinstance(types, list):
+ for typ in types:
+ cls._listeners[typ].add(func)
+ else:
+ raise Exception("types param is neither a string nor a list")
+
+ @classmethod
+ def new_notification(cls, notify_type, notify_value):
+ cls._queue.append((notify_type, notify_value))
+ with cls._cond:
+ cls._cond.notify()
+
+ @classmethod
+ def notify_listeners(cls, events):
+ for ev in events:
+ notify_type, notify_value = ev
+ with cls._lock:
+ listeners = list(cls._listeners[notify_type])
+ listeners.extend(cls._listeners[cls._ALL_TYPES_])
+ for listener in listeners:
+ listener(notify_value)
+
+ def run(self):
+ logger.debug("notification queue started")
+ while self._running:
+ private_buffer = []
+ logger.debug("NQ: processing queue: %s", len(self._queue))
+ try:
+ while True:
+ private_buffer.append(self._queue.popleft())
+ except IndexError:
+ pass
+ self.notify_listeners(private_buffer)
+ with self._cond:
+ self._cond.wait(1.0)
+ # flush remaining events
+ logger.debug("NQ: flush remaining events: %s", len(self._queue))
+ self.notify_listeners(self._queue)
+ self._queue.clear()
+ logger.debug("notification queue finished")
--- /dev/null
+[tox]
+envlist = cov-init,py27,py3,cov-report,lint
+skipsdist = true
+
+[testenv]
+deps=-r{toxinidir}/requirements.txt
+setenv=
+ UNITTEST=true
+ WEBTEST_INTERACTIVE=false
+ COVERAGE_FILE= .coverage.{envname}
+ PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.3:{toxinidir}/../../../../build/lib/cython_modules/lib.2
+ LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
+ PATH = {toxinidir}/../../../../build/bin:$PATH
+commands=
+ {envbindir}/py.test --cov=. --cov-report= --junitxml=junit.{envname}.xml --doctest-modules controllers/rbd.py tests/
+
+[testenv:cov-init]
+setenv =
+ COVERAGE_FILE = .coverage
+deps = coverage
+commands =
+ coverage erase
+
+[testenv:cov-report]
+setenv =
+ COVERAGE_FILE = .coverage
+deps = coverage
+commands =
+ coverage combine
+ coverage report
+ coverage xml
+
+[testenv:lint]
+setenv =
+ PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.3:{toxinidir}/../../../../build/lib/cython_modules/lib.2
+ LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
+deps=-r{toxinidir}/requirements.txt
+commands=
+ pylint --rcfile=.pylintrc --jobs=5 . module.py tools.py controllers tests services
+ pycodestyle --max-line-length=100 --exclude=python2.7,.tox,venv,frontend --ignore=E402,E121,E123,E126,E226,E24,E704,W503 .
+++ /dev/null
-[run]
-omit = tests/*
- */python*/*
- ceph_module_mock.py
- __init__.py
- */mgr_module.py
-
+++ /dev/null
-# EditorConfig helps developers define and maintain consistent coding styles
-# between different editors and IDEs.: http://EditorConfig.org
-
-# top-most EditorConfig file
-root = true
-
-# Unix-style newlines with a newline ending every file
-[*]
-end_of_line = lf
-insert_final_newline = true
-
-# Set default charset
-[*.{js,py}]
-charset = utf-8
-
-# 4 space indentation for Python files
-[*.py]
-indent_style = space
-indent_size = 4
-
-# Indentation override for all JS under frontend directory
-[frontend/**.js]
-indent_style = space
-indent_size = 2
-
-# Indentation override for all HTML under frontend directory
-[frontend/**.html]
-indent_style = space
-indent_size = 2
+++ /dev/null
-.coverage*
-htmlcov
-.tox
-coverage.xml
-junit*xml
-__pycache__
-.cache
-ceph.conf
-wheelhouse*
-
-# IDE
-.vscode
-.idea
-*.egg
-
-# virtualenv
-venv
+++ /dev/null
-[MASTER]
-
-# A comma-separated list of package or module names from where C extensions may
-# be loaded. Extensions are loading into the active Python interpreter and may
-# run arbitrary code
-extension-pkg-whitelist=rados,rbd
-
-# Add files or directories to the blacklist. They should be base names, not
-# paths.
-ignore=CVS
-
-# Add files or directories matching the regex patterns to the blacklist. The
-# regex matches against base names, not paths.
-ignore-patterns=
-
-# Python code to execute, usually for sys.path manipulation such as
-# pygtk.require().
-#init-hook=
-
-# Use multiple processes to speed up Pylint.
-jobs=1
-
-# List of plugins (as comma separated values of python modules names) to load,
-# usually to register additional checkers.
-load-plugins=
-
-# Pickle collected data for later comparisons.
-persistent=yes
-
-# Specify a configuration file.
-#rcfile=
-
-# When enabled, pylint would attempt to guess common misconfiguration and emit
-# user-friendly hints instead of false-positive error messages
-suggestion-mode=yes
-
-# Allow loading of arbitrary C extensions. Extensions are imported into the
-# active Python interpreter and may run arbitrary code.
-unsafe-load-any-extension=no
-
-
-[MESSAGES CONTROL]
-
-# Only show warnings with the listed confidence levels. Leave empty to show
-# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
-confidence=
-
-# Disable the message, report, category or checker with the given id(s). You
-# can either give multiple identifiers separated by comma (,) or put this
-# option multiple times (only on the command line, not in the configuration
-# file where it should appear only once).You can also use "--disable=all" to
-# disable everything first and then reenable specific checks. For example, if
-# you want to run only the similarities checker, you can use "--disable=all
-# --enable=similarities". If you want to run only the classes checker, but have
-# no Warning level messages displayed, use"--disable=all --enable=classes
-# --disable=W"
-disable=print-statement,
- parameter-unpacking,
- unpacking-in-except,
- old-raise-syntax,
- backtick,
- long-suffix,
- old-ne-operator,
- old-octal-literal,
- import-star-module-level,
- non-ascii-bytes-literal,
- raw-checker-failed,
- bad-inline-option,
- locally-disabled,
- locally-enabled,
- file-ignored,
- suppressed-message,
- useless-suppression,
- deprecated-pragma,
- apply-builtin,
- basestring-builtin,
- buffer-builtin,
- cmp-builtin,
- coerce-builtin,
- execfile-builtin,
- file-builtin,
- long-builtin,
- raw_input-builtin,
- reduce-builtin,
- standarderror-builtin,
- unicode-builtin,
- xrange-builtin,
- coerce-method,
- delslice-method,
- getslice-method,
- setslice-method,
- no-absolute-import,
- old-division,
- dict-iter-method,
- dict-view-method,
- next-method-called,
- metaclass-assignment,
- indexing-exception,
- raising-string,
- reload-builtin,
- oct-method,
- hex-method,
- nonzero-method,
- cmp-method,
- input-builtin,
- round-builtin,
- intern-builtin,
- unichr-builtin,
- map-builtin-not-iterating,
- zip-builtin-not-iterating,
- range-builtin-not-iterating,
- filter-builtin-not-iterating,
- using-cmp-argument,
- eq-without-hash,
- div-method,
- idiv-method,
- rdiv-method,
- exception-message-attribute,
- invalid-str-codec,
- sys-max-int,
- bad-python3-import,
- deprecated-string-function,
- deprecated-str-translate-call,
- deprecated-itertools-function,
- deprecated-types-field,
- next-method-defined,
- dict-items-not-iterating,
- dict-keys-not-iterating,
- dict-values-not-iterating,
- missing-docstring,
- invalid-name,
- no-self-use,
- too-few-public-methods,
- no-member,
- fixme
-
-# Enable the message, report, category or checker with the given id(s). You can
-# either give multiple identifier separated by comma (,) or put this option
-# multiple time (only on the command line, not in the configuration file where
-# it should appear only once). See also the "--disable" option for examples.
-enable=c-extension-no-member
-
-
-[REPORTS]
-
-# Python expression which should return a note less than 10 (10 is the highest
-# note). You have access to the variables errors warning, statement which
-# respectively contain the number of errors / warnings messages and the total
-# number of statements analyzed. This is used by the global evaluation report
-# (RP0004).
-evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-
-# Template used to display messages. This is a python new-style format string
-# used to format the message information. See doc for all details
-#msg-template=
-
-# Set the output format. Available formats are text, parseable, colorized, json
-# and msvs (visual studio).You can also give a reporter class, eg
-# mypackage.mymodule.MyReporterClass.
-output-format=text
-
-# Tells whether to display a full report or only the messages
-reports=no
-
-# Activate the evaluation score.
-score=yes
-
-
-[REFACTORING]
-
-# Maximum number of nested blocks for function / method body
-max-nested-blocks=5
-
-# Complete name of functions that never returns. When checking for
-# inconsistent-return-statements if a never returning function is called then
-# it will be considered as an explicit return statement and no message will be
-# printed.
-never-returning-functions=optparse.Values,sys.exit
-
-
-[VARIABLES]
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid to define new builtins when possible.
-additional-builtins=
-
-# Tells whether unused global variables should be treated as a violation.
-allow-global-unused-variables=yes
-
-# List of strings which can identify a callback function by name. A callback
-# name must start or end with one of those strings.
-callbacks=cb_,
- _cb
-
-# A regular expression matching the name of dummy variables (i.e. expectedly
-# not used).
-dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
-
-# Argument names that match this expression will be ignored. Default to name
-# with leading underscore
-ignored-argument-names=_.*|^ignored_|^unused_
-
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
-
-# List of qualified module names which can have objects that can redefine
-# builtins.
-redefining-builtins-modules=six.moves,past.builtins,future.builtins
-
-
-[BASIC]
-
-# Naming style matching correct argument names
-argument-naming-style=snake_case
-
-# Regular expression matching correct argument names. Overrides argument-
-# naming-style
-#argument-rgx=
-
-# Naming style matching correct attribute names
-attr-naming-style=snake_case
-
-# Regular expression matching correct attribute names. Overrides attr-naming-
-# style
-#attr-rgx=
-
-# Bad variable names which should always be refused, separated by a comma
-bad-names=foo,
- bar,
- baz,
- toto,
- tutu,
- tata
-
-# Naming style matching correct class attribute names
-class-attribute-naming-style=any
-
-# Regular expression matching correct class attribute names. Overrides class-
-# attribute-naming-style
-#class-attribute-rgx=
-
-# Naming style matching correct class names
-class-naming-style=PascalCase
-
-# Regular expression matching correct class names. Overrides class-naming-style
-#class-rgx=
-
-# Naming style matching correct constant names
-const-naming-style=UPPER_CASE
-
-# Regular expression matching correct constant names. Overrides const-naming-
-# style
-#const-rgx=
-
-# Minimum line length for functions/classes that require docstrings, shorter
-# ones are exempt.
-docstring-min-length=-1
-
-# Naming style matching correct function names
-function-naming-style=snake_case
-
-# Regular expression matching correct function names. Overrides function-
-# naming-style
-#function-rgx=
-
-# Good variable names which should always be accepted, separated by a comma
-good-names=i,
- j,
- k,
- ex,
- Run,
- _
-
-# Include a hint for the correct naming format with invalid-name
-include-naming-hint=no
-
-# Naming style matching correct inline iteration names
-inlinevar-naming-style=any
-
-# Regular expression matching correct inline iteration names. Overrides
-# inlinevar-naming-style
-#inlinevar-rgx=
-
-# Naming style matching correct method names
-method-naming-style=snake_case
-
-# Regular expression matching correct method names. Overrides method-naming-
-# style
-#method-rgx=
-
-# Naming style matching correct module names
-module-naming-style=snake_case
-
-# Regular expression matching correct module names. Overrides module-naming-
-# style
-#module-rgx=
-
-# Colon-delimited sets of names that determine each other's naming style when
-# the name regexes allow several styles.
-name-group=
-
-# Regular expression which should only match function or class names that do
-# not require a docstring.
-no-docstring-rgx=^_
-
-# List of decorators that produce properties, such as abc.abstractproperty. Add
-# to this list to register other decorators that produce valid properties.
-property-classes=abc.abstractproperty
-
-# Naming style matching correct variable names
-variable-naming-style=snake_case
-
-# Regular expression matching correct variable names. Overrides variable-
-# naming-style
-#variable-rgx=
-
-
-[FORMAT]
-
-# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
-expected-line-ending-format=
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=^\s*(# )?<?https?://\S+>?$
-
-# Number of spaces of indent required inside a hanging or continued line.
-indent-after-paren=4
-
-# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
-# tab).
-indent-string=' '
-
-# Maximum number of characters on a single line.
-max-line-length=100
-
-# Maximum number of lines in a module
-max-module-lines=1000
-
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=trailing-comma,
- dict-separator
-
-# Allow the body of a class to be on the same line as the declaration if body
-# contains single statement.
-single-line-class-stmt=no
-
-# Allow the body of an if to be on the same line as the test if there is no
-# else.
-single-line-if-stmt=no
-
-
-[SPELLING]
-
-# Limits count of emitted suggestions for spelling mistakes
-max-spelling-suggestions=4
-
-# Spelling dictionary name. Available dictionaries: none. To make it working
-# install python-enchant package.
-spelling-dict=
-
-# List of comma separated words that should not be checked.
-spelling-ignore-words=
-
-# A path to a file that contains private dictionary; one word per line.
-spelling-private-dict-file=
-
-# Tells whether to store unknown words to indicated private dictionary in
-# --spelling-private-dict-file option instead of raising a message.
-spelling-store-unknown-words=no
-
-
-[TYPECHECK]
-
-# List of decorators that produce context managers, such as
-# contextlib.contextmanager. Add to this list to register other decorators that
-# produce valid context managers.
-contextmanager-decorators=contextlib.contextmanager
-
-# List of members which are set dynamically and missed by pylint inference
-# system, and so shouldn't trigger E1101 when accessed. Python regular
-# expressions are accepted.
-generated-members=
-
-# Tells whether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-ignore-mixin-members=yes
-
-# This flag controls whether pylint should warn about no-member and similar
-# checks whenever an opaque object is returned when inferring. The inference
-# can return multiple potential results while evaluating a Python object, but
-# some branches might not be evaluated, which results in partial inference. In
-# that case, it might be useful to still emit no-member and other checks for
-# the rest of the inferred objects.
-ignore-on-opaque-inference=yes
-
-# List of class names for which member attributes should not be checked (useful
-# for classes with dynamically set attributes). This supports the use of
-# qualified names.
-ignored-classes=optparse.Values,thread._local,_thread._local
-
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis. It
-# supports qualified module names, as well as Unix pattern matching.
-ignored-modules=
-
-# Show a hint with possible names when a member name was not found. The aspect
-# of finding the hint is based on edit distance.
-missing-member-hint=yes
-
-# The minimum edit distance a name should have in order to be considered a
-# similar match for a missing member name.
-missing-member-hint-distance=1
-
-# The total number of similar names that should be taken in consideration when
-# showing a hint for a missing member.
-missing-member-max-choices=1
-
-
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-notes=FIXME,
- XXX,
- TODO
-
-
-[LOGGING]
-
-# Logging modules to check that the string format arguments are in logging
-# function parameter format
-logging-modules=logging
-
-
-[SIMILARITIES]
-
-# Ignore comments when computing similarities.
-ignore-comments=yes
-
-# Ignore docstrings when computing similarities.
-ignore-docstrings=yes
-
-# Ignore imports when computing similarities.
-ignore-imports=no
-
-# Minimum lines number of a similarity.
-min-similarity-lines=4
-
-
-[IMPORTS]
-
-# Allow wildcard imports from modules that define __all__.
-allow-wildcard-with-all=no
-
-# Analyse import fallback blocks. This can be used to support both Python 2 and
-# 3 compatible code, which means that the block might have code that exists
-# only in one or another interpreter, leading to false positives when analysed.
-analyse-fallback-blocks=no
-
-# Deprecated modules which should not be used, separated by a comma
-deprecated-modules=regsub,
- TERMIOS,
- Bastion,
- rexec
-
-# Create a graph of external dependencies in the given file (report RP0402 must
-# not be disabled)
-ext-import-graph=
-
-# Create a graph of every (i.e. internal and external) dependencies in the
-# given file (report RP0402 must not be disabled)
-import-graph=
-
-# Create a graph of internal dependencies in the given file (report RP0402 must
-# not be disabled)
-int-import-graph=
-
-# Force import order to recognize a module as part of the standard
-# compatibility libraries.
-known-standard-library=
-
-# Force import order to recognize a module as part of a third party library.
-known-third-party=enchant
-
-
-[CLASSES]
-
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,
- __new__,
- setUp
-
-# List of member names, which should be excluded from the protected access
-# warning.
-exclude-protected=_asdict,
- _fields,
- _replace,
- _source,
- _make
-
-# List of valid names for the first argument in a class method.
-valid-classmethod-first-arg=cls
-
-# List of valid names for the first argument in a metaclass class method.
-valid-metaclass-classmethod-first-arg=mcs
-
-
-[DESIGN]
-
-# Maximum number of arguments for function / method
-max-args=5
-
-# Maximum number of attributes for a class (see R0902).
-max-attributes=7
-
-# Maximum number of boolean expressions in a if statement
-max-bool-expr=5
-
-# Maximum number of branch for function / method body
-max-branches=12
-
-# Maximum number of locals for function / method body
-max-locals=15
-
-# Maximum number of parents for a class (see R0901).
-max-parents=7
-
-# Maximum number of public methods for a class (see R0904).
-max-public-methods=20
-
-# Maximum number of return / yield for function / method body
-max-returns=6
-
-# Maximum number of statements in function / method body
-max-statements=50
-
-# Minimum number of public methods for a class (see R0903).
-min-public-methods=2
-
-
-[EXCEPTIONS]
-
-# Exceptions that will emit a warning when being caught. Defaults to
-# "Exception"
-overgeneral-exceptions=Exception
+++ /dev/null
-set(MGR_DASHBOARD_V2_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/mgr-dashboard_v2-virtualenv)
-
-add_custom_target(mgr-dashboard_v2-test-venv
- COMMAND
- ${CMAKE_SOURCE_DIR}/src/tools/setup-virtualenv.sh ${MGR_DASHBOARD_V2_VIRTUALENV} &&
- ${MGR_DASHBOARD_V2_VIRTUALENV}/bin/pip install --no-index --use-wheel --find-links=file:${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/wheelhouse -r requirements.txt
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2
- COMMENT "dashboard_v2 tests virtualenv is being created")
-add_dependencies(tests mgr-dashboard_v2-test-venv)
-
-if(WITH_MGR_DASHBOARD_V2_FRONTEND AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
- find_program(NPM_BIN
- NAMES npm
- HINTS $ENV{NPM_ROOT}/bin)
- if(NOT NPM_BIN)
- message(FATAL_ERROR "WITH_MGR_DASHBOARD_V2_FRONTEND set, but npm not found")
- endif()
-
-add_custom_command(
- OUTPUT "${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend/node_modules"
- COMMAND ${NPM_BIN} install
- DEPENDS frontend/package.json
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend
- COMMENT "dashboard_v2 frontend dependencies are being installed"
-)
-
-add_custom_target(mgr-dashboard_v2-frontend-deps
- DEPENDS frontend/node_modules
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend
-)
-
-# Glob some frontend files. With CMake 3.6, this can be simplified
-# to *.ts *.html. Just add:
-# list(FILTER frontend_src INCLUDE REGEX "frontend/src")
-file(
- GLOB_RECURSE frontend_src
- frontend/src/*.ts
- frontend/src/*.html
- frontend/src/*/*.ts
- frontend/src/*/*.html
- frontend/src/*/*/*.ts
- frontend/src/*/*/*.html
- frontend/src/*/*/*/*.ts
- frontend/src/*/*/*/*.html
- frontend/src/*/*/*/*/*.ts
- frontend/src/*/*/*/*/*.html
- frontend/src/*/*/*/*/*/*.ts
- frontend/src/*/*/*/*/*/*.html)
-
-if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
- set(npm_command ${NPM_BIN} run build -- --prod)
-else()
- set(npm_command ${NPM_BIN} run build)
-endif()
-
-add_custom_command(
- OUTPUT "${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend/dist"
- COMMAND ${npm_command}
- DEPENDS ${frontend_src} frontend/node_modules
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend
- COMMENT "dashboard_v2 frontend is being created"
-)
-add_custom_target(mgr-dashboard_v2-frontend-build
- DEPENDS frontend/dist mgr-dashboard_v2-frontend-deps
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/frontend
-)
-add_dependencies(ceph-mgr mgr-dashboard_v2-frontend-build)
-endif(WITH_MGR_DASHBOARD_V2_FRONTEND AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
+++ /dev/null
-Dashboard v2 Developer Documentation
-====================================
-
-Frontend Development
---------------------
-
-Before you can start the dashboard from within a development environment, you
-will need to generate the frontend code and either use a compiled and running
-Ceph cluster (e.g. started by ``vstart.sh``) or the standalone development web
-server.
-
-The build process is based on `Node.js <https://nodejs.org/>`_ and requires the
-`Node Package Manager <https://www.npmjs.com/>`_ ``npm`` to be installed.
-
-Prerequisites
-~~~~~~~~~~~~~
-
-Run ``npm install`` in directory ``src/pybind/mgr/dashboard_v2/frontend`` to
-install the required packages locally.
-
-.. note::
-
- If you do not have the `Angular CLI <https://github.com/angular/angular-cli>`_
- installed globally, then you need to execute ``ng`` commands with an
- additional ``npm run`` before it.
-
-Setting up a Development Server
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Create the ``proxy.conf.json`` file based on ``proxy.conf.json.sample``.
-
-Run ``npm start -- --proxy-config proxy.conf.json`` for a dev server.
-Navigate to ``http://localhost:4200/``. The app will automatically
-reload if you change any of the source files.
-
-Code Scaffolding
-~~~~~~~~~~~~~~~~
-
-Run ``ng generate component component-name`` to generate a new
-component. You can also use
-``ng generate directive|pipe|service|class|guard|interface|enum|module``.
-
-Build the Project
-~~~~~~~~~~~~~~~~~
-
-Run ``npm run build`` to build the project. The build artifacts will be
-stored in the ``dist/`` directory. Use the ``-prod`` flag for a
-production build. Navigate to ``http://localhost:8080``.
-
-Running Unit Tests
-~~~~~~~~~~~~~~~~~~
-
-Run ``npm run test`` to execute the unit tests via `Karma
-<https://karma-runner.github.io>`_.
-
-Running End-to-End Tests
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-Run ``npm run e2e`` to execute the end-to-end tests via
-`Protractor <http://www.protractortest.org/>`__.
-
-Further Help
-~~~~~~~~~~~~
-
-To get more help on the Angular CLI use ``ng help`` or go check out the
-`Angular CLI
-README <https://github.com/angular/angular-cli/blob/master/README.md>`__.
-
-Example of a Generator
-~~~~~~~~~~~~~~~~~~~~~~
-
-::
-
- # Create module 'Core'
- src/app> ng generate module core -m=app --routing
-
- # Create module 'Auth' under module 'Core'
- src/app/core> ng generate module auth -m=core --routing
- or, alternatively:
- src/app> ng generate module core/auth -m=core --routing
-
- # Create component 'Login' under module 'Auth'
- src/app/core/auth> ng generate component login -m=core/auth
- or, alternatively:
- src/app> ng generate component core/auth/login -m=core/auth
-
-Frontend Typescript Code Style Guide Recommendations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Group the imports based on its source and separate them with a blank
-line.
-
-The source groups can be either from Angular, external or internal.
-
-Example:
-
-.. code:: javascript
-
- import { Component } from '@angular/core';
- import { Router } from '@angular/router';
-
- import { ToastsManager } from 'ng2-toastr';
-
- import { Credentials } from '../../../shared/models/credentials.model';
- import { HostService } from './services/host.service';
-
-
-Backend Development
--------------------
-
-The Python backend code of this module requires a number of Python modules to be
-installed. They are listed in file ``requirements.txt``. Using `pip
-<https://pypi.python.org/pypi/pip>`_ you may install all required dependencies
-by issuing ``pip install -r requirements.txt`` in directory
-``src/pybind/mgr/dashboard_v2``.
-
-If you're using the `ceph-dev-docker development environment
-<https://github.com/ricardoasmarques/ceph-dev-docker/>`_, simply run
-``./install_deps.sh`` from the toplevel directory to install them.
-
-Unit Testing and Linting
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-We included a ``tox`` configuration file that will run the unit tests under
-Python 2 or 3, as well as linting tools to guarantee the uniformity of code.
-
-You need to install ``tox`` and ``coverage`` before running it. To install the
-packages in your system, either install it via your operating system's package
-management tools, e.g. by running ``dnf install python-tox python-coverage`` on
-Fedora Linux.
-
-Alternatively, you can use Python's native package installation method::
-
- $ pip install tox
- $ pip install coverage
-
-The unit tests must run against a real Ceph cluster (no mocks are used). This
-has the advantage of catching bugs originated from changes in the internal Ceph
-code.
-
-Our ``tox.ini`` script will start a ``vstart`` Ceph cluster before running the
-python unit tests, and then it stops the cluster after the tests are run. Of
-course this implies that you have built/compiled Ceph previously.
-
-To run tox, run the following command in the root directory (where ``tox.ini``
-is located)::
-
- $ PATH=../../../../build/bin:$PATH tox
-
-We also collect coverage information from the backend code. You can check the
-coverage information provided by the tox output, or by running the following
-command after tox has finished successfully::
-
- $ coverage html
-
-This command will create a directory ``htmlcov`` with an HTML representation of
-the code coverage of the backend.
-
-You can also run a single step of the tox script (aka tox environment), for
-instance if you only want to run the linting tools, do::
-
- $ PATH=../../../../build/bin:$PATH tox -e lint
-
-How to run a single unit test without using ``tox``?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-When developing the code of a controller and respective test code, it's useful
-to be able to run that single test file without going through the whole ``tox``
-workflow.
-
-Since the tests must run against a real Ceph cluster, the first thing is to have
-a Ceph cluster running. For that we can leverage the tox environment that starts
-a Ceph cluster::
-
- $ PATH=../../../../build/bin:$PATH tox -e ceph-cluster-start
-
-The command above uses ``vstart.sh`` script to start a Ceph cluster and
-automatically enables the ``dashboard_v2`` module, and configures its cherrypy
-web server to listen in port ``9865``.
-
-After starting the Ceph cluster we can run our test file using ``py.test`` like
-this::
-
- DASHBOARD_V2_PORT=9865 UNITTEST=true py.test -s tests/test_mycontroller.py
-
-You can run tests multiple times without having to start and stop the Ceph
-cluster.
-
-After you finish your tests, you can stop the Ceph cluster using another tox
-environment::
-
- $ tox -e ceph-cluster-stop
-
-How to add a new controller?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If you want to add a new endpoint to the backend, you just need to add a
-class derived from ``BaseController`` decorated with ``ApiController`` in a
-Python file located under the ``controllers`` directory. The Dashboard module
-will automatically load your new controller upon start.
-
-For example create a file ``ping2.py`` under ``controllers`` directory with the
-following code::
-
- import cherrypy
- from ..tools import ApiController, BaseController
-
- @ApiController('ping2')
- class Ping2(BaseController):
- @cherrypy.expose
- def default(self, *args):
- return "Hello"
-
-Every path given in the ``ApiController`` decorator will automatically be
-prefixed with ``api``. After reloading the Dashboard module you can access the
-above mentioned controller by pointing your browser to
-http://mgr_hostname:8080/api/ping2.
-
-It is also possible to have nested controllers. The ``RgwController`` uses
-this technique to make the daemons available through the URL
-http://mgr_hostname:8080/api/rgw/daemon::
-
- @ApiController('rgw')
- @AuthRequired()
- class Rgw(RESTController):
- pass
-
-
- @ApiController('rgw/daemon')
- @AuthRequired()
- class RgwDaemon(RESTController):
-
- def list(self):
- pass
-
-
-Note that paths must be unique and that a path like ``rgw/daemon`` has to have
-a parent ``rgw``. Otherwise it won't work.
-
-How does the RESTController work?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-We also provide a simple mechanism to create REST based controllers using the
-``RESTController`` class. Any class which inherits from ``RESTController`` will,
-by default, return JSON.
-
-The ``RESTController`` is basically an additional abstraction layer which eases
-and unifies the work with collections. A collection is just an array of objects
-with a specific type. ``RESTController`` enables some default mappings of
-request types and given parameters to specific method names. This may sound
-complicated at first, but it's fairly easy. Lets have look at the following
-example::
-
- import cherrypy
- from ..tools import ApiController, RESTController
-
- @ApiController('ping2')
- class Ping2(RESTController):
- def list(self):
- return {"msg": "Hello"}
-
- def get(self, id):
- return self.objects[id]
-
-In this case, the ``list`` method is automatically used for all requests to
-``api/ping2`` where no additional argument is given and where the request type
-is ``GET``. If the request is given an additional argument, the ID in our
-case, it won't map to ``list`` anymore but to ``get`` and return the element
-with the given ID (assuming that ``self.objects`` has been filled before). The
-same applies to other request types:
-
-+--------------+------------+----------------+-------------+
-| Request type | Arguments | Method | Status Code |
-+==============+============+================+=============+
-| GET | No | list | 200 |
-+--------------+------------+----------------+-------------+
-| PUT | No | bulk_set | 200 |
-+--------------+------------+----------------+-------------+
-| PATCH | No | bulk_set | 200 |
-+--------------+------------+----------------+-------------+
-| POST | No | create | 201 |
-+--------------+------------+----------------+-------------+
-| DELETE | No | bulk_delete | 204 |
-+--------------+------------+----------------+-------------+
-| GET | Yes | get | 200 |
-+--------------+------------+----------------+-------------+
-| PUT | Yes | set | 200 |
-+--------------+------------+----------------+-------------+
-| PATCH | Yes | set | 200 |
-+--------------+------------+----------------+-------------+
-| DELETE | Yes | delete | 204 |
-+--------------+------------+----------------+-------------+
-
-How to restrict access to a controller?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If you require that only authenticated users can access you controller, just
-add the ``AuthRequired`` decorator to your controller class.
-
-Example::
-
- import cherrypy
- from ..tools import ApiController, AuthRequired, RESTController
-
-
- @ApiController('ping2')
- @AuthRequired()
- class Ping2(RESTController):
- def list(self):
- return {"msg": "Hello"}
-
-Now only authenticated users will be able to "ping" your controller.
-
-
-How to access the manager module instance from a controller?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-We provide the manager module instance as a global variable that can be
-imported in any module. We also provide a logger instance in the same way.
-
-Example::
-
- import cherrypy
- from .. import logger, mgr
- from ..tools import ApiController, RESTController
-
-
- @ApiController('servers')
- class Servers(RESTController):
- def list(self):
- logger.debug('Listing available servers')
- return {'servers': mgr.list_servers()}
-
-
-How to write a unit test for a controller?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-We provide a test helper class called ``ControllerTestCase`` to easily create
-unit tests for your controller.
-
-If we want to write a unit test for the above ``Ping2`` controller, create a
-``test_ping2.py`` file under the ``tests`` directory with the following code::
-
- from .helper import ControllerTestCase
- from .controllers.ping2 import Ping2
-
-
- class Ping2Test(ControllerTestCase):
- @classmethod
- def setup_test(cls):
- Ping2._cp_config['tools.authentica.on'] = False
-
- def test_ping2(self):
- self._get("/api/ping2")
- self.assertStatus(200)
- self.assertJsonBody({'msg': 'Hello'})
-
-The ``ControllerTestCase`` class will call the dashboard module code that loads
-the controllers and initializes the CherryPy webserver. Then it will call the
-``setup_test()`` class method to execute additional instructions that each test
-case needs to add to the test.
-In the example above we use the ``setup_test()`` method to disable the
-authentication handler for the ``Ping2`` controller.
-
-
-How to listen for manager notifications in a controller?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The manager notifies the modules of several types of cluster events, such
-as cluster logging event, etc...
-
-Each module has a "global" handler function called ``notify`` that the manager
-calls to notify the module. But this handler function must not block or spend
-too much time processing the event notification.
-For this reason we provide a notification queue that controllers can register
-themselves with to receive cluster notifications.
-
-The example below represents a controller that implements a very simple live
-log viewer page::
-
- from __future__ import absolute_import
-
- import collections
-
- import cherrypy
-
- from ..tools import ApiController, BaseController, NotificationQueue
-
-
- @ApiController('livelog')
- class LiveLog(BaseController):
- log_buffer = collections.deque(maxlen=1000)
-
- def __init__(self):
- super(LiveLog, self).__init__()
- NotificationQueue.register(self.log, 'clog')
-
- def log(self, log_struct):
- self.log_buffer.appendleft(log_struct)
-
- @cherrypy.expose
- def default(self):
- ret = '<html><meta http-equiv="refresh" content="2" /><body>'
- for l in self.log_buffer:
- ret += "{}<br>".format(l)
- ret += "</body></html>"
- return ret
-
-As you can see above, the ``NotificationQueue`` class provides a register
-method that receives the function as its first argument, and receives the
-"notification type" as the second argument.
-You can omit the second argument of the ``register`` method, and in that case
-you are registering to listen all notifications of any type.
-
-Here is an list of notification types (these might change in the future) that
-can be used:
-
-* ``clog``: cluster log notifications
-* ``command``: notification when a command issued by ``MgrModule.send_command``
- completes
-* ``perf_schema_update``: perf counters schema update
-* ``mon_map``: monitor map update
-* ``fs_map``: cephfs map update
-* ``osd_map``: OSD map update
-* ``service_map``: services (RGW, RBD-Mirror, etc.) map update
-* ``mon_status``: monitor status regular update
-* ``health``: health status regular update
-* ``pg_summary``: regular update of PG status information
-
-
-How to write a unit test when a controller accesses a Ceph module?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Consider the following example that implements a controller that retrieves the
-list of RBD images of the ``rbd`` pool::
-
- import rbd
- from .. import mgr
- from ..tools import ApiController, RESTController
-
-
- @ApiController('rbdimages')
- class RbdImages(RESTController):
- def __init__(self):
- self.ioctx = mgr.rados.open_ioctx('rbd')
- self.rbd = rbd.RBD()
-
- def list(self):
- return [{'name': n} for n in self.rbd.list(self.ioctx)]
-
-In the example above, we want to mock the return value of the ``rbd.list``
-function, so that we can test the JSON response of the controller.
-
-The unit test code will look like the following::
-
- import mock
- from .helper import ControllerTestCase
-
-
- class RbdImagesTest(ControllerTestCase):
- @mock.patch('rbd.RBD.list')
- def test_list(self, rbd_list_mock):
- rbd_list_mock.return_value = ['img1', 'img2']
- self._get('/api/rbdimages')
- self.assertJsonBody([{'name': 'img1'}, {'name': 'img2'}])
-
-
-
-How to add a new configuration setting?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If you need to store some configuration setting for a new feature, we already
-provide an easy mechanism for you to specify/use the new config setting.
-
-For instance, if you want to add a new configuration setting to hold the
-email address of the dashboard admin, just add a setting name as a class
-attribute to the ``Options`` class in the ``settings.py`` file::
-
- # ...
- class Options(object):
- # ...
-
- ADMIN_EMAIL_ADDRESS = ('admin@admin.com', str)
-
-The value of the class attribute is a pair composed by the default value for that
-setting, and the python type of the value.
-
-By declaring the ``ADMIN_EMAIL_ADDRESS`` class attribute, when you restart the
-dashboard plugin, you will atomatically gain two additional CLI commands to
-get and set that setting::
-
- $ ceph dashboard get-admin-email-address
- $ ceph dashboard set-admin-email-address <value>
-
-To access, or modify the config setting value from your Python code, either
-inside a controller or anywhere else, you just need to import the ``Settings``
-class and access it like this::
-
- from settings import Settings
-
- # ...
- tmp_var = Settings.ADMIN_EMAIL_ADDRESS
-
- # ....
- Settings.ADMIN_EMAIL_ADDRESS = 'myemail@admin.com'
-
-The settings management implementation will make sure that if you change a
-setting value from the Python code you will see that change when accessing
-that setting from the CLI and vice-versa.
-
+++ /dev/null
-Dashboard and Administration Module for Ceph Manager (aka "Dashboard v2")
-=========================================================================
-
-Overview
---------
-
-The original Ceph Manager Dashboard that was shipped with Ceph "Luminous"
-started out as a simple read-only view into various run-time information and
-performance data of a Ceph cluster.
-
-However, there is a `growing demand <http://pad.ceph.com/p/mimic-dashboard>`_
-for adding more web-based management capabilities, to make it easier for
-administrators that prefer a WebUI over the command line.
-
-This module is an ongoing project to add a native web based monitoring and
-administration application to Ceph Manager. It aims at becoming a successor of
-the existing dashboard, which provides read-only functionality and uses a
-simpler architecture to achieve the original goal.
-
-The code and architecture of this module is derived from and inspired by the
-`openATTIC Ceph management and monitoring tool <https://openattic.org/>`_ (both
-the backend and WebUI). The development is actively driven by the team behind
-openATTIC.
-
-The intention is to reuse as much of the existing openATTIC code as possible,
-while adapting it to the different environment. The current openATTIC backend
-implementation is based on Django and the Django REST framework, the Manager
-module's backend code will use the CherryPy framework and a custom REST API
-implementation instead.
-
-The WebUI implementation will be developed using Angular/TypeScript, merging
-both functionality from the existing dashboard as well as adding new
-functionality originally developed for the standalone version of openATTIC.
-
-The porting and migration of the existing openATTIC and dashboard functionality
-will be done in stages. The tasks are currently tracked in the `openATTIC team's
-JIRA instance <https://tracker.openattic.org/browse/OP-3039>`_.
-
-Enabling and Starting the Dashboard
------------------------------------
-
-If you have installed Ceph from distribution packages, the package management
-system should have taken care of installing all the required dependencies.
-
-If you want to start the dashboard from within a development environment, you
-need to have built Ceph (see the toplevel ``README.md`` file and the `developer
-documentation <http://docs.ceph.com/docs/master/dev/>`_ for details on how to
-accomplish this.
-
-Finally, you need to build the dashboard frontend code. See the file
-``HACKING.rst`` in this directory for instructions on setting up the necessary
-development environment.
-
-From within a running Ceph cluster, you can start the Dashboard module by
-running the following command::
-
- $ ceph mgr module enable dashboard_v2
-
-You can see currently enabled Manager modules with::
-
- $ ceph mgr module ls
-
-In order to be able to log in, you need to define a username and password, which
-will be stored in the MON's configuration database::
-
- $ ceph dashboard set-login-credentials <username> <password>
-
-The password will be stored as a hash using ``bcrypt``.
-
-The Dashboard's WebUI should then be reachable on TCP port 8080.
-
-Working on the Dashboard Code
------------------------------
-
-If you're interested in helping with the development of the dashboard, please
-see the file ``HACKING.rst`` for details on how to set up a development
-environment and some other development-related topics.
+++ /dev/null
-# -*- coding: utf-8 -*-
-# pylint: disable=wrong-import-position,global-statement,protected-access
-"""
-openATTIC module
-"""
-from __future__ import absolute_import
-
-import os
-
-
-if 'UNITTEST' not in os.environ:
- class _LoggerProxy(object):
- def __init__(self):
- self._logger = None
-
- def __getattr__(self, item):
- if self._logger is None:
- raise AttributeError("logger not initialized")
- return getattr(self._logger, item)
-
- class _ModuleProxy(object):
- def __init__(self):
- self._mgr = None
-
- def init(self, module_inst):
- global logger
- self._mgr = module_inst
- logger._logger = self._mgr._logger
-
- def __getattr__(self, item):
- if self._mgr is None:
- raise AttributeError("global manager module instance not initialized")
- return getattr(self._mgr, item)
-
- mgr = _ModuleProxy()
- logger = _LoggerProxy()
-
- from .module import Module, StandbyModule
-else:
- import logging
- logging.basicConfig(level=logging.DEBUG)
- logger = logging.getLogger(__name__)
- logging.root.handlers[0].setLevel(logging.DEBUG)
- os.environ['PATH'] = '{}:{}'.format(os.path.abspath('../../../../build/bin'),
- os.environ['PATH'])
-
- # Mock ceph module otherwise every module that is involved in a testcase and imports it will
- # raise an ImportError
- import sys
- import mock
- sys.modules['ceph_module'] = mock.Mock()
-
- mgr = mock.Mock()
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import time
-import sys
-
-import bcrypt
-import cherrypy
-
-from ..tools import ApiController, RESTController, Session
-from .. import logger, mgr
-
-
-@ApiController('auth')
-class Auth(RESTController):
- """
- Provide login and logout actions.
-
- Supported config-keys:
-
- | KEY | DEFAULT | DESCR |
- ------------------------------------------------------------------------|
- | username | None | Username |
- | password | None | Password encrypted using bcrypt |
- | session-expire | 1200 | Session will expire after <expires> |
- | | seconds without activity |
- """
-
- @RESTController.args_from_json
- def create(self, username, password, stay_signed_in=False):
- now = time.time()
- config_username = mgr.get_config('username', None)
- config_password = mgr.get_config('password', None)
- hash_password = Auth.password_hash(password,
- config_password)
- if username == config_username and hash_password == config_password:
- cherrypy.session.regenerate()
- cherrypy.session[Session.USERNAME] = username
- cherrypy.session[Session.TS] = now
- cherrypy.session[Session.EXPIRE_AT_BROWSER_CLOSE] = not stay_signed_in
- logger.debug('Login successful')
- return {'username': username}
-
- cherrypy.response.status = 403
- if config_username is None:
- logger.warning('No Credentials configured. Need to call `ceph dashboard '
- 'set-login-credentials <username> <password>` first.')
- else:
- logger.debug('Login failed')
- return {'detail': 'Invalid credentials'}
-
- def bulk_delete(self):
- logger.debug('Logout successful')
- cherrypy.session[Session.USERNAME] = None
- cherrypy.session[Session.TS] = None
-
- @staticmethod
- def password_hash(password, salt_password=None):
- if not salt_password:
- salt_password = bcrypt.gensalt()
- if sys.version_info > (3, 0):
- return bcrypt.hashpw(password, salt_password)
- return bcrypt.hashpw(password.encode('utf8'), salt_password)
-
- @staticmethod
- def check_auth():
- username = cherrypy.session.get(Session.USERNAME)
- if not username:
- logger.debug('Unauthorized access to %s',
- cherrypy.url(relative='server'))
- raise cherrypy.HTTPError(401, 'You are not authorized to access '
- 'that resource')
- now = time.time()
- expires = float(mgr.get_config(
- 'session-expire', Session.DEFAULT_EXPIRE))
- if expires > 0:
- username_ts = cherrypy.session.get(Session.TS, None)
- if username_ts and float(username_ts) < (now - expires):
- cherrypy.session[Session.USERNAME] = None
- cherrypy.session[Session.TS] = None
- logger.debug('Session expired')
- raise cherrypy.HTTPError(401,
- 'Session expired. You are not '
- 'authorized to access that resource')
- cherrypy.session[Session.TS] = now
-
- @staticmethod
- def set_login_credentials(username, password):
- mgr.set_config('username', username)
- hashed_passwd = Auth.password_hash(password)
- mgr.set_config('password', hashed_passwd)
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from collections import defaultdict
-import json
-
-import cherrypy
-from mgr_module import CommandResult
-
-from .. import mgr
-from ..tools import ApiController, AuthRequired, BaseController, ViewCache
-
-
-@ApiController('cephfs')
-@AuthRequired()
-class CephFS(BaseController):
- def __init__(self):
- super(CephFS, self).__init__()
-
- # Stateful instances of CephFSClients, hold cached results. Key to
- # dict is FSCID
- self.cephfs_clients = {}
-
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def clients(self, fs_id):
- fs_id = self.fs_id_to_int(fs_id)
-
- return self._clients(fs_id)
-
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def data(self, fs_id):
- fs_id = self.fs_id_to_int(fs_id)
-
- return self.fs_status(fs_id)
-
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def mds_counters(self, fs_id):
- """
- Result format: map of daemon name to map of counter to list of datapoints
- rtype: dict[str, dict[str, list]]
- """
-
- # Opinionated list of interesting performance counters for the GUI --
- # if you need something else just add it. See how simple life is
- # when you don't have to write general purpose APIs?
- counters = [
- "mds_server.handle_client_request",
- "mds_log.ev",
- "mds_cache.num_strays",
- "mds.exported",
- "mds.exported_inodes",
- "mds.imported",
- "mds.imported_inodes",
- "mds.inodes",
- "mds.caps",
- "mds.subtrees"
- ]
-
- fs_id = self.fs_id_to_int(fs_id)
-
- result = {}
- mds_names = self._get_mds_names(fs_id)
-
- for mds_name in mds_names:
- result[mds_name] = {}
- for counter in counters:
- data = mgr.get_counter("mds", mds_name, counter)
- if data is not None:
- result[mds_name][counter] = data[counter]
- else:
- result[mds_name][counter] = []
-
- return dict(result)
-
- @staticmethod
- def fs_id_to_int(fs_id):
- try:
- return int(fs_id)
- except ValueError:
- raise cherrypy.HTTPError(400, "Invalid cephfs id {}".format(fs_id))
-
- def _get_mds_names(self, filesystem_id=None):
- names = []
-
- fsmap = mgr.get("fs_map")
- for fs in fsmap['filesystems']:
- if filesystem_id is not None and fs['id'] != filesystem_id:
- continue
- names.extend([info['name']
- for _, info in fs['mdsmap']['info'].items()])
-
- if filesystem_id is None:
- names.extend(info['name'] for info in fsmap['standbys'])
-
- return names
-
- def get_rate(self, daemon_type, daemon_name, stat):
- data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
-
- if data and len(data) > 1:
- return (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
-
- return 0
-
- # pylint: disable=too-many-locals,too-many-statements,too-many-branches
- def fs_status(self, fs_id):
- mds_versions = defaultdict(list)
-
- fsmap = mgr.get("fs_map")
- filesystem = None
- for fs in fsmap['filesystems']:
- if fs['id'] == fs_id:
- filesystem = fs
- break
-
- if filesystem is None:
- raise cherrypy.HTTPError(404,
- "CephFS id {0} not found".format(fs_id))
-
- rank_table = []
-
- mdsmap = filesystem['mdsmap']
-
- client_count = 0
-
- for rank in mdsmap["in"]:
- up = "mds_{0}".format(rank) in mdsmap["up"]
- if up:
- gid = mdsmap['up']["mds_{0}".format(rank)]
- info = mdsmap['info']['gid_{0}'.format(gid)]
- dns = self.get_latest("mds", info['name'], "mds.inodes")
- inos = self.get_latest("mds", info['name'], "mds_mem.ino")
-
- if rank == 0:
- client_count = self.get_latest("mds", info['name'],
- "mds_sessions.session_count")
- elif client_count == 0:
- # In case rank 0 was down, look at another rank's
- # sessionmap to get an indication of clients.
- client_count = self.get_latest("mds", info['name'],
- "mds_sessions.session_count")
-
- laggy = "laggy_since" in info
-
- state = info['state'].split(":")[1]
- if laggy:
- state += "(laggy)"
-
- # if state == "active" and not laggy:
- # c_state = self.colorize(state, self.GREEN)
- # else:
- # c_state = self.colorize(state, self.YELLOW)
-
- # Populate based on context of state, e.g. client
- # ops for an active daemon, replay progress, reconnect
- # progress
- activity = ""
-
- if state == "active":
- activity = self.get_rate("mds",
- info['name'],
- "mds_server.handle_client_request")
-
- metadata = mgr.get_metadata('mds', info['name'])
- mds_versions[metadata.get('ceph_version', 'unknown')].append(
- info['name'])
- rank_table.append(
- {
- "rank": rank,
- "state": state,
- "mds": info['name'],
- "activity": activity,
- "dns": dns,
- "inos": inos
- }
- )
-
- else:
- rank_table.append(
- {
- "rank": rank,
- "state": "failed",
- "mds": "",
- "activity": "",
- "dns": 0,
- "inos": 0
- }
- )
-
- # Find the standby replays
- # pylint: disable=unused-variable
- for gid_str, daemon_info in mdsmap['info'].iteritems():
- if daemon_info['state'] != "up:standby-replay":
- continue
-
- inos = self.get_latest("mds", daemon_info['name'], "mds_mem.ino")
- dns = self.get_latest("mds", daemon_info['name'], "mds.inodes")
-
- activity = self.get_rate(
- "mds", daemon_info['name'], "mds_log.replay")
-
- rank_table.append(
- {
- "rank": "{0}-s".format(daemon_info['rank']),
- "state": "standby-replay",
- "mds": daemon_info['name'],
- "activity": activity,
- "dns": dns,
- "inos": inos
- }
- )
-
- df = mgr.get("df")
- pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
- osdmap = mgr.get("osd_map")
- pools = dict([(p['pool'], p) for p in osdmap['pools']])
- metadata_pool_id = mdsmap['metadata_pool']
- data_pool_ids = mdsmap['data_pools']
-
- pools_table = []
- for pool_id in [metadata_pool_id] + data_pool_ids:
- pool_type = "metadata" if pool_id == metadata_pool_id else "data"
- stats = pool_stats[pool_id]
- pools_table.append({
- "pool": pools[pool_id]['pool_name'],
- "type": pool_type,
- "used": stats['bytes_used'],
- "avail": stats['max_avail']
- })
-
- standby_table = []
- for standby in fsmap['standbys']:
- metadata = mgr.get_metadata('mds', standby['name'])
- mds_versions[metadata.get('ceph_version', 'unknown')].append(
- standby['name'])
-
- standby_table.append({
- 'name': standby['name']
- })
-
- return {
- "cephfs": {
- "id": fs_id,
- "name": mdsmap['fs_name'],
- "client_count": client_count,
- "ranks": rank_table,
- "pools": pools_table
- },
- "standbys": standby_table,
- "versions": mds_versions
- }
-
- def _clients(self, fs_id):
- cephfs_clients = self.cephfs_clients.get(fs_id, None)
- if cephfs_clients is None:
- cephfs_clients = CephFSClients(mgr, fs_id)
- self.cephfs_clients[fs_id] = cephfs_clients
-
- try:
- status, clients = cephfs_clients.get()
- except AttributeError:
- raise cherrypy.HTTPError(404,
- "No cephfs with id {0}".format(fs_id))
- if clients is None:
- raise cherrypy.HTTPError(404,
- "No cephfs with id {0}".format(fs_id))
-
- # Decorate the metadata with some fields that will be
- # indepdendent of whether it's a kernel or userspace
- # client, so that the javascript doesn't have to grok that.
- for client in clients:
- if "ceph_version" in client['client_metadata']:
- client['type'] = "userspace"
- client['version'] = client['client_metadata']['ceph_version']
- client['hostname'] = client['client_metadata']['hostname']
- elif "kernel_version" in client['client_metadata']:
- client['type'] = "kernel"
- client['version'] = client['client_metadata']['kernel_version']
- client['hostname'] = client['client_metadata']['hostname']
- else:
- client['type'] = "unknown"
- client['version'] = ""
- client['hostname'] = ""
-
- return {
- 'status': status,
- 'data': clients
- }
-
- def get_latest(self, daemon_type, daemon_name, stat):
- data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
- if data:
- return data[-1][1]
- return 0
-
-
-class CephFSClients(object):
- def __init__(self, module_inst, fscid):
- self._module = module_inst
- self.fscid = fscid
-
- # pylint: disable=unused-variable
- @ViewCache()
- def get(self):
- mds_spec = "{0}:0".format(self.fscid)
- result = CommandResult("")
- self._module.send_command(result, "mds", mds_spec,
- json.dumps({
- "prefix": "session ls",
- }),
- "")
- r, outb, outs = result.wait()
- # TODO handle nonzero returns, e.g. when rank isn't active
- assert r == 0
- return json.loads(outb)
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import cherrypy
-
-from .. import mgr
-from ..tools import ApiController, RESTController, AuthRequired
-
-
-@ApiController('cluster_conf')
-@AuthRequired()
-class ClusterConfiguration(RESTController):
- def list(self):
- options = mgr.get("config_options")['options']
- return options
-
- def get(self, name):
- for option in mgr.get('config_options')['options']:
- if option['name'] == name:
- return option
-
- raise cherrypy.HTTPError(404)
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import collections
-import json
-
-import cherrypy
-from mgr_module import CommandResult
-
-from .. import mgr
-from ..services.ceph_service import CephService
-from ..tools import ApiController, AuthRequired, BaseController, NotificationQueue
-
-
-LOG_BUFFER_SIZE = 30
-
-
-@ApiController('dashboard')
-@AuthRequired()
-class Dashboard(BaseController):
- def __init__(self):
- super(Dashboard, self).__init__()
-
- self._log_initialized = False
-
- self.log_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE)
- self.audit_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE)
-
- def append_log(self, log_struct):
- if log_struct['channel'] == "audit":
- self.audit_buffer.appendleft(log_struct)
- else:
- self.log_buffer.appendleft(log_struct)
-
- def load_buffer(self, buf, channel_name):
- result = CommandResult("")
- mgr.send_command(result, "mon", "", json.dumps({
- "prefix": "log last",
- "format": "json",
- "channel": channel_name,
- "num": LOG_BUFFER_SIZE
- }), "")
- r, outb, outs = result.wait()
- if r != 0:
- # Oh well. We won't let this stop us though.
- self.log.error("Error fetching log history (r={0}, \"{1}\")".format(
- r, outs))
- else:
- try:
- lines = json.loads(outb)
- except ValueError:
- self.log.error("Error decoding log history")
- else:
- for l in lines:
- buf.appendleft(l)
-
- # pylint: disable=R0914
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def health(self):
- if not self._log_initialized:
- self._log_initialized = True
-
- self.load_buffer(self.log_buffer, "cluster")
- self.load_buffer(self.audit_buffer, "audit")
-
- NotificationQueue.register(self.append_log, 'clog')
-
- # Fuse osdmap with pg_summary to get description of pools
- # including their PG states
-
- osd_map = self.osd_map()
-
- pools = CephService.get_pool_list_with_stats()
-
- # Not needed, skip the effort of transmitting this
- # to UI
- del osd_map['pg_temp']
-
- df = mgr.get("df")
- df['stats']['total_objects'] = sum(
- [p['stats']['objects'] for p in df['pools']])
-
- return {
- "health": self.health_data(),
- "mon_status": self.mon_status(),
- "fs_map": mgr.get('fs_map'),
- "osd_map": osd_map,
- "clog": list(self.log_buffer),
- "audit_log": list(self.audit_buffer),
- "pools": pools,
- "mgr_map": mgr.get("mgr_map"),
- "df": df
- }
-
- def mon_status(self):
- mon_status_data = mgr.get("mon_status")
- return json.loads(mon_status_data['json'])
-
- def osd_map(self):
- osd_map = mgr.get("osd_map")
-
- assert osd_map is not None
-
- osd_map['tree'] = mgr.get("osd_map_tree")
- osd_map['crush'] = mgr.get("osd_map_crush")
- osd_map['crush_map_text'] = mgr.get("osd_map_crush_map_text")
- osd_map['osd_metadata'] = mgr.get("osd_metadata")
-
- return osd_map
-
- def health_data(self):
- health_data = mgr.get("health")
- health = json.loads(health_data['json'])
-
- # Transform the `checks` dict into a list for the convenience
- # of rendering from javascript.
- checks = []
- for k, v in health['checks'].items():
- v['type'] = k
- checks.append(v)
-
- checks = sorted(checks, key=lambda c: c['severity'])
-
- health['checks'] = checks
-
- return health
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .. import mgr
-from ..tools import ApiController, AuthRequired, RESTController
-
-
-@ApiController('host')
-@AuthRequired()
-class Host(RESTController):
- def list(self):
- return mgr.list_servers()
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import json
-
-import cherrypy
-
-from .. import mgr
-from ..tools import ApiController, AuthRequired, BaseController
-
-
-@ApiController('monitor')
-@AuthRequired()
-class Monitor(BaseController):
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def default(self):
- in_quorum, out_quorum = [], []
-
- counters = ['mon.num_sessions']
-
- mon_status_json = mgr.get("mon_status")
- mon_status = json.loads(mon_status_json['json'])
-
- for mon in mon_status["monmap"]["mons"]:
- mon["stats"] = {}
- for counter in counters:
- data = mgr.get_counter("mon", mon["name"], counter)
- if data is not None:
- mon["stats"][counter.split(".")[1]] = data[counter]
- else:
- mon["stats"][counter.split(".")[1]] = []
- if mon["rank"] in mon_status["quorum"]:
- in_quorum.append(mon)
- else:
- out_quorum.append(mon)
-
- return {
- 'mon_status': mon_status,
- 'in_quorum': in_quorum,
- 'out_quorum': out_quorum
- }
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import json
-
-from mgr_module import CommandResult
-
-from .. import logger, mgr
-from ..tools import ApiController, AuthRequired, RESTController
-
-
-@ApiController('osd')
-@AuthRequired()
-class Osd(RESTController):
- def get_counter(self, daemon_name, stat):
- return mgr.get_counter('osd', daemon_name, stat)[stat]
-
- def get_rate(self, daemon_name, stat):
- data = self.get_counter(daemon_name, stat)
- rate = 0
- if data and len(data) > 1:
- rate = (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
- return rate
-
- def get_latest(self, daemon_name, stat):
- data = self.get_counter(daemon_name, stat)
- latest = 0
- if data and data[-1] and len(data[-1]) == 2:
- latest = data[-1][1]
- return latest
-
- def list(self):
- osds = self.get_osd_map()
- # Extending by osd stats information
- for s in mgr.get('osd_stats')['osd_stats']:
- osds[str(s['osd'])].update({'osd_stats': s})
- # Extending by osd node information
- nodes = mgr.get('osd_map_tree')['nodes']
- osd_tree = [(str(o['id']), o) for o in nodes if o['id'] >= 0]
- for o in osd_tree:
- osds[o[0]].update({'tree': o[1]})
- # Extending by osd parent node information
- hosts = [(h['name'], h) for h in nodes if h['id'] < 0]
- for h in hosts:
- for o_id in h[1]['children']:
- if o_id >= 0:
- osds[str(o_id)]['host'] = h[1]
- # Extending by osd histogram data
- for o_id in osds:
- o = osds[o_id]
- o['stats'] = {}
- o['stats_history'] = {}
- osd_spec = str(o['osd'])
- for s in ['osd.op_w', 'osd.op_in_bytes', 'osd.op_r', 'osd.op_out_bytes']:
- prop = s.split('.')[1]
- o['stats'][prop] = self.get_rate(osd_spec, s)
- o['stats_history'][prop] = self.get_counter(osd_spec, s)
- # Gauge stats
- for s in ['osd.numpg', 'osd.stat_bytes', 'osd.stat_bytes_used']:
- o['stats'][s.split('.')[1]] = self.get_latest(osd_spec, s)
- return osds.values()
-
- def get_osd_map(self):
- osds = {}
- for osd in mgr.get('osd_map')['osds']:
- osd['id'] = osd['osd']
- osds[str(osd['id'])] = osd
- return osds
-
- def get(self, svc_id):
- result = CommandResult('')
- mgr.send_command(result, 'osd', svc_id,
- json.dumps({
- 'prefix': 'perf histogram dump',
- }),
- '')
- r, outb, outs = result.wait()
- if r != 0:
- histogram = None
- logger.warning('Failed to load histogram for OSD %s', svc_id)
- logger.debug(outs)
- histogram = outs
- else:
- histogram = json.loads(outb)
- return {
- 'osd_map': self.get_osd_map()[svc_id],
- 'osd_metadata': mgr.get_metadata('osd', svc_id),
- 'histogram': histogram,
- }
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .. import mgr
-from ..tools import ApiController, AuthRequired, RESTController
-
-
-class PerfCounter(RESTController):
- def __init__(self, service_type):
- self._service_type = service_type
-
- def _get_rate(self, daemon_type, daemon_name, stat):
- data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
- if data and len(data) > 1:
- return (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
- return 0
-
- def _get_latest(self, daemon_type, daemon_name, stat):
- data = mgr.get_counter(daemon_type, daemon_name, stat)[stat]
- if data:
- return data[-1][1]
- return 0
-
- def get(self, service_id):
- schema = mgr.get_perf_schema(
- self._service_type, str(service_id)).values()[0]
- counters = []
-
- for key, value in sorted(schema.items()):
- counter = dict()
- counter['name'] = str(key)
- counter['description'] = value['description']
- # pylint: disable=W0212
- if mgr._stattype_to_str(value['type']) == 'counter':
- counter['value'] = self._get_rate(
- self._service_type, service_id, key)
- counter['unit'] = mgr._unit_to_str(value['units'])
- else:
- counter['value'] = self._get_latest(
- self._service_type, service_id, key)
- counter['unit'] = ''
- counters.append(counter)
-
- return {
- 'service': {
- 'type': self._service_type,
- 'id': service_id
- },
- 'counters': counters
- }
-
-
-@ApiController('perf_counters')
-@AuthRequired()
-class PerfCounters(RESTController):
- def __init__(self):
- self.mds = PerfCounter('mds')
- self.mon = PerfCounter('mon')
- self.osd = PerfCounter('osd')
- self.rgw = PerfCounter('rgw')
- self.rbd_mirror = PerfCounter('rbd-mirror')
- self.mgr = PerfCounter('mgr')
-
- def list(self):
- counters = mgr.get_all_perf_counters()
- return counters
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from ..services.ceph_service import CephService
-from ..tools import ApiController, RESTController, AuthRequired
-
-
-@ApiController('pool')
-@AuthRequired()
-class Pool(RESTController):
-
- @classmethod
- def _serialize_pool(cls, pool, attrs):
- if not attrs or not isinstance(attrs, list):
- return pool
-
- res = {}
- for attr in attrs:
- if attr not in pool:
- continue
- if attr == 'type':
- res[attr] = {1: 'replicated', 3: 'erasure'}[pool[attr]]
- else:
- res[attr] = pool[attr]
-
- # pool_name is mandatory
- res['pool_name'] = pool['pool_name']
- return res
-
- @staticmethod
- def _str_to_bool(var):
- if isinstance(var, bool):
- return var
- return var.lower() in ("true", "yes", "1", 1)
-
- def list(self, attrs=None, stats=False):
- if attrs:
- attrs = attrs.split(',')
-
- if self._str_to_bool(stats):
- pools = CephService.get_pool_list_with_stats()
- else:
- pools = CephService.get_pool_list()
-
- return [self._serialize_pool(pool, attrs) for pool in pools]
-
- def get(self, pool_name, attrs=None, stats=False):
- pools = self.list(attrs, stats)
- return [pool for pool in pools if pool['pool_name'] == pool_name][0]
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import math
-import cherrypy
-import rbd
-
-from .. import mgr
-from ..tools import ApiController, AuthRequired, RESTController, ViewCache
-
-
-@ApiController('rbd')
-@AuthRequired()
-class Rbd(RESTController):
-
- RBD_FEATURES_NAME_MAPPING = {
- rbd.RBD_FEATURE_LAYERING: "layering",
- rbd.RBD_FEATURE_STRIPINGV2: "striping",
- rbd.RBD_FEATURE_EXCLUSIVE_LOCK: "exclusive-lock",
- rbd.RBD_FEATURE_OBJECT_MAP: "object-map",
- rbd.RBD_FEATURE_FAST_DIFF: "fast-diff",
- rbd.RBD_FEATURE_DEEP_FLATTEN: "deep-flatten",
- rbd.RBD_FEATURE_JOURNALING: "journaling",
- rbd.RBD_FEATURE_DATA_POOL: "data-pool",
- rbd.RBD_FEATURE_OPERATIONS: "operations",
- }
-
- def __init__(self):
- self.rbd = None
-
- @staticmethod
- def _format_bitmask(features):
- """
- Formats the bitmask:
-
- >>> Rbd._format_bitmask(45)
- 'deep-flatten, exclusive-lock, layering, object-map'
- """
- names = [val for key, val in Rbd.RBD_FEATURES_NAME_MAPPING.items()
- if key & features == key]
- return ', '.join(sorted(names))
-
- @staticmethod
- def _format_features(features):
- """
- Converts the features list to bitmask:
-
- >>> Rbd._format_features(['deep-flatten', 'exclusive-lock', 'layering', 'object-map'])
- 45
-
- >>> Rbd._format_features(None) is None
- True
-
- >>> Rbd._format_features('not a list') is None
- True
- """
- if not features or not isinstance(features, list):
- return None
-
- res = 0
- for key, value in Rbd.RBD_FEATURES_NAME_MAPPING.items():
- if value in features:
- res = key | res
- return res
-
- @ViewCache()
- def _rbd_list(self, pool_name):
- ioctx = mgr.rados.open_ioctx(pool_name)
- self.rbd = rbd.RBD()
- names = self.rbd.list(ioctx)
- result = []
- for name in names:
- i = rbd.Image(ioctx, name)
- stat = i.stat()
- stat['name'] = name
- features = i.features()
- stat['features'] = features
- stat['features_name'] = self._format_bitmask(features)
-
- try:
- parent_info = i.parent_info()
- parent = "{}@{}".format(parent_info[0], parent_info[1])
- if parent_info[0] != pool_name:
- parent = "{}/{}".format(parent_info[0], parent)
- stat['parent'] = parent
- except rbd.ImageNotFound:
- pass
- result.append(stat)
- return result
-
- def get(self, pool_name):
- # pylint: disable=unbalanced-tuple-unpacking
- status, value = self._rbd_list(pool_name)
- if status == ViewCache.VALUE_EXCEPTION:
- raise value
- return {'status': status, 'value': value}
-
- def create(self, data):
- if not self.rbd:
- self.rbd = rbd.RBD()
-
- # Get input values
- name = data.get('name')
- pool_name = data.get('pool_name')
- size = data.get('size')
- obj_size = data.get('obj_size')
- features = data.get('features')
- stripe_unit = data.get('stripe_unit')
- stripe_count = data.get('stripe_count')
- data_pool = data.get('data_pool')
-
- # Set order
- order = None
- if obj_size and obj_size > 0:
- order = int(round(math.log(float(obj_size), 2)))
-
- # Set features
- feature_bitmask = self._format_features(features)
-
- ioctx = mgr.rados.open_ioctx(pool_name)
-
- try:
- self.rbd.create(ioctx, name, size, order=order, old_format=False,
- features=feature_bitmask, stripe_unit=stripe_unit,
- stripe_count=stripe_count, data_pool=data_pool)
- except rbd.OSError as e:
- cherrypy.response.status = 400
- return {'success': False, 'detail': str(e), 'errno': e.errno}
- return {'success': True}
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import json
-import re
-
-from functools import partial
-
-import cherrypy
-import rbd
-
-from .. import logger, mgr
-from ..services.ceph_service import CephService
-from ..tools import ApiController, AuthRequired, BaseController, ViewCache
-
-
-@ViewCache()
-def get_daemons_and_pools(): # pylint: disable=R0915
- def get_daemons():
- daemons = []
- for hostname, server in CephService.get_service_map('rbd-mirror').items():
- for service in server['services']:
- id = service['id'] # pylint: disable=W0622
- metadata = service['metadata']
- status = service['status']
-
- try:
- status = json.loads(status['json'])
- except (ValueError, KeyError) as _:
- status = {}
-
- instance_id = metadata['instance_id']
- if id == instance_id:
- # new version that supports per-cluster leader elections
- id = metadata['id']
-
- # extract per-daemon service data and health
- daemon = {
- 'id': id,
- 'instance_id': instance_id,
- 'version': metadata['ceph_version'],
- 'server_hostname': hostname,
- 'service': service,
- 'server': server,
- 'metadata': metadata,
- 'status': status
- }
- daemon = dict(daemon, **get_daemon_health(daemon))
- daemons.append(daemon)
-
- return sorted(daemons, key=lambda k: k['instance_id'])
-
- def get_daemon_health(daemon):
- health = {
- 'health_color': 'info',
- 'health': 'Unknown'
- }
- for _, pool_data in daemon['status'].items(): # TODO: simplify
- if (health['health'] != 'error' and
- [k for k, v in pool_data.get('callouts', {}).items()
- if v['level'] == 'error']):
- health = {
- 'health_color': 'error',
- 'health': 'Error'
- }
- elif (health['health'] != 'error' and
- [k for k, v in pool_data.get('callouts', {}).items()
- if v['level'] == 'warning']):
- health = {
- 'health_color': 'warning',
- 'health': 'Warning'
- }
- elif health['health_color'] == 'info':
- health = {
- 'health_color': 'success',
- 'health': 'OK'
- }
- return health
-
- def get_pools(daemons): # pylint: disable=R0912, R0915
- pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')]
- pool_stats = {}
- rbdctx = rbd.RBD()
- for pool_name in pool_names:
- logger.debug("Constructing IOCtx %s", pool_name)
- try:
- ioctx = mgr.rados.open_ioctx(pool_name)
- except TypeError:
- logger.exception("Failed to open pool %s", pool_name)
- continue
-
- try:
- mirror_mode = rbdctx.mirror_mode_get(ioctx)
- except: # noqa pylint: disable=W0702
- logger.exception("Failed to query mirror mode %s", pool_name)
-
- stats = {}
- if mirror_mode == rbd.RBD_MIRROR_MODE_DISABLED:
- continue
- elif mirror_mode == rbd.RBD_MIRROR_MODE_IMAGE:
- mirror_mode = "image"
- elif mirror_mode == rbd.RBD_MIRROR_MODE_POOL:
- mirror_mode = "pool"
- else:
- mirror_mode = "unknown"
- stats['health_color'] = "warning"
- stats['health'] = "Warning"
-
- pool_stats[pool_name] = dict(stats, **{
- 'mirror_mode': mirror_mode
- })
-
- for daemon in daemons:
- for _, pool_data in daemon['status'].items():
- stats = pool_stats.get(pool_data['name'], None)
- if stats is None:
- continue
-
- if pool_data.get('leader', False):
- # leader instance stores image counts
- stats['leader_id'] = daemon['metadata']['instance_id']
- stats['image_local_count'] = pool_data.get('image_local_count', 0)
- stats['image_remote_count'] = pool_data.get('image_remote_count', 0)
-
- if (stats.get('health_color', '') != 'error' and
- pool_data.get('image_error_count', 0) > 0):
- stats['health_color'] = 'error'
- stats['health'] = 'Error'
- elif (stats.get('health_color', '') != 'error' and
- pool_data.get('image_warning_count', 0) > 0):
- stats['health_color'] = 'warning'
- stats['health'] = 'Warning'
- elif stats.get('health', None) is None:
- stats['health_color'] = 'success'
- stats['health'] = 'OK'
-
- for _, stats in pool_stats.items():
- if stats.get('health', None) is None:
- # daemon doesn't know about pool
- stats['health_color'] = 'error'
- stats['health'] = 'Error'
- elif stats.get('leader_id', None) is None:
- # no daemons are managing the pool as leader instance
- stats['health_color'] = 'warning'
- stats['health'] = 'Warning'
- return pool_stats
-
- daemons = get_daemons()
- return {
- 'daemons': daemons,
- 'pools': get_pools(daemons)
- }
-
-
-@ApiController('rbdmirror')
-@AuthRequired()
-class RbdMirror(BaseController):
-
- def __init__(self):
- self.pool_data = {}
-
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def default(self):
- status, content_data = self._get_content_data()
- return {'status': status, 'content_data': content_data}
-
- @ViewCache()
- def _get_pool_datum(self, pool_name):
- data = {}
- logger.debug("Constructing IOCtx %s", pool_name)
- try:
- ioctx = mgr.rados.open_ioctx(pool_name)
- except TypeError:
- logger.exception("Failed to open pool %s", pool_name)
- return None
-
- mirror_state = {
- 'down': {
- 'health': 'issue',
- 'state_color': 'warning',
- 'state': 'Unknown',
- 'description': None
- },
- rbd.MIRROR_IMAGE_STATUS_STATE_UNKNOWN: {
- 'health': 'issue',
- 'state_color': 'warning',
- 'state': 'Unknown'
- },
- rbd.MIRROR_IMAGE_STATUS_STATE_ERROR: {
- 'health': 'issue',
- 'state_color': 'error',
- 'state': 'Error'
- },
- rbd.MIRROR_IMAGE_STATUS_STATE_SYNCING: {
- 'health': 'syncing'
- },
- rbd.MIRROR_IMAGE_STATUS_STATE_STARTING_REPLAY: {
- 'health': 'ok',
- 'state_color': 'success',
- 'state': 'Starting'
- },
- rbd.MIRROR_IMAGE_STATUS_STATE_REPLAYING: {
- 'health': 'ok',
- 'state_color': 'success',
- 'state': 'Replaying'
- },
- rbd.MIRROR_IMAGE_STATUS_STATE_STOPPING_REPLAY: {
- 'health': 'ok',
- 'state_color': 'success',
- 'state': 'Stopping'
- },
- rbd.MIRROR_IMAGE_STATUS_STATE_STOPPED: {
- 'health': 'ok',
- 'state_color': 'info',
- 'state': 'Primary'
- }
- }
-
- rbdctx = rbd.RBD()
- try:
- mirror_image_status = rbdctx.mirror_image_status_list(ioctx)
- data['mirror_images'] = sorted([
- dict({
- 'name': image['name'],
- 'description': image['description']
- }, **mirror_state['down' if not image['up'] else image['state']])
- for image in mirror_image_status
- ], key=lambda k: k['name'])
- except rbd.ImageNotFound:
- pass
- except: # noqa pylint: disable=W0702
- logger.exception("Failed to list mirror image status %s", pool_name)
-
- return data
-
- @ViewCache()
- def _get_content_data(self): # pylint: disable=R0914
-
- def get_pool_datum(pool_name):
- pool_datum = self.pool_data.get(pool_name, None)
- if pool_datum is None:
- pool_datum = partial(self._get_pool_datum, pool_name)
- self.pool_data[pool_name] = pool_datum
-
- _, value = pool_datum()
- return value
-
- pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')]
- _, data = get_daemons_and_pools()
- if isinstance(data, Exception):
- logger.exception("Failed to get rbd-mirror daemons list")
- raise type(data)(str(data))
- daemons = data.get('daemons', [])
- pool_stats = data.get('pools', {})
-
- pools = []
- image_error = []
- image_syncing = []
- image_ready = []
- for pool_name in pool_names:
- pool = get_pool_datum(pool_name) or {}
- stats = pool_stats.get(pool_name, {})
- if stats.get('mirror_mode', None) is None:
- continue
-
- mirror_images = pool.get('mirror_images', [])
- for mirror_image in mirror_images:
- image = {
- 'pool_name': pool_name,
- 'name': mirror_image['name']
- }
-
- if mirror_image['health'] == 'ok':
- image.update({
- 'state_color': mirror_image['state_color'],
- 'state': mirror_image['state'],
- 'description': mirror_image['description']
- })
- image_ready.append(image)
- elif mirror_image['health'] == 'syncing':
- p = re.compile("bootstrapping, IMAGE_COPY/COPY_OBJECT (.*)%")
- image.update({
- 'progress': (p.findall(mirror_image['description']) or [0])[0]
- })
- image_syncing.append(image)
- else:
- image.update({
- 'state_color': mirror_image['state_color'],
- 'state': mirror_image['state'],
- 'description': mirror_image['description']
- })
- image_error.append(image)
-
- pools.append(dict({
- 'name': pool_name
- }, **stats))
-
- return {
- 'daemons': daemons,
- 'pools': pools,
- 'image_error': image_error,
- 'image_syncing': image_syncing,
- 'image_ready': image_ready
- }
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import json
-
-from .. import logger
-from ..services.ceph_service import CephService
-from ..tools import ApiController, RESTController, AuthRequired
-
-
-@ApiController('rgw')
-@AuthRequired()
-class Rgw(RESTController):
- pass
-
-
-@ApiController('rgw/daemon')
-@AuthRequired()
-class RgwDaemon(RESTController):
-
- def list(self):
- daemons = []
- for hostname, server in CephService.get_service_map('rgw').items():
- for service in server['services']:
- metadata = service['metadata']
- status = service['status']
- if 'json' in status:
- try:
- status = json.loads(status['json'])
- except ValueError:
- logger.warning("%s had invalid status json", service['id'])
- status = {}
- else:
- logger.warning('%s has no key "json" in status', service['id'])
-
- # extract per-daemon service data and health
- daemon = {
- 'id': service['id'],
- 'version': metadata['ceph_version'],
- 'server_hostname': hostname
- }
-
- daemons.append(daemon)
-
- return sorted(daemons, key=lambda k: k['id'])
-
- def get(self, svc_id):
- daemon = {
- 'rgw_metadata': [],
- 'rgw_id': svc_id,
- 'rgw_status': []
- }
- service = CephService.get_service('rgw', svc_id)
- if not service:
- return daemon
-
- metadata = service['metadata']
- status = service['status']
- if 'json' in status:
- try:
- status = json.loads(status['json'])
- except ValueError:
- logger.warning("%s had invalid status json", service['id'])
- status = {}
- else:
- logger.warning('%s has no key "json" in status', service['id'])
-
- daemon['rgw_metadata'] = metadata
- daemon['rgw_status'] = status
- return daemon
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import json
-
-import cherrypy
-
-from .. import logger, mgr
-from ..controllers.rbd_mirroring import get_daemons_and_pools
-from ..tools import AuthRequired, ApiController, BaseController
-from ..services.ceph_service import CephService
-
-
-@ApiController('summary')
-@AuthRequired()
-class Summary(BaseController):
- def _rbd_pool_data(self):
- pool_names = [pool['pool_name'] for pool in CephService.get_pool_list('rbd')]
- return sorted(pool_names)
-
- def _health_status(self):
- health_data = mgr.get("health")
- return json.loads(health_data["json"])['status']
-
- def _filesystems(self):
- fsmap = mgr.get("fs_map")
- return [
- {
- "id": f['id'],
- "name": f['mdsmap']['fs_name']
- }
- for f in fsmap['filesystems']
- ]
-
- def _rbd_mirroring(self):
- _, data = get_daemons_and_pools()
-
- if isinstance(data, Exception):
- logger.exception("Failed to get rbd-mirror daemons and pools")
- raise type(data)(str(data))
- else:
- daemons = data.get('daemons', [])
- pools = data.get('pools', {})
-
- warnings = 0
- errors = 0
- for daemon in daemons:
- if daemon['health_color'] == 'error':
- errors += 1
- elif daemon['health_color'] == 'warning':
- warnings += 1
- for _, pool in pools.items():
- if pool['health_color'] == 'error':
- errors += 1
- elif pool['health_color'] == 'warning':
- warnings += 1
- return {'warnings': warnings, 'errors': errors}
-
- @cherrypy.expose
- @cherrypy.tools.json_out()
- def default(self):
- return {
- 'rbd_pools': self._rbd_pool_data(),
- 'health_status': self._health_status(),
- 'filesystems': self._filesystems(),
- 'rbd_mirroring': self._rbd_mirroring(),
- 'mgr_id': mgr.get_mgr_id(),
- 'have_mon_connection': mgr.have_mon_connection()
- }
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-from .. import mgr
-from ..services.ceph_service import CephService
-from ..tools import ApiController, AuthRequired, RESTController
-
-SERVICE_TYPE = 'tcmu-runner'
-
-
-@ApiController('tcmuiscsi')
-@AuthRequired()
-class TcmuIscsi(RESTController):
- # pylint: disable=too-many-locals,too-many-nested-blocks
- def list(self): # pylint: disable=unused-argument
- daemons = {}
- images = {}
- for service in CephService.get_service_list(SERVICE_TYPE):
- metadata = service['metadata']
- status = service['status']
- hostname = service['hostname']
-
- daemon = daemons.get(hostname, None)
- if daemon is None:
- daemon = {
- 'server_hostname': hostname,
- 'version': metadata['ceph_version'],
- 'optimized_paths': 0,
- 'non_optimized_paths': 0
- }
- daemons[hostname] = daemon
-
- service_id = service['id']
- device_id = service_id.split(':')[-1]
- image = images.get(device_id)
- if image is None:
- image = {
- 'device_id': device_id,
- 'pool_name': metadata['pool_name'],
- 'name': metadata['image_name'],
- 'id': metadata.get('image_id', None),
- 'optimized_paths': [],
- 'non_optimized_paths': []
- }
- images[device_id] = image
-
- if status.get('lock_owner', 'false') == 'true':
- daemon['optimized_paths'] += 1
- image['optimized_paths'].append(hostname)
-
- perf_key_prefix = "librbd-{id}-{pool}-{name}.".format(
- id=metadata.get('image_id', ''),
- pool=metadata['pool_name'],
- name=metadata['image_name'])
- perf_key = "{}lock_acquired_time".format(perf_key_prefix)
- lock_acquired_time = (mgr.get_counter(
- 'tcmu-runner', service_id, perf_key)[perf_key] or
- [[0, 0]])[-1][1] / 1000000000
- if lock_acquired_time > image.get('optimized_since', 0):
- image['optimized_since'] = lock_acquired_time
- image['stats'] = {}
- image['stats_history'] = {}
- for s in ['rd', 'wr', 'rd_bytes', 'wr_bytes']:
- perf_key = "{}{}".format(perf_key_prefix, s)
- image['stats'][s] = mgr.get_rate(
- 'tcmu-runner', service_id, perf_key)
- image['stats_history'][s] = mgr.get_counter(
- 'tcmu-runner', service_id, perf_key)[perf_key]
- else:
- daemon['non_optimized_paths'] += 1
- image['non_optimized_paths'].append(hostname)
-
- return {
- 'daemons': sorted(daemons.values(), key=lambda d: d['server_hostname']),
- 'images': sorted(images.values(), key=lambda i: ['id']),
- }
+++ /dev/null
-{
- "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
- "project": {
- "name": "ceph-dashboard"
- },
- "apps": [
- {
- "root": "src",
- "outDir": "dist",
- "assets": [
- "assets",
- "favicon.ico"
- ],
- "index": "index.html",
- "main": "main.ts",
- "polyfills": "polyfills.ts",
- "test": "test.ts",
- "tsconfig": "tsconfig.app.json",
- "testTsconfig": "tsconfig.spec.json",
- "prefix": "cd",
- "styles": [
- "../node_modules/bootstrap/dist/css/bootstrap.css",
- "../node_modules/ng2-toastr/bundles/ng2-toastr.min.css",
- "../node_modules/font-awesome/css/font-awesome.css",
- "../node_modules/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css",
- "styles.scss"
- ],
- "scripts": [
- "../node_modules/chart.js/dist/Chart.bundle.js"
- ],
- "environmentSource": "environments/environment.ts",
- "environments": {
- "dev": "environments/environment.ts",
- "prod": "environments/environment.prod.ts"
- }
- }
- ],
- "e2e": {
- "protractor": {
- "config": "./protractor.conf.js"
- }
- },
- "lint": [
- {
- "project": "src/tsconfig.app.json",
- "exclude": "**/node_modules/**"
- },
- {
- "project": "src/tsconfig.spec.json",
- "exclude": "**/node_modules/**"
- },
- {
- "project": "e2e/tsconfig.e2e.json",
- "exclude": "**/node_modules/**"
- }
- ],
- "test": {
- "karma": {
- "config": "./karma.conf.js"
- }
- },
- "defaults": {
- "styleExt": "scss",
- "component": {}
- }
-}
+++ /dev/null
-# Editor configuration, see http://editorconfig.org
-root = true
-
-[*]
-charset = utf-8
-indent_style = space
-indent_size = 2
-insert_final_newline = true
-trim_trailing_whitespace = true
-
-[*.md]
-max_line_length = off
-trim_trailing_whitespace = false
+++ /dev/null
-# See http://help.github.com/ignore-files/ for more about ignoring files.
-
-# compiled output
-/dist
-/tmp
-/out-tsc
-
-# dependencies
-/node_modules
-
-# IDEs and editors
-/.idea
-.project
-.classpath
-.c9/
-*.launch
-.settings/
-*.sublime-workspace
-
-# IDE - VSCode
-.vscode/*
-!.vscode/settings.json
-!.vscode/tasks.json
-!.vscode/launch.json
-!.vscode/extensions.json
-
-# misc
-/.sass-cache
-/connect.lock
-/coverage
-/libpeerconnection.log
-npm-debug.log
-testem.log
-/typings
-
-# e2e
-/e2e/*.js
-/e2e/*.map
-
-# System Files
-.DS_Store
-Thumbs.db
-
-# Package lock files
-yarn.lock
-package-lock.json
-
-# Ceph
-!core
-!*.core
+++ /dev/null
-import { AppPage } from './app.po';
-
-describe('ceph-dashboard App', () => {
- let page: AppPage;
-
- beforeEach(() => {
- page = new AppPage();
- });
-
- it('should display welcome message', () => {
- page.navigateTo();
- expect(page.getParagraphText()).toEqual('Welcome to oa!');
- });
-});
+++ /dev/null
-import { browser, by, element } from 'protractor';
-
-export class AppPage {
- navigateTo() {
- return browser.get('/');
- }
-
- getParagraphText() {
- return element(by.css('oa-root h1')).getText();
- }
-}
+++ /dev/null
-{
- "extends": "../tsconfig.json",
- "compilerOptions": {
- "outDir": "../out-tsc/e2e",
- "baseUrl": "./",
- "module": "commonjs",
- "target": "es5",
- "types": [
- "jasmine",
- "jasminewd2",
- "node"
- ]
- }
-}
+++ /dev/null
-// Karma configuration file, see link for more information
-// https://karma-runner.github.io/1.0/config/configuration-file.html
-
-module.exports = function (config) {
- config.set({
- basePath: '',
- frameworks: ['jasmine', '@angular/cli'],
- plugins: [
- require('karma-jasmine'),
- require('karma-chrome-launcher'),
- require('karma-jasmine-html-reporter'),
- require('karma-coverage-istanbul-reporter'),
- require('@angular/cli/plugins/karma'),
- require('karma-phantomjs-launcher'),
- require('karma-junit-reporter')
- ],
- client:{
- clearContext: false // leave Jasmine Spec Runner output visible in browser
- },
- coverageIstanbulReporter: {
- reports: [ 'html', 'lcovonly', 'cobertura' ],
- fixWebpackSourcePaths: true
- },
- angularCli: {
- environment: 'dev'
- },
- reporters: ['progress', 'kjhtml', 'junit'],
- junitReporter: {
- 'outputFile': 'junit.frontend.xml',
- 'suite': 'dashboard_v2',
- 'useBrowserName': false
- },
- port: 9876,
- colors: true,
- logLevel: config.LOG_INFO,
- autoWatch: true,
- browsers: ['Chrome'],
- singleRun: false
- });
-};
+++ /dev/null
-{
- "name": "ceph-dashboard",
- "version": "0.0.0",
- "license": "MIT",
- "scripts": {
- "ng": "ng",
- "start": "ng serve",
- "build": "ng build",
- "test": "ng test",
- "lint": "ng lint",
- "e2e": "ng e2e"
- },
- "private": true,
- "dependencies": {
- "@angular/animations": "^5.0.0",
- "@angular/common": "^5.0.0",
- "@angular/compiler": "^5.0.0",
- "@angular/core": "^5.0.0",
- "@angular/forms": "^5.0.0",
- "@angular/http": "^5.0.0",
- "@angular/platform-browser": "^5.0.0",
- "@angular/platform-browser-dynamic": "^5.0.0",
- "@angular/router": "^5.0.0",
- "@swimlane/ngx-datatable": "^11.1.7",
- "@types/lodash": "^4.14.95",
- "awesome-bootstrap-checkbox": "0.3.7",
- "bootstrap": "^3.3.7",
- "chart.js": "^2.7.1",
- "core-js": "^2.4.1",
- "font-awesome": "4.7.0",
- "lodash": "^4.17.4",
- "moment": "2.20.1",
- "ng2-charts": "^1.6.0",
- "ng2-toastr": "4.1.2",
- "ngx-bootstrap": "^2.0.1",
- "rxjs": "^5.5.2",
- "zone.js": "^0.8.14"
- },
- "devDependencies": {
- "@angular/cli": "^1.6.5",
- "@angular/compiler-cli": "^5.0.0",
- "@angular/language-service": "^5.0.0",
- "@types/jasmine": "~2.5.53",
- "@types/jasminewd2": "~2.0.2",
- "@types/node": "~6.0.60",
- "codelyzer": "^4.0.1",
- "copy-webpack-plugin": "4.3.0",
- "jasmine-core": "~2.6.2",
- "jasmine-spec-reporter": "~4.1.0",
- "karma": "~1.7.0",
- "karma-chrome-launcher": "~2.1.1",
- "karma-cli": "~1.0.1",
- "karma-coverage-istanbul-reporter": "^1.2.1",
- "karma-jasmine": "~1.1.0",
- "karma-jasmine-html-reporter": "^0.2.2",
- "karma-junit-reporter": "^1.2.0",
- "karma-phantomjs-launcher": "^1.0.4",
- "node": "^8.9.4",
- "protractor": "~5.1.2",
- "ts-node": "~3.2.0",
- "tslint": "~5.9.1",
- "tslint-eslint-rules": "^4.1.1",
- "typescript": "~2.4.2"
- }
-}
+++ /dev/null
-// Protractor configuration file, see link for more information
-// https://github.com/angular/protractor/blob/master/lib/config.ts
-
-const { SpecReporter } = require('jasmine-spec-reporter');
-
-exports.config = {
- allScriptsTimeout: 11000,
- specs: [
- './e2e/**/*.e2e-spec.ts'
- ],
- capabilities: {
- 'browserName': 'chrome'
- },
- directConnect: true,
- baseUrl: 'http://localhost:4200/',
- framework: 'jasmine',
- jasmineNodeOpts: {
- showColors: true,
- defaultTimeoutInterval: 30000,
- print: function() {}
- },
- onPrepare() {
- require('ts-node').register({
- project: 'e2e/tsconfig.e2e.json'
- });
- jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
- }
-};
+++ /dev/null
-{
- "/api/": {
- "target": "http://localhost:8080",
- "secure": false,
- "logLevel": "debug"
- }
-}
+++ /dev/null
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-
-import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
-import { MirroringComponent } from './ceph/block/mirroring/mirroring.component';
-import { PoolDetailComponent } from './ceph/block/pool-detail/pool-detail.component';
-import { CephfsComponent } from './ceph/cephfs/cephfs/cephfs.component';
-import { ClientsComponent } from './ceph/cephfs/clients/clients.component';
-import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
-import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
-import { MonitorComponent } from './ceph/cluster/monitor/monitor.component';
-import { OsdListComponent } from './ceph/cluster/osd/osd-list/osd-list.component';
-import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component';
-import {
- PerformanceCounterComponent
-} from './ceph/performance-counter/performance-counter/performance-counter.component';
-import { RgwDaemonListComponent } from './ceph/rgw/rgw-daemon-list/rgw-daemon-list.component';
-import { LoginComponent } from './core/auth/login/login.component';
-import { NotFoundComponent } from './core/not-found/not-found.component';
-import { AuthGuardService } from './shared/services/auth-guard.service';
-
-const routes: Routes = [
- { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
- { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuardService] },
- { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
- { path: 'login', component: LoginComponent },
- { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
- {
- path: 'rgw',
- component: RgwDaemonListComponent,
- canActivate: [AuthGuardService]
- },
- { path: 'block/iscsi', component: IscsiComponent, canActivate: [AuthGuardService] },
- { path: 'block/pool/:name', component: PoolDetailComponent, canActivate: [AuthGuardService] },
- {
- path: 'perf_counters/:type/:id',
- component: PerformanceCounterComponent,
- canActivate: [AuthGuardService]
- },
- { path: 'monitor', component: MonitorComponent, canActivate: [AuthGuardService] },
- { path: 'cephfs/:id/clients', component: ClientsComponent, canActivate: [AuthGuardService] },
- { path: 'cephfs/:id', component: CephfsComponent, canActivate: [AuthGuardService] },
- { path: 'configuration', component: ConfigurationComponent, canActivate: [AuthGuardService] },
- { path: 'mirroring', component: MirroringComponent, canActivate: [AuthGuardService] },
- { path: '404', component: NotFoundComponent },
- { path: 'osd', component: OsdListComponent, canActivate: [AuthGuardService] },
- { path: '**', redirectTo: '/404'}
-];
-
-@NgModule({
- imports: [RouterModule.forRoot(routes, { useHash: true })],
- exports: [RouterModule]
-})
-export class AppRoutingModule { }
+++ /dev/null
-<cd-navigation *ngIf="!isLoginActive()"></cd-navigation>
-<div class="container-fluid"
- [ngClass]="{'full-height':isLoginActive()}">
- <router-outlet></router-outlet>
-</div>
+++ /dev/null
-import { async, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { ToastModule } from 'ng2-toastr';
-
-import { AppComponent } from './app.component';
-import { BlockModule } from './ceph/block/block.module';
-import { ClusterModule } from './ceph/cluster/cluster.module';
-import { CoreModule } from './core/core.module';
-import { SharedModule } from './shared/shared.module';
-
-describe('AppComponent', () => {
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- imports: [
- RouterTestingModule,
- CoreModule,
- SharedModule,
- ToastModule.forRoot(),
- ClusterModule,
- BlockModule
- ],
- declarations: [AppComponent]
- }).compileComponents();
- })
- );
-});
+++ /dev/null
-import { Component, ViewContainerRef } from '@angular/core';
-import { Router } from '@angular/router';
-
-import { ToastsManager } from 'ng2-toastr';
-
-import { AuthStorageService } from './shared/services/auth-storage.service';
-
-@Component({
- selector: 'cd-root',
- templateUrl: './app.component.html',
- styleUrls: ['./app.component.scss']
-})
-export class AppComponent {
- title = 'cd';
-
- constructor(private authStorageService: AuthStorageService,
- private router: Router,
- public toastr: ToastsManager,
- private vcr: ViewContainerRef) {
- this.toastr.setRootViewContainerRef(vcr);
- }
-
- isLoginActive() {
- return this.router.url === '/login' || !this.authStorageService.isLoggedIn();
- }
-
-}
+++ /dev/null
-import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
-import { NgModule } from '@angular/core';
-import { BrowserModule } from '@angular/platform-browser';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-
-import { ToastModule, ToastOptions } from 'ng2-toastr/ng2-toastr';
-
-import { AccordionModule, BsDropdownModule, TabsModule } from 'ngx-bootstrap';
-import { AppRoutingModule } from './app-routing.module';
-import { AppComponent } from './app.component';
-import { CephModule } from './ceph/ceph.module';
-import { CoreModule } from './core/core.module';
-import { AuthInterceptorService } from './shared/services/auth-interceptor.service';
-import { SharedModule } from './shared/shared.module';
-
-export class CustomOption extends ToastOptions {
- animate = 'flyRight';
- newestOnTop = true;
- showCloseButton = true;
- enableHTML = true;
-}
-
-@NgModule({
- declarations: [
- AppComponent
- ],
- imports: [
- HttpClientModule,
- BrowserModule,
- BrowserAnimationsModule,
- ToastModule.forRoot(),
- AppRoutingModule,
- HttpClientModule,
- CoreModule,
- SharedModule,
- CephModule,
- AccordionModule.forRoot(),
- BsDropdownModule.forRoot(),
- TabsModule.forRoot(),
- HttpClientModule,
- BrowserAnimationsModule
- ],
- exports: [SharedModule],
- providers: [
- {
- provide: HTTP_INTERCEPTORS,
- useClass: AuthInterceptorService,
- multi: true
- },
- {
- provide: ToastOptions,
- useClass: CustomOption
- },
- ],
- bootstrap: [AppComponent]
-})
-export class AppModule { }
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-
-import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { ComponentsModule } from '../../shared/components/components.module';
-import { PipesModule } from '../../shared/pipes/pipes.module';
-import { ServicesModule } from '../../shared/services/services.module';
-import { SharedModule } from '../../shared/shared.module';
-import { IscsiComponent } from './iscsi/iscsi.component';
-import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
-import { MirroringComponent } from './mirroring/mirroring.component';
-import { PoolDetailComponent } from './pool-detail/pool-detail.component';
-
-@NgModule({
- imports: [
- CommonModule,
- FormsModule,
- TabsModule.forRoot(),
- ProgressbarModule.forRoot(),
- SharedModule,
- ComponentsModule,
- PipesModule,
- ServicesModule
- ],
- declarations: [
- PoolDetailComponent,
- IscsiComponent,
- MirroringComponent,
- MirrorHealthColorPipe
- ]
-})
-export class BlockModule { }
+++ /dev/null
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Block</li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">iSCSI</li>
- </ol>
-</nav>
-
-<legend i18n>Daemons</legend>
-<cd-table [data]="daemons"
- (fetchData)="refresh()"
- [columns]="daemonsColumns">
-</cd-table>
-
-<legend i18n>Images</legend>
-<cd-table [data]="images"
- [columns]="imagesColumns">
-</cd-table>
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { AppModule } from '../../../app.module';
-import { TcmuIscsiService } from '../../../shared/services/tcmu-iscsi.service';
-import { IscsiComponent } from './iscsi.component';
-
-describe('IscsiComponent', () => {
- let component: IscsiComponent;
- let fixture: ComponentFixture<IscsiComponent>;
-
- const fakeService = {
- tcmuiscsi: () => {
- return new Promise(function(resolve, reject) {
- return;
- });
- },
- };
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- imports: [AppModule],
- providers: [{ provide: TcmuIscsiService, useValue: fakeService }]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(IscsiComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component } from '@angular/core';
-
-import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
-import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
-import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
-import { ListPipe } from '../../../shared/pipes/list.pipe';
-import { RelativeDatePipe } from '../../../shared/pipes/relative-date.pipe';
-import { TcmuIscsiService } from '../../../shared/services/tcmu-iscsi.service';
-
-@Component({
- selector: 'cd-iscsi',
- templateUrl: './iscsi.component.html',
- styleUrls: ['./iscsi.component.scss']
-})
-export class IscsiComponent {
-
- daemons = [];
- daemonsColumns: any;
- images = [];
- imagesColumns: any;
-
- constructor(private tcmuIscsiService: TcmuIscsiService,
- cephShortVersionPipe: CephShortVersionPipe,
- dimlessBinaryPipe: DimlessBinaryPipe,
- dimlessPipe: DimlessPipe,
- relativeDatePipe: RelativeDatePipe,
- listPipe: ListPipe) {
- this.daemonsColumns = [
- {
- name: 'Hostname',
- prop: 'server_hostname'
- },
- {
- name: '# Active/Optimized',
- prop: 'optimized_paths',
- },
- {
- name: '# Active/Non-Optimized',
- prop: 'non_optimized_paths'
- },
- {
- name: 'Version',
- prop: 'version',
- pipe: cephShortVersionPipe
- }
- ];
- this.imagesColumns = [
- {
- name: 'Pool',
- prop: 'pool_name'
- },
- {
- name: 'Image',
- prop: 'name'
- },
- {
- name: 'Active/Optimized',
- prop: 'optimized_paths',
- pipe: listPipe
- },
- {
- name: 'Active/Non-Optimized',
- prop: 'non_optimized_paths',
- pipe: listPipe
- },
- {
- name: 'Read Bytes',
- prop: 'stats.rd_bytes',
- pipe: dimlessBinaryPipe
- },
- {
- name: 'Write Bytes',
- prop: 'stats.wr_bytes',
- pipe: dimlessBinaryPipe
- },
- {
- name: 'Read Ops',
- prop: 'stats.rd',
- pipe: dimlessPipe
- },
- {
- name: 'Write Ops',
- prop: 'stats.wr',
- pipe: dimlessPipe
- },
- {
- name: 'A/O Since',
- prop: 'optimized_since',
- pipe: relativeDatePipe
- },
- ];
-
- }
-
- refresh() {
- this.tcmuIscsiService.tcmuiscsi().then((resp) => {
- this.daemons = resp.daemons;
- this.images = resp.images;
- });
- }
-
-}
+++ /dev/null
-import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
-
-describe('MirrorHealthColorPipe', () => {
- it('create an instance', () => {
- const pipe = new MirrorHealthColorPipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
- name: 'mirrorHealthColor'
-})
-export class MirrorHealthColorPipe implements PipeTransform {
- transform(value: any, args?: any): any {
- if (value === 'warning') {
- return 'label label-warning';
- } else if (value === 'error') {
- return 'label label-danger';
- } else if (value === 'success') {
- return 'label label-success';
- }
- return 'label label-info';
- }
-}
+++ /dev/null
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item" i18n>Block</li>
- <li class="breadcrumb-item active"
- aria-current="page" i18n>Mirroring</li>
- </ol>
-</nav>
-
-<cd-view-cache [status]="status"></cd-view-cache>
-
-<div class="row">
- <div class="col-sm-6">
- <fieldset>
- <legend i18n>Daemons</legend>
-
- <cd-table [data]="daemons.data"
- columnMode="flex"
- [columns]="daemons.columns"
- [autoReload]="30000"
- (fetchData)="refresh()">
- </cd-table>
- </fieldset>
- </div>
-
- <div class="col-sm-6">
- <fieldset>
- <legend i18n>Pools</legend>
-
- <cd-table [data]="pools.data"
- columnMode="flex"
- [autoReload]="0"
- (fetchData)="refresh()"
- [columns]="pools.columns">
- </cd-table>
- </fieldset>
- </div>
-</div>
-
-<div class="row">
- <div class="col-md-12">
- <fieldset>
- <legend i18n>Images</legend>
- <tabset>
- <tab heading="Issues" i18n-heading>
- <cd-table [data]="image_error.data"
- columnMode="flex"
- [autoReload]="0"
- (fetchData)="refresh()"
- [columns]="image_error.columns">
- </cd-table>
- </tab>
- <tab heading="Syncing" i18n-heading>
- <cd-table [data]="image_syncing.data"
- columnMode="flex"
- [autoReload]="0"
- (fetchData)="refresh()"
- [columns]="image_syncing.columns">
- </cd-table>
- </tab>
- <tab heading="Ready" i18n-heading>
- <cd-table [data]="image_ready.data"
- columnMode="flex"
- [autoReload]="0"
- (fetchData)="refresh()"
- [columns]="image_ready.columns">
- </cd-table>
- </tab>
- </tabset>
- </fieldset>
- </div>
-</div>
-
-<ng-template #healthTmpl
- let-row="row"
- let-value="value">
- <span [ngClass]="row.health_color | mirrorHealthColor">{{ value }}</span>
-</ng-template>
-
-<ng-template #stateTmpl
- let-row="row"
- let-value="value">
- <span [ngClass]="row.state_color | mirrorHealthColor">{{ value }}</span>
-</ng-template>
-
-<ng-template #syncTmpl>
- <span class="label label-info">Syncing</span>
-</ng-template>
-
-<ng-template #progressTmpl
- let-value="value">
- <progressbar type="info"
- [value]="value">
- </progressbar>
-</ng-template>
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { BsDropdownModule, TabsModule } from 'ngx-bootstrap';
-import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
-import { Observable } from 'rxjs/Observable';
-
-import { RbdMirroringService } from '../../../shared/services/rbd-mirroring.service';
-import { SharedModule } from '../../../shared/shared.module';
-import { MirrorHealthColorPipe } from '../mirror-health-color.pipe';
-import { MirroringComponent } from './mirroring.component';
-
-describe('MirroringComponent', () => {
- let component: MirroringComponent;
- let fixture: ComponentFixture<MirroringComponent>;
-
- const fakeService = {
- get: (service_type: string, service_id: string) => {
- return Observable.create(observer => {
- return () => console.log('disposed');
- });
- }
- };
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- declarations: [MirroringComponent, MirrorHealthColorPipe],
- imports: [
- SharedModule,
- BsDropdownModule.forRoot(),
- TabsModule.forRoot(),
- ProgressbarModule.forRoot(),
- HttpClientTestingModule
- ],
- providers: [{ provide: RbdMirroringService, useValue: fakeService }]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(MirroringComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
-
-import * as _ from 'lodash';
-
-import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
-import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
-import { RbdMirroringService } from '../../../shared/services/rbd-mirroring.service';
-
-@Component({
- selector: 'cd-mirroring',
- templateUrl: './mirroring.component.html',
- styleUrls: ['./mirroring.component.scss']
-})
-export class MirroringComponent implements OnInit {
- @ViewChild('healthTmpl') healthTmpl: TemplateRef<any>;
- @ViewChild('stateTmpl') stateTmpl: TemplateRef<any>;
- @ViewChild('syncTmpl') syncTmpl: TemplateRef<any>;
- @ViewChild('progressTmpl') progressTmpl: TemplateRef<any>;
-
- contentData: any;
-
- status: ViewCacheStatus;
- daemons = {
- data: [],
- columns: []
- };
- pools = {
- data: [],
- columns: {}
- };
- image_error = {
- data: [],
- columns: {}
- };
- image_syncing = {
- data: [],
- columns: {}
- };
- image_ready = {
- data: [],
- columns: {}
- };
-
- constructor(
- private http: HttpClient,
- private rbdMirroringService: RbdMirroringService,
- private cephShortVersionPipe: CephShortVersionPipe
- ) { }
-
- ngOnInit() {
- this.daemons.columns = [
- { prop: 'instance_id', name: 'Instance', flexGrow: 2 },
- { prop: 'id', name: 'ID', flexGrow: 2 },
- { prop: 'server_hostname', name: 'Hostname', flexGrow: 2 },
- {
- prop: 'server_hostname',
- name: 'Version',
- pipe: this.cephShortVersionPipe,
- flexGrow: 2
- },
- {
- prop: 'health',
- name: 'Health',
- cellTemplate: this.healthTmpl,
- flexGrow: 1
- }
- ];
-
- this.pools.columns = [
- { prop: 'name', name: 'Name', flexGrow: 2 },
- { prop: 'mirror_mode', name: 'Mode', flexGrow: 2 },
- { prop: 'leader_id', name: 'Leader', flexGrow: 2 },
- { prop: 'image_local_count', name: '# Local', flexGrow: 2 },
- { prop: 'image_remote_count', name: '# Remote', flexGrow: 2 },
- {
- prop: 'health',
- name: 'Health',
- cellTemplate: this.healthTmpl,
- flexGrow: 1
- }
- ];
-
- this.image_error.columns = [
- { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
- { prop: 'name', name: 'Image', flexGrow: 2 },
- { prop: 'description', name: 'Issue', flexGrow: 4 },
- {
- prop: 'state',
- name: 'State',
- cellTemplate: this.stateTmpl,
- flexGrow: 1
- }
- ];
-
- this.image_syncing.columns = [
- { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
- { prop: 'name', name: 'Image', flexGrow: 2 },
- {
- prop: 'progress',
- name: 'Progress',
- cellTemplate: this.progressTmpl,
- flexGrow: 2
- },
- {
- prop: 'state',
- name: 'State',
- cellTemplate: this.syncTmpl,
- flexGrow: 1
- }
- ];
-
- this.image_ready.columns = [
- { prop: 'pool_name', name: 'Pool', flexGrow: 2 },
- { prop: 'name', name: 'Image', flexGrow: 2 },
- { prop: 'description', name: 'Description', flexGrow: 4 },
- {
- prop: 'state',
- name: 'State',
- cellTemplate: this.stateTmpl,
- flexGrow: 1
- }
- ];
- }
-
- refresh() {
- this.rbdMirroringService.get().subscribe((data: any) => {
- this.daemons.data = data.content_data.daemons;
- this.pools.data = data.content_data.pools;
- this.image_error.data = data.content_data.image_error;
- this.image_syncing.data = data.content_data.image_syncing;
- this.image_ready.data = data.content_data.image_ready;
-
- this.status = data.status;
- });
- }
-}
+++ /dev/null
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Block</li>
- <li i18n
- class="breadcrumb-item">Pools</li>
- <li class="breadcrumb-item active"
- aria-current="page">{{ name }}</li>
- </ol>
-</nav>
-
-<cd-view-cache [status]="viewCacheStatus"></cd-view-cache>
-
-<cd-table [data]="images"
- columnMode="flex"
- [columns]="columns"
- (fetchData)="loadImages()">
-</cd-table>
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { AlertModule, BsDropdownModule, TabsModule } from 'ngx-bootstrap';
-
-import { ComponentsModule } from '../../../shared/components/components.module';
-import { SharedModule } from '../../../shared/shared.module';
-import { PoolDetailComponent } from './pool-detail.component';
-
-describe('PoolDetailComponent', () => {
- let component: PoolDetailComponent;
- let fixture: ComponentFixture<PoolDetailComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [
- SharedModule,
- BsDropdownModule.forRoot(),
- TabsModule.forRoot(),
- AlertModule.forRoot(),
- ComponentsModule,
- RouterTestingModule,
- HttpClientTestingModule
- ],
- declarations: [ PoolDetailComponent ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(PoolDetailComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnDestroy, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-
-import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
-import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
-import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
-import { PoolService } from '../../../shared/services/pool.service';
-
-@Component({
- selector: 'cd-pool-detail',
- templateUrl: './pool-detail.component.html',
- styleUrls: ['./pool-detail.component.scss']
-})
-export class PoolDetailComponent implements OnInit, OnDestroy {
- name: string;
- images: any;
- columns: CdTableColumn[];
- retries: number;
- routeParamsSubscribe: any;
- viewCacheStatus: ViewCacheStatus;
-
- constructor(
- private route: ActivatedRoute,
- private poolService: PoolService,
- dimlessBinaryPipe: DimlessBinaryPipe,
- dimlessPipe: DimlessPipe
- ) {
- this.columns = [
- {
- name: 'Name',
- prop: 'name',
- flexGrow: 2
- },
- {
- name: 'Size',
- prop: 'size',
- flexGrow: 1,
- cellClass: 'text-right',
- pipe: dimlessBinaryPipe
- },
- {
- name: 'Objects',
- prop: 'num_objs',
- flexGrow: 1,
- cellClass: 'text-right',
- pipe: dimlessPipe
- },
- {
- name: 'Object size',
- prop: 'obj_size',
- flexGrow: 1,
- cellClass: 'text-right',
- pipe: dimlessBinaryPipe
- },
- {
- name: 'Features',
- prop: 'features_name',
- flexGrow: 3
- },
- {
- name: 'Parent',
- prop: 'parent',
- flexGrow: 2
- }
- ];
- }
-
- ngOnInit() {
- this.routeParamsSubscribe = this.route.params.subscribe((params: { name: string }) => {
- this.name = params.name;
- this.images = [];
- this.retries = 0;
- });
- }
-
- ngOnDestroy() {
- this.routeParamsSubscribe.unsubscribe();
- }
-
- loadImages() {
- this.poolService.rbdPoolImages(this.name).then(
- resp => {
- this.viewCacheStatus = resp.status;
- this.images = resp.value;
- },
- () => {
- this.viewCacheStatus = ViewCacheStatus.ValueException;
- }
- );
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { SharedModule } from '../shared/shared.module';
-import { BlockModule } from './block/block.module';
-import { CephfsModule } from './cephfs/cephfs.module';
-import { ClusterModule } from './cluster/cluster.module';
-import { DashboardModule } from './dashboard/dashboard.module';
-import { PerformanceCounterModule } from './performance-counter/performance-counter.module';
-import { RgwModule } from './rgw/rgw.module';
-
-@NgModule({
- imports: [
- CommonModule,
- ClusterModule,
- DashboardModule,
- RgwModule,
- PerformanceCounterModule,
- BlockModule,
- CephfsModule,
- SharedModule
- ],
- declarations: []
-})
-export class CephModule { }
+++ /dev/null
-<div class="chart-container">
- <canvas baseChart
- #chartCanvas
- [datasets]="chart?.datasets"
- [options]="chart?.options"
- [chartType]="chart?.chartType">
- </canvas>
- <div class="chartjs-tooltip"
- #chartTooltip>
- <table></table>
- </div>
-</div>
+++ /dev/null
-@import '../../../../styles/chart-tooltip.scss';
-
-.chart-container {
- height: 500px;
- width: 100%;
-}
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { ChartsModule } from 'ng2-charts/ng2-charts';
-
-import { CephfsChartComponent } from './cephfs-chart.component';
-
-describe('CephfsChartComponent', () => {
- let component: CephfsChartComponent;
- let fixture: ComponentFixture<CephfsChartComponent>;
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- imports: [ChartsModule],
- declarations: [CephfsChartComponent]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(CephfsChartComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, ElementRef, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
-
-import * as _ from 'lodash';
-import * as moment from 'moment';
-
-import { ChartTooltip } from '../../../shared/models/chart-tooltip';
-
-@Component({
- selector: 'cd-cephfs-chart',
- templateUrl: './cephfs-chart.component.html',
- styleUrls: ['./cephfs-chart.component.scss']
-})
-export class CephfsChartComponent implements OnChanges, OnInit {
- @ViewChild('chartCanvas') chartCanvas: ElementRef;
- @ViewChild('chartTooltip') chartTooltip: ElementRef;
-
- @Input() mdsCounter: any;
-
- lhsCounter = 'mds.inodes';
- rhsCounter = 'mds_server.handle_client_request';
-
- chart: any;
-
- constructor() {}
-
- ngOnInit() {
- if (_.isUndefined(this.mdsCounter)) {
- return;
- }
-
- const getTitle = title => {
- return moment(title).format('LTS');
- };
-
- const getStyleTop = tooltip => {
- return tooltip.caretY - tooltip.height - 15 + 'px';
- };
-
- const getStyleLeft = tooltip => {
- return tooltip.caretX + 'px';
- };
-
- const chartTooltip = new ChartTooltip(
- this.chartCanvas,
- this.chartTooltip,
- getStyleLeft,
- getStyleTop
- );
- chartTooltip.getTitle = getTitle;
- chartTooltip.checkOffset = true;
-
- const lhsData = this.convert_timeseries(this.mdsCounter[this.lhsCounter]);
- const rhsData = this.delta_timeseries(this.mdsCounter[this.rhsCounter]);
-
- this.chart = {
- datasets: [
- {
- label: this.lhsCounter,
- yAxisID: 'LHS',
- data: lhsData,
- tension: 0.1
- },
- {
- label: this.rhsCounter,
- yAxisID: 'RHS',
- data: rhsData,
- tension: 0.1
- }
- ],
- options: {
- responsive: true,
- maintainAspectRatio: false,
- legend: {
- position: 'top'
- },
- scales: {
- xAxes: [
- {
- position: 'top',
- type: 'time',
- time: {
- displayFormats: {
- quarter: 'MMM YYYY'
- }
- }
- }
- ],
- yAxes: [
- {
- id: 'LHS',
- type: 'linear',
- position: 'left',
- min: 0
- },
- {
- id: 'RHS',
- type: 'linear',
- position: 'right',
- min: 0
- }
- ]
- },
- tooltips: {
- enabled: false,
- mode: 'index',
- intersect: false,
- position: 'nearest',
- custom: tooltip => {
- chartTooltip.customTooltips(tooltip);
- }
- }
- },
- chartType: 'line'
- };
- }
-
- ngOnChanges() {
- if (!this.chart) {
- return;
- }
-
- const lhsData = this.convert_timeseries(this.mdsCounter[this.lhsCounter]);
- const rhsData = this.delta_timeseries(this.mdsCounter[this.rhsCounter]);
-
- this.chart.datasets[0].data = lhsData;
- this.chart.datasets[1].data = rhsData;
- }
-
- // Convert ceph-mgr's time series format (list of 2-tuples
- // with seconds-since-epoch timestamps) into what chart.js
- // can handle (list of objects with millisecs-since-epoch
- // timestamps)
- convert_timeseries(sourceSeries) {
- const data = [];
- _.each(sourceSeries, dp => {
- data.push({
- x: dp[0] * 1000,
- y: dp[1]
- });
- });
-
- return data;
- }
-
- delta_timeseries(sourceSeries) {
- let i;
- let prev = sourceSeries[0];
- const result = [];
- for (i = 1; i < sourceSeries.length; i++) {
- const cur = sourceSeries[i];
- const tdelta = cur[0] - prev[0];
- const vdelta = cur[1] - prev[1];
- const rate = vdelta / tdelta;
-
- result.push({
- x: cur[0] * 1000,
- y: rate
- });
-
- prev = cur;
- }
- return result;
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { ChartsModule } from 'ng2-charts/ng2-charts';
-import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
-
-import { AppRoutingModule } from '../../app-routing.module';
-import { SharedModule } from '../../shared/shared.module';
-import { CephfsChartComponent } from './cephfs-chart/cephfs-chart.component';
-import { CephfsService } from './cephfs.service';
-import { CephfsComponent } from './cephfs/cephfs.component';
-import { ClientsComponent } from './clients/clients.component';
-
-@NgModule({
- imports: [
- CommonModule,
- SharedModule,
- AppRoutingModule,
- ChartsModule,
- ProgressbarModule.forRoot()
- ],
- declarations: [CephfsComponent, ClientsComponent, CephfsChartComponent],
- providers: [CephfsService]
-})
-export class CephfsModule {}
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { CephfsService } from './cephfs.service';
-
-describe('CephfsService', () => {
- beforeEach(() => {
- TestBed.configureTestingModule({
- imports: [HttpClientModule],
- providers: [CephfsService]
- });
- });
-
- it(
- 'should be created',
- inject([CephfsService], (service: CephfsService) => {
- expect(service).toBeTruthy();
- })
- );
-});
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class CephfsService {
- baseURL = 'api/cephfs';
-
- constructor(private http: HttpClient) {}
-
- getCephfs(id) {
- return this.http.get(`${this.baseURL}/data/${id}`);
- }
-
- getClients(id) {
- return this.http.get(`${this.baseURL}/clients/${id}`);
- }
-
- getMdsCounters(id) {
- return this.http.get(`${this.baseURL}/mds_counters/${id}`);
- }
-}
+++ /dev/null
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Filesystem</li>
- <li class="breadcrumb-item active"
- aria-current="page">{{ name }}</li>
- </ol>
-</nav>
-
-<div class="row">
- <div class="col-md-12">
- <i class="fa fa-desktop"></i>
- <a i18n
- [routerLink]="['/cephfs/' + id + '/clients']">
- <span style="font-weight:bold;">{{ clientCount }}</span>
- Clients
- </a>
- </div>
-</div>
-
-<div class="row">
- <div class="col-sm-6">
- <fieldset>
- <legend i18n>Ranks</legend>
-
- <cd-table [data]="ranks.data"
- [columns]="ranks.columns"
- (fetchData)="refresh()"
- [toolHeader]="false">
- </cd-table>
- </fieldset>
-
- <cd-table-key-value [data]="standbys">
- </cd-table-key-value>
- </div>
-
- <div class="col-sm-6">
- <fieldset>
- <legend i18n>Pools</legend>
-
- <cd-table [data]="pools.data"
- [columns]="pools.columns"
- [toolHeader]="false">
- </cd-table>
-
- </fieldset>
- </div>
-</div>
-
-<div class="row"
- *ngFor="let mdsCounter of objectValues(mdsCounters); trackBy: trackByFn">
- <div class="cold-md-12">
- <cd-cephfs-chart [mdsCounter]="mdsCounter"></cd-cephfs-chart>
- </div>
-</div>
-
-<!-- templates -->
-<ng-template #poolProgressTmpl
- let-row="row">
- <progressbar type="danger"
- [value]="row.used * 100.0 / row.avail">
- </progressbar>
-</ng-template>
-
-<ng-template #activityTmpl
- let-row="row"
- let-value="value">
- {{ row.state === 'standby-replay' ? 'Evts' : 'Reqs' }}: {{ value | dimless }} /s
-</ng-template>
+++ /dev/null
-.progress {
- margin-bottom: 0px;
-}
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { ChartsModule } from 'ng2-charts/ng2-charts';
-import { BsDropdownModule, ProgressbarModule } from 'ngx-bootstrap';
-import { Observable } from 'rxjs/Observable';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { CephfsChartComponent } from '../cephfs-chart/cephfs-chart.component';
-import { CephfsService } from '../cephfs.service';
-import { CephfsComponent } from './cephfs.component';
-
-describe('CephfsComponent', () => {
- let component: CephfsComponent;
- let fixture: ComponentFixture<CephfsComponent>;
-
- const fakeFilesystemService = {
- getCephfs: id => {
- return Observable.create(observer => {
- return () => console.log('disposed');
- });
- },
- getMdsCounters: id => {
- return Observable.create(observer => {
- return () => console.log('disposed');
- });
- }
- };
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- imports: [
- SharedModule,
- ChartsModule,
- RouterTestingModule,
- BsDropdownModule.forRoot(),
- ProgressbarModule.forRoot()
- ],
- declarations: [CephfsComponent, CephfsChartComponent],
- providers: [
- { provide: CephfsService, useValue: fakeFilesystemService }
- ]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(CephfsComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-
-import * as _ from 'lodash';
-import { Subscription } from 'rxjs/Subscription';
-
-import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
-import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
-import { CephfsService } from '../cephfs.service';
-
-@Component({
- selector: 'cd-cephfs',
- templateUrl: './cephfs.component.html',
- styleUrls: ['./cephfs.component.scss']
-})
-export class CephfsComponent implements OnInit, OnDestroy {
- @ViewChild('poolProgressTmpl') poolProgressTmpl: TemplateRef<any>;
- @ViewChild('activityTmpl') activityTmpl: TemplateRef<any>;
-
- routeParamsSubscribe: Subscription;
-
- objectValues = Object.values;
-
- id: number;
- name: string;
- ranks: any;
- pools: any;
- standbys = [];
- clientCount: number;
-
- mdsCounters = {};
-
- constructor(
- private route: ActivatedRoute,
- private cephfsService: CephfsService,
- private dimlessBinary: DimlessBinaryPipe,
- private dimless: DimlessPipe
- ) {}
-
- ngOnInit() {
- this.ranks = {
- columns: [
- { prop: 'rank' },
- { prop: 'state' },
- { prop: 'mds', name: 'Daemon' },
- { prop: 'activity', cellTemplate: this.activityTmpl },
- { prop: 'dns', name: 'Dentries', pipe: this.dimless },
- { prop: 'inos', name: 'Inodes', pipe: this.dimless }
- ],
- data: []
- };
-
- this.pools = {
- columns: [
- { prop: 'pool' },
- { prop: 'type' },
- { prop: 'used', pipe: this.dimlessBinary },
- { prop: 'avail', pipe: this.dimlessBinary },
- {
- name: 'Usage',
- cellTemplate: this.poolProgressTmpl,
- comparator: (valueA, valueB, rowA, rowB, sortDirection) => {
- const valA = rowA.used / rowA.avail;
- const valB = rowB.used / rowB.avail;
-
- if (valA === valB) {
- return 0;
- }
-
- if (valA > valB) {
- return 1;
- } else {
- return -1;
- }
- }
- }
- ],
- data: []
- };
-
- this.routeParamsSubscribe = this.route.params.subscribe((params: { id: number }) => {
- this.id = params.id;
-
- this.ranks.data = [];
- this.pools.data = [];
- this.standbys = [];
- this.mdsCounters = {};
- });
- }
-
- ngOnDestroy() {
- this.routeParamsSubscribe.unsubscribe();
- }
-
- refresh() {
- this.cephfsService.getCephfs(this.id).subscribe((data: any) => {
- this.ranks.data = data.cephfs.ranks;
- this.pools.data = data.cephfs.pools;
- this.standbys = [
- {
- key: 'Standby daemons',
- value: data.standbys.map(value => value.name).join(', ')
- }
- ];
- this.name = data.cephfs.name;
- this.clientCount = data.cephfs.client_count;
- });
-
- this.cephfsService.getMdsCounters(this.id).subscribe(data => {
- _.each(this.mdsCounters, (value, key) => {
- if (data[key] === undefined) {
- delete this.mdsCounters[key];
- }
- });
-
- _.each(data, (mdsData: any, mdsName) => {
- mdsData.name = mdsName;
- this.mdsCounters[mdsName] = mdsData;
- });
- });
- }
-
- trackByFn(index, item) {
- return item.name;
- }
-}
+++ /dev/null
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Filesystem</li>
- <li class="breadcrumb-item">
- <a [routerLink]="['/cephfs/' + id]">{{ name }}</a>
- </li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">Clients</li>
- </ol>
-</nav>
-
-<fieldset>
- <cd-view-cache [status]="viewCacheStatus"></cd-view-cache>
-
- <cd-table [data]="clients.data"
- [columns]="clients.columns"
- (fetchData)="refresh()"
- [header]="false">
- </cd-table>
-</fieldset>
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { BsDropdownModule } from 'ngx-bootstrap';
-import { Observable } from 'rxjs/Observable';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { CephfsService } from '../cephfs.service';
-import { ClientsComponent } from './clients.component';
-
-describe('ClientsComponent', () => {
- let component: ClientsComponent;
- let fixture: ComponentFixture<ClientsComponent>;
-
- const fakeFilesystemService = {
- getCephfs: id => {
- return Observable.create(observer => {
- return () => console.log('disposed');
- });
- },
- getClients: id => {
- return Observable.create(observer => {
- return () => console.log('disposed');
- });
- }
- };
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- imports: [
- RouterTestingModule,
- BsDropdownModule.forRoot(),
- SharedModule
- ],
- declarations: [ClientsComponent],
- providers: [{ provide: CephfsService, useValue: fakeFilesystemService }]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(ClientsComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnDestroy, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-
-import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
-import { CephfsService } from '../cephfs.service';
-
-@Component({
- selector: 'cd-clients',
- templateUrl: './clients.component.html',
- styleUrls: ['./clients.component.scss']
-})
-export class ClientsComponent implements OnInit, OnDestroy {
- routeParamsSubscribe: any;
-
- id: number;
- name: string;
- clients: any;
- viewCacheStatus: ViewCacheStatus;
-
- constructor(private route: ActivatedRoute, private cephfsService: CephfsService) {}
-
- ngOnInit() {
- this.clients = {
- columns: [
- { prop: 'id' },
- { prop: 'type' },
- { prop: 'state' },
- { prop: 'version' },
- { prop: 'hostname', name: 'Host' },
- { prop: 'root' }
- ],
- data: []
- };
-
- this.routeParamsSubscribe = this.route.params.subscribe((params: { id: number }) => {
- this.id = params.id;
- this.clients.data = [];
- this.viewCacheStatus = ViewCacheStatus.ValueNone;
-
- this.cephfsService.getCephfs(this.id).subscribe((data: any) => {
- this.name = data.cephfs.name;
- });
- });
- }
-
- ngOnDestroy() {
- this.routeParamsSubscribe.unsubscribe();
- }
-
- refresh() {
- this.cephfsService.getClients(this.id).subscribe((data: any) => {
- this.viewCacheStatus = data.status;
- this.clients.data = data.data;
- });
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { RouterModule } from '@angular/router';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { ComponentsModule } from '../../shared/components/components.module';
-import { SharedModule } from '../../shared/shared.module';
-import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
-import { ConfigurationComponent } from './configuration/configuration.component';
-import { HostsComponent } from './hosts/hosts.component';
-import { MonitorService } from './monitor.service';
-import { MonitorComponent } from './monitor/monitor.component';
-import { OsdDetailsComponent } from './osd/osd-details/osd-details.component';
-import { OsdListComponent } from './osd/osd-list/osd-list.component';
-import {
- OsdPerformanceHistogramComponent
-} from './osd/osd-performance-histogram/osd-performance-histogram.component';
-import { OsdService } from './osd/osd.service';
-
-@NgModule({
- entryComponents: [
- OsdDetailsComponent
- ],
- imports: [
- CommonModule,
- PerformanceCounterModule,
- ComponentsModule,
- TabsModule.forRoot(),
- SharedModule,
- RouterModule,
- FormsModule
- ],
- declarations: [
- HostsComponent,
- MonitorComponent,
- ConfigurationComponent,
- OsdListComponent,
- OsdDetailsComponent,
- OsdPerformanceHistogramComponent
- ],
- providers: [
- MonitorService,
- OsdService
- ]
-})
-export class ClusterModule {}
+++ /dev/null
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item">Cluster</li>
- <li class="breadcrumb-item active"
- aria-current="page">Configuration Documentation</li>
- </ol>
-</nav>
-
-<div class="dataTables_wrapper">
- <div class="dataTables_header clearfix form-inline">
- <!-- filters -->
- <div class="form-group pull-right filter"
- *ngFor="let filter of filters">
- <label>{{ filter.label }}: </label>
- <select class="form-control input-sm"
- [(ngModel)]="filter.value"
- (ngModelChange)="updateFilter()">
- <option *ngFor="let opt of filter.options">{{ opt }}</option>
- </select>
- </div>
- <!-- end filters -->
- </div>
-
- <table class="oadatatable table table-striped table-condensed table-bordered table-hover">
- <thead class="datatable-header">
- <tr>
- <th >Name</th>
- <th style="width:400px;">Description</th>
- <th>Type</th>
- <th>Level</th>
- <th style="width: 200px">Default</th>
- <th>Tags</th>
- <th>Services</th>
- <th>See_also</th>
- <th>Max</th>
- <th>Min</th>
- </tr>
- </thead>
- <tbody>
- <tr *ngFor="let row of data | filter:filters">
- <td >{{ row.name }}</td>
- <td>
- <p>
- {{ row.desc }}</p>
- <p *ngIf="row.long_desc"
- class=text-muted>{{ row.long_desc }}</p>
- </td>
- <td>{{ row.type }}</td>
- <td>{{ row.level }}</td>
- <td class="wrap">
- {{ row.default }} {{ row.daemon_default }}
- </td>
- <td>
- <p *ngFor="let item of row.tags">{{ item }}</p>
- </td>
- <td>
- <p *ngFor="let item of row.services">{{ item }}</p>
- </td>
- <td class="wrap">
- <p *ngFor="let item of row.see_also">{{ item }}</p>
- </td>
- <td>{{ row.max }}</td>
- <td>{{ row.min }}</td>
- </tr>
- </tbody>
- </table>
-</div>
+++ /dev/null
-@import '../../../shared/datatable/table/table.component.scss';
-
-td.wrap {
- word-break: break-all;
-}
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormsModule } from '@angular/forms';
-
-import { Observable } from 'rxjs/Observable';
-
-import { ConfigurationService } from '../../../shared/services/configuration.service';
-import { SharedModule } from '../../../shared/shared.module';
-import { ConfigurationComponent } from './configuration.component';
-
-describe('ConfigurationComponent', () => {
- let component: ConfigurationComponent;
- let fixture: ComponentFixture<ConfigurationComponent>;
-
- const fakeService = {
- getConfigData: () => {
- return Observable.create(observer => {
- return () => console.log('disposed');
- });
- }
- };
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- declarations: [ConfigurationComponent],
- providers: [{ provide: ConfigurationService, useValue: fakeService }],
- imports: [SharedModule, FormsModule]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(ConfigurationComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
-
-import { ConfigurationService } from '../../../shared/services/configuration.service';
-
-@Component({
- selector: 'cd-configuration',
- templateUrl: './configuration.component.html',
- styleUrls: ['./configuration.component.scss']
-})
-export class ConfigurationComponent implements OnInit {
- @ViewChild('arrayTmpl') arrayTmpl: TemplateRef<any>;
-
- data = [];
- columns: any;
-
- filters = [
- {
- label: 'Level',
- prop: 'level',
- value: 'basic',
- options: ['basic', 'advanced', 'dev'],
- applyFilter: (row, value) => {
- enum Level {
- basic = 0,
- advanced = 1,
- dev = 2
- }
-
- const levelVal = Level[value];
-
- return Level[row.level] <= levelVal;
- }
- },
- {
- label: 'Service',
- prop: 'services',
- value: 'any',
- options: ['mon', 'mgr', 'osd', 'mds', 'common', 'mds_client', 'rgw', 'any'],
- applyFilter: (row, value) => {
- if (value === 'any') {
- return true;
- }
-
- return row.services.includes(value);
- }
- }
- ];
-
- constructor(private configurationService: ConfigurationService) {}
-
- ngOnInit() {
- this.columns = [
- { flexGrow: 2, canAutoResize: true, prop: 'name' },
- { flexGrow: 2, prop: 'desc', name: 'Description' },
- { flexGrow: 2, prop: 'long_desc', name: 'Long description' },
- { flexGrow: 1, prop: 'type' },
- { flexGrow: 1, prop: 'level' },
- { flexGrow: 1, prop: 'default' },
- { flexGrow: 2, prop: 'daemon_default', name: 'Daemon default' },
- { flexGrow: 1, prop: 'tags', name: 'Tags', cellTemplate: this.arrayTmpl },
- { flexGrow: 1, prop: 'services', name: 'Services', cellTemplate: this.arrayTmpl },
- { flexGrow: 1, prop: 'see_also', name: 'See_also', cellTemplate: this.arrayTmpl },
- { flexGrow: 1, prop: 'max', name: 'Max' },
- { flexGrow: 1, prop: 'min', name: 'Min' }
- ];
-
- this.fetchData();
- }
-
- fetchData() {
- this.configurationService.getConfigData().subscribe((data: any) => {
- this.data = data;
- });
- }
-
- updateFilter() {
- this.data = [...this.data];
- }
-}
+++ /dev/null
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Cluster</li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">Hosts</li>
- </ol>
-</nav>
-<cd-table [data]="hosts"
- [columns]="columns"
- columnMode="flex"
- (fetchData)="getHosts()">
- <ng-template #servicesTpl let-value="value">
- <span *ngFor="let service of value; last as isLast">
- <a [routerLink]="[service.cdLink]">{{ service.type }}.{{ service.id }}</a>{{ !isLast ? ", " : "" }}
- </span>
- </ng-template>
-</cd-table>
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { BsDropdownModule } from 'ngx-bootstrap';
-
-import { ComponentsModule } from '../../../shared/components/components.module';
-import { SharedModule } from '../../../shared/shared.module';
-import { HostsComponent } from './hosts.component';
-
-describe('HostsComponent', () => {
- let component: HostsComponent;
- let fixture: ComponentFixture<HostsComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [
- SharedModule,
- HttpClientTestingModule,
- ComponentsModule,
- BsDropdownModule.forRoot(),
- RouterTestingModule
- ],
- declarations: [
- HostsComponent
- ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(HostsComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
-
-import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
-import { HostService } from '../../../shared/services/host.service';
-
-@Component({
- selector: 'cd-hosts',
- templateUrl: './hosts.component.html',
- styleUrls: ['./hosts.component.scss']
-})
-export class HostsComponent implements OnInit {
-
- columns: Array<CdTableColumn> = [];
- hosts: Array<object> = [];
- isLoadingHosts = false;
-
- @ViewChild('servicesTpl') public servicesTpl: TemplateRef<any>;
-
- constructor(private hostService: HostService,
- private cephShortVersionPipe: CephShortVersionPipe) { }
-
- ngOnInit() {
- this.columns = [
- {
- name: 'Hostname',
- prop: 'hostname',
- flexGrow: 1
- },
- {
- name: 'Services',
- prop: 'services',
- flexGrow: 3,
- cellTemplate: this.servicesTpl
- },
- {
- name: 'Version',
- prop: 'ceph_version',
- flexGrow: 1,
- pipe: this.cephShortVersionPipe
- }
- ];
- }
-
- getHosts() {
- if (this.isLoadingHosts) {
- return;
- }
- this.isLoadingHosts = true;
- this.hostService.list().then((resp) => {
- resp.map((host) => {
- host.services.map((service) => {
- service.cdLink = `/perf_counters/${service.type}/${service.id}`;
- return service;
- });
- return host;
- });
- this.hosts = resp;
- this.isLoadingHosts = false;
- }).catch(() => {
- this.isLoadingHosts = false;
- });
- }
-}
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import {
- HttpClientTestingModule,
- HttpTestingController
-} from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { MonitorService } from './monitor.service';
-
-describe('MonitorService', () => {
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [MonitorService],
- imports: [HttpClientTestingModule, HttpClientModule]
- });
- });
-
- it('should be created', inject([MonitorService], (service: MonitorService) => {
- expect(service).toBeTruthy();
- }));
-});
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class MonitorService {
- constructor(private http: HttpClient) {}
-
- getMonitor() {
- return this.http.get('api/monitor');
- }
-}
+++ /dev/null
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Cluster</li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">Monitors</li>
- </ol>
-</nav>
-
-<div class="row">
- <div class="col-md-4">
- <fieldset>
- <legend i18n>Status</legend>
- <table class="table table-striped"
- *ngIf="mon_status">
- <tr>
- <td i18n
- class="bold">Cluster ID</td>
- <td>{{ mon_status.monmap.fsid }}</td>
- </tr>
- <tr>
- <td i18n
- class="bold">monmap modified</td>
- <td>{{ mon_status.monmap.modified }}</td>
- </tr>
- <tr>
- <td i18n
- class="bold">monmap epoch</td>
- <td>{{ mon_status.monmap.epoch }}</td>
- </tr>
- <tr>
- <td i18n
- class="bold">quorum con</td>
- <td>{{ mon_status.features.quorum_con }}</td>
- </tr>
- <tr>
- <td i18n
- class="bold">quorum mon</td>
- <td>{{ mon_status.features.quorum_mon }}</td>
- </tr>
- <tr>
- <td i18n
- class="bold">required con</td>
- <td>{{ mon_status.features.required_con }}</td>
- </tr>
- <tr>
- <td i18n
- class="bold">required mon</td>
- <td>{{ mon_status.features.required_mon }}</td>
- </tr>
- </table>
- </fieldset>
- </div>
-
- <div class="col-md-8">
- <fieldset>
- <legend i18n
- class="in-quorum">In Quorum</legend>
- <cd-table [data]="inQuorum.data"
- [columns]="inQuorum.columns">
- </cd-table>
-
- <legend i18n
- class="in-quorum">Not In Quorum</legend>
- <cd-table [data]="notInQuorum.data"
- (fetchData)="refresh()"
- [columns]="notInQuorum.columns">
- </cd-table>
- </fieldset>
- </div>
-</div>
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { AppModule } from '../../../app.module';
-import { MonitorComponent } from './monitor.component';
-
-describe('MonitorComponent', () => {
- let component: MonitorComponent;
- let fixture: ComponentFixture<MonitorComponent>;
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- imports: [AppModule]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(MonitorComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-});
+++ /dev/null
-import { Component } from '@angular/core';
-
-import { CellTemplate } from '../../../shared/enum/cell-template.enum';
-import { MonitorService } from '../monitor.service';
-
-@Component({
- selector: 'cd-monitor',
- templateUrl: './monitor.component.html',
- styleUrls: ['./monitor.component.scss']
-})
-export class MonitorComponent {
-
- mon_status: any;
- inQuorum: any;
- notInQuorum: any;
-
- interval: any;
- sparklineStyle = {
- height: '30px',
- width: '50%'
- };
-
- constructor(private monitorService: MonitorService) {
- this.inQuorum = {
- columns: [
- { prop: 'name', name: 'Name', cellTransformation: CellTemplate.routerLink },
- { prop: 'rank', name: 'Rank' },
- { prop: 'public_addr', name: 'Public Address' },
- {
- prop: 'cdOpenSessions',
- name: 'Open Sessions',
- cellTransformation: CellTemplate.sparkline
- }
- ],
- data: []
- };
-
- this.notInQuorum = {
- columns: [
- { prop: 'name', name: 'Name', cellTransformation: CellTemplate.routerLink },
- { prop: 'rank', name: 'Rank' },
- { prop: 'public_addr', name: 'Public Address' }
- ],
- data: []
- };
- }
-
- refresh() {
- this.monitorService.getMonitor().subscribe((data: any) => {
- data.in_quorum.map((row) => {
- row.cdOpenSessions = row.stats.num_sessions.map(i => i[1]);
- row.cdLink = '/perf_counters/mon/' + row.name;
- return row;
- });
-
- data.out_quorum.map((row) => {
- row.cdLink = '/perf_counters/mon/' + row.name;
- return row;
- });
-
- this.inQuorum.data = [...data.in_quorum];
- this.notInQuorum.data = [...data.out_quorum];
- this.mon_status = data.mon_status;
- });
- }
-}
+++ /dev/null
-<tabset *ngIf="selection.hasSingleSelection">
- <tab heading="Attributes (OSD map)">
- <cd-table-key-value *ngIf="osd.loaded"
- [data]="osd.details.osd_map">
- </cd-table-key-value>
- </tab>
- <tab heading="Metadata">
- <cd-table-key-value *ngIf="osd.loaded"
- (fetchData)="osd.autoRefresh()"
- [data]="osd.details.osd_metadata">
- </cd-table-key-value>
- </tab>
- <tab heading="Performance counter">
- <cd-table-performance-counter *ngIf="osd.loaded"
- serviceType="osd"
- [serviceId]="osd.id">
- </cd-table-performance-counter>
- </tab>
- <tab heading="Histogram">
- <h3 *ngIf="osd.loaded && osd.histogram_failed">
- Histogram not available -> <span class="text-warning">{{ osd.histogram_failed }}</span>
- </h3>
- <div class="row" *ngIf="osd.loaded && osd.details.histogram">
- <div class="col-md-6">
- <h4>Writes</h4>
- <cd-osd-performance-histogram [histogram]="osd.details.histogram.osd.op_w_latency_in_bytes_histogram">
- </cd-osd-performance-histogram>
- </div>
- <div class="col-md-6">
- <h4>Reads</h4>
- <cd-osd-performance-histogram [histogram]="osd.details.histogram.osd.op_r_latency_out_bytes_histogram">
- </cd-osd-performance-histogram>
- </div>
- </div>
- </tab>
-</tabset>
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TabsModule } from 'ngx-bootstrap';
-
-import { DataTableModule } from '../../../../shared/datatable/datatable.module';
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
-import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
-import {
- OsdPerformanceHistogramComponent
-} from '../osd-performance-histogram/osd-performance-histogram.component';
-import { OsdService } from '../osd.service';
-import { OsdDetailsComponent } from './osd-details.component';
-
-describe('OsdDetailsComponent', () => {
- let component: OsdDetailsComponent;
- let fixture: ComponentFixture<OsdDetailsComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [
- HttpClientModule,
- TabsModule.forRoot(),
- PerformanceCounterModule,
- DataTableModule
- ],
- declarations: [
- OsdDetailsComponent,
- OsdPerformanceHistogramComponent
- ],
- providers: [OsdService]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(OsdDetailsComponent);
- component = fixture.componentInstance;
-
- component.selection = new CdTableSelection();
-
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, Input, OnChanges } from '@angular/core';
-
-import * as _ from 'lodash';
-
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
-import { OsdService } from '../osd.service';
-
-@Component({
- selector: 'cd-osd-details',
- templateUrl: './osd-details.component.html',
- styleUrls: ['./osd-details.component.scss']
-})
-export class OsdDetailsComponent implements OnChanges {
- @Input() selection: CdTableSelection;
-
- osd: any;
-
- constructor(private osdService: OsdService) {}
-
- ngOnChanges() {
- this.osd = {
- loaded: false
- };
- if (this.selection.hasSelection) {
- this.osd = this.selection.first();
- this.osd.autoRefresh = () => {
- this.refresh();
- };
- this.refresh();
- }
- }
-
- refresh() {
- this.osdService.getDetails(this.osd.tree.id)
- .subscribe((data: any) => {
- this.osd.details = data;
- if (!_.isObject(data.histogram)) {
- this.osd.histogram_failed = data.histogram;
- this.osd.details.histogram = undefined;
- }
- this.osd.loaded = true;
- });
- }
-}
+++ /dev/null
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item">Cluster</li>
- <li class="breadcrumb-item active">OSDs</li>
- </ol>
-</nav>
-<cd-table [data]="osds"
- (fetchData)="getOsdList()"
- [columns]="columns"
- selectionType="single"
- (updateSelection)="updateSelection($event)">
- <cd-osd-details cdTableDetail
- [selection]="selection">
- </cd-osd-details>
-</cd-table>
-
-<ng-template #statusColor
- let-value="value">
- <span *ngFor="let state of value; last as last">
- <span [class.text-success]="'up' === state || 'in' === state"
- [class.text-warning]="'down' === state || 'out' === state">
- {{ state }}</span><span *ngIf="!last">, </span>
- <!-- Has to be on the same line to prevent a space between state and comma. -->
- </span>
-</ng-template>
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { DataTableModule } from '../../../../shared/datatable/datatable.module';
-import { DimlessPipe } from '../../../../shared/pipes/dimless.pipe';
-import { FormatterService } from '../../../../shared/services/formatter.service';
-import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
-import { OsdDetailsComponent } from '../osd-details/osd-details.component';
-import {
- OsdPerformanceHistogramComponent
-} from '../osd-performance-histogram/osd-performance-histogram.component';
-import { OsdService } from '../osd.service';
-import { OsdListComponent } from './osd-list.component';
-
-describe('OsdListComponent', () => {
- let component: OsdListComponent;
- let fixture: ComponentFixture<OsdListComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [
- HttpClientModule,
- PerformanceCounterModule,
- TabsModule.forRoot(),
- DataTableModule
- ],
- declarations: [
- OsdListComponent,
- OsdDetailsComponent,
- OsdPerformanceHistogramComponent
- ],
- providers: [OsdService, DimlessPipe, FormatterService]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(OsdListComponent);
- component = fixture.componentInstance;
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
-
-import { CellTemplate } from '../../../../shared/enum/cell-template.enum';
-import { CdTableColumn } from '../../../../shared/models/cd-table-column';
-import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
-import { DimlessPipe } from '../../../../shared/pipes/dimless.pipe';
-import { OsdService } from '../osd.service';
-
-@Component({
- selector: 'cd-osd-list',
- templateUrl: './osd-list.component.html',
- styleUrls: ['./osd-list.component.scss']
-})
-
-export class OsdListComponent implements OnInit {
- @ViewChild('statusColor') statusColor: TemplateRef<any>;
-
- osds = [];
- columns: CdTableColumn[];
- selection = new CdTableSelection();
-
- constructor(
- private osdService: OsdService,
- private dimlessPipe: DimlessPipe
- ) { }
-
- ngOnInit() {
- this.columns = [
- {prop: 'host.name', name: 'Host'},
- {prop: 'id', name: 'ID', cellTransformation: CellTemplate.bold},
- {prop: 'collectedStates', name: 'Status', cellTemplate: this.statusColor},
- {prop: 'stats.numpg', name: 'PGs'},
- {prop: 'usedPercent', name: 'Usage'},
- {
- prop: 'stats_history.out_bytes',
- name: 'Read bytes',
- cellTransformation: CellTemplate.sparkline
- },
- {
- prop: 'stats_history.in_bytes',
- name: 'Writes bytes',
- cellTransformation: CellTemplate.sparkline
- },
- {prop: 'stats.op_r', name: 'Read ops', cellTransformation: CellTemplate.perSecond},
- {prop: 'stats.op_w', name: 'Write ops', cellTransformation: CellTemplate.perSecond}
- ];
- }
-
- updateSelection(selection: CdTableSelection) {
- this.selection = selection;
- }
-
- getOsdList() {
- this.osdService.getList().subscribe((data: any[]) => {
- this.osds = data;
- data.map((osd) => {
- osd.collectedStates = this.collectStates(osd);
- osd.stats_history.out_bytes = osd.stats_history.op_out_bytes.map(i => i[1]);
- osd.stats_history.in_bytes = osd.stats_history.op_in_bytes.map(i => i[1]);
- osd.usedPercent = this.dimlessPipe.transform(osd.stats.stat_bytes_used) + ' / ' +
- this.dimlessPipe.transform(osd.stats.stat_bytes);
- return osd;
- });
- });
- }
-
- collectStates(osd) {
- const select = (onState, offState) => osd[onState] ? onState : offState;
- return [select('up', 'down'), select('in', 'out')];
- }
-
- beforeShowDetails(selection: CdTableSelection) {
- return selection.hasSingleSelection;
- }
-}
+++ /dev/null
-<table>
- <tr style="height: 10px;"
- *ngFor="let row of valuesStyle">
- <td style="width: 10px; height: 10px;"
- *ngFor="let col of row"
- [ngStyle]="col">
- </td>
- </tr>
-</table>
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { OsdPerformanceHistogramComponent } from './osd-performance-histogram.component';
-
-describe('OsdPerformanceHistogramComponent', () => {
- let component: OsdPerformanceHistogramComponent;
- let fixture: ComponentFixture<OsdPerformanceHistogramComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ OsdPerformanceHistogramComponent ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(OsdPerformanceHistogramComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, Input, OnChanges } from '@angular/core';
-
-import * as _ from 'lodash';
-
-@Component({
- selector: 'cd-osd-performance-histogram',
- templateUrl: './osd-performance-histogram.component.html',
- styleUrls: ['./osd-performance-histogram.component.scss']
-})
-export class OsdPerformanceHistogramComponent implements OnChanges {
- @Input() histogram: any;
- valuesStyle: any;
- last = {};
-
- constructor() { }
-
- ngOnChanges() {
- this.render();
- }
-
- hexdigits(v): string {
- const i = Math.floor(v * 255).toString(16);
- return i.length === 1 ? '0' + i : i;
- }
-
- hexcolor(r, g, b) {
- return '#' + this.hexdigits(r) + this.hexdigits(g) + this.hexdigits(b);
- }
-
- render() {
- if (!this.histogram) {
- return;
- }
- let sum = 0;
- let max = 0;
-
- _.each(this.histogram.values, (row, i) => {
- _.each(row, (col, j) => {
- let val;
- if (this.last && this.last[i] && this.last[i][j]) {
- val = col - this.last[i][j];
- } else {
- val = col;
- }
- sum += val;
- max = Math.max(max, val);
- });
- });
-
- this.valuesStyle = this.histogram.values.map((row, i) => {
- return row.map((col, j) => {
- const val = this.last && this.last[i] && this.last[i][j] ? col - this.last[i][j] : col;
- const g = max ? val / max : 0;
- const r = 1 - g;
- return {backgroundColor: this.hexcolor(r, g, 0)};
- });
- });
-
- this.last = this.histogram.values;
- }
-}
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { OsdService } from './osd.service';
-
-describe('OsdService', () => {
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [OsdService],
- imports: [
- HttpClientModule,
- ],
- });
- });
-
- it('should be created', inject([OsdService], (service: OsdService) => {
- expect(service).toBeTruthy();
- }));
-});
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class OsdService {
- private path = 'api/osd';
-
- constructor (private http: HttpClient) {}
-
- getList () {
- return this.http.get(`${this.path}`);
- }
-
- getDetails(id: number) {
- return this.http.get(`${this.path}/${id}`);
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-
-import { ChartsModule } from 'ng2-charts';
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { SharedModule } from '../../shared/shared.module';
-import { DashboardService } from './dashboard.service';
-import { DashboardComponent } from './dashboard/dashboard.component';
-import { HealthPieComponent } from './health-pie/health-pie.component';
-import { HealthComponent } from './health/health.component';
-import { LogColorPipe } from './log-color.pipe';
-import { MdsSummaryPipe } from './mds-summary.pipe';
-import { MgrSummaryPipe } from './mgr-summary.pipe';
-import { MonSummaryPipe } from './mon-summary.pipe';
-import { OsdSummaryPipe } from './osd-summary.pipe';
-import { PgStatusStylePipe } from './pg-status-style.pipe';
-import { PgStatusPipe } from './pg-status.pipe';
-
-@NgModule({
- imports: [CommonModule, TabsModule.forRoot(), SharedModule, ChartsModule, RouterModule],
- declarations: [
- HealthComponent,
- DashboardComponent,
- MonSummaryPipe,
- OsdSummaryPipe,
- LogColorPipe,
- MgrSummaryPipe,
- PgStatusPipe,
- MdsSummaryPipe,
- PgStatusStylePipe,
- HealthPieComponent
- ],
- providers: [DashboardService]
-})
-export class DashboardModule {}
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { appendFile } from 'fs';
-
-import { DashboardService } from './dashboard.service';
-
-describe('DashboardService', () => {
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [DashboardService],
- imports: [HttpClientTestingModule, HttpClientModule]
- });
- });
-
- it(
- 'should be created',
- inject([DashboardService], (service: DashboardService) => {
- expect(service).toBeTruthy();
- })
- );
-});
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class DashboardService {
- constructor(private http: HttpClient) {}
-
- getHealth() {
- return this.http.get('api/dashboard/health');
- }
-}
+++ /dev/null
-<div>
- <tabset *ngIf="hasGrafana">
- <tab i18n-heading
- heading="Health">
- <cd-health></cd-health>
- </tab>
- <tab i18n-heading
- heading="Statistics">
- </tab>
- </tabset>
- <cd-health *ngIf="!hasGrafana"></cd-health>
-</div>
+++ /dev/null
-div {
- padding-top: 20px;
-}
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { DashboardComponent } from './dashboard.component';
-
-describe('DashboardComponent', () => {
- let component: DashboardComponent;
- let fixture: ComponentFixture<DashboardComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ DashboardComponent ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(DashboardComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- // it('should create', () => {
- // expect(component).toBeTruthy();
- // });
-});
+++ /dev/null
-import { Component, OnInit } from '@angular/core';
-
-@Component({
- selector: 'cd-dashboard',
- templateUrl: './dashboard.component.html',
- styleUrls: ['./dashboard.component.scss']
-})
-export class DashboardComponent implements OnInit {
- hasGrafana = false; // TODO: Temporary var, remove when grafana is implemented
-
- constructor() { }
-
- ngOnInit() {
- }
-
-}
+++ /dev/null
-<div class="chart-container">
- <canvas baseChart
- #chartCanvas
- [datasets]="chart.dataset"
- [chartType]="chart.chartType"
- [options]="chart.options"
- [labels]="chart.labels"
- [colors]="chart.colors"
- width="120"
- height="120"></canvas>
- <div class="chartjs-tooltip"
- #chartTooltip>
- <table></table>
- </div>
-</div>
+++ /dev/null
-@import '../../../../styles/chart-tooltip.scss';
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { ChartsModule } from 'ng2-charts/ng2-charts';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { HealthPieComponent } from './health-pie.component';
-
-describe('HealthPieComponent', () => {
- let component: HealthPieComponent;
- let fixture: ComponentFixture<HealthPieComponent>;
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- imports: [ChartsModule, SharedModule],
- declarations: [HealthPieComponent]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(HealthPieComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import {
- Component,
- ElementRef,
- EventEmitter,
- Input,
- OnChanges,
- OnInit,
- Output,
- ViewChild
-} from '@angular/core';
-
-import * as Chart from 'chart.js';
-import * as _ from 'lodash';
-
-import { ChartTooltip } from '../../../shared/models/chart-tooltip';
-import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
-
-@Component({
- selector: 'cd-health-pie',
- templateUrl: './health-pie.component.html',
- styleUrls: ['./health-pie.component.scss']
-})
-export class HealthPieComponent implements OnChanges, OnInit {
- @ViewChild('chartCanvas') chartCanvasRef: ElementRef;
- @ViewChild('chartTooltip') chartTooltipRef: ElementRef;
-
- @Input() data: any;
- @Input() tooltipFn: any;
- @Output() prepareFn = new EventEmitter();
-
- chart: any = {
- chartType: 'doughnut',
- dataset: [
- {
- label: null,
- borderWidth: 0
- }
- ],
- options: {
- responsive: true,
- legend: { display: false },
- animation: { duration: 0 },
-
- tooltips: {
- enabled: false
- }
- },
- colors: [
- {
- borderColor: 'transparent'
- }
- ]
- };
-
- constructor(private dimlessBinary: DimlessBinaryPipe) {}
-
- ngOnInit() {
- // An extension to Chart.js to enable rendering some
- // text in the middle of a doughnut
- Chart.pluginService.register({
- beforeDraw: function(chart) {
- if (!chart.options.center_text) {
- return;
- }
-
- const width = chart.chart.width,
- height = chart.chart.height,
- ctx = chart.chart.ctx;
-
- ctx.restore();
- const fontSize = (height / 114).toFixed(2);
- ctx.font = fontSize + 'em sans-serif';
- ctx.textBaseline = 'middle';
-
- const text = chart.options.center_text,
- textX = Math.round((width - ctx.measureText(text).width) / 2),
- textY = height / 2;
-
- ctx.fillText(text, textX, textY);
- ctx.save();
- }
- });
-
- const getStyleTop = (tooltip, positionY) => {
- return positionY + tooltip.caretY - tooltip.height - 10 + 'px';
- };
-
- const getStyleLeft = (tooltip, positionX) => {
- return positionX + tooltip.caretX + 'px';
- };
-
- const getBody = (body) => {
- const bodySplit = body[0].split(': ');
- bodySplit[1] = this.dimlessBinary.transform(bodySplit[1]);
- return bodySplit.join(': ');
- };
-
- const chartTooltip = new ChartTooltip(
- this.chartCanvasRef,
- this.chartTooltipRef,
- getStyleLeft,
- getStyleTop,
- );
- chartTooltip.getBody = getBody;
-
- const self = this;
- this.chart.options.tooltips.custom = (tooltip) => {
- chartTooltip.customTooltips(tooltip);
- };
-
- this.prepareFn.emit([this.chart, this.data]);
- }
-
- ngOnChanges() {
- this.prepareFn.emit([this.chart, this.data]);
- }
-}
+++ /dev/null
-<div *ngIf="contentData">
- <div class="row">
- <!-- HEALTH -->
- <div class="col-md-6">
- <div class="well">
- <fieldset>
- <legend i18n>Health</legend>
- <ng-container i18n>Overall status:</ng-container>
- <span [ngStyle]="contentData.health.status | healthColor">{{ contentData.health.status }}</span>
- <ul>
- <li *ngFor="let check of contentData.health.checks">
- <span [ngStyle]="check.severity | healthColor">{{ check.type }}</span>: {{ check.summary.message }}
- </li>
- </ul>
- </fieldset>
- </div>
- </div>
-
- <div class="col-md-6">
- <!--STATS -->
- <div class="row">
- <div class="col-md-6">
- <div class="well">
- <div class="media">
- <div class="media-left">
- <i class="fa fa-database fa-fw"></i>
- </div>
- <div class="media-body">
- <span class="media-heading"
- i18n="ceph monitors">
- <a routerLink="/monitor/">Monitors</a>
- </span>
- <span class="media-text">{{ contentData.mon_status | monSummary }}</span>
- </div>
- </div>
- </div>
- </div>
- <div class="col-md-6">
- <div class="well">
- <div class="media">
- <div class="media-left">
- <i class="fa fa-hdd-o fa-fw"></i>
- </div>
- <div class="media-body">
- <span class="media-heading"
- i18n="ceph OSDs">
- <a routerLink="/osd/">OSDs</a>
- </span>
- <span class="media-text">{{ contentData.osd_map | osdSummary }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="col-md-6">
- <div class="well">
- <div class="media">
- <div class="media-left">
- <i class="fa fa-folder fa-fw"></i>
- </div>
- <div class="media-body">
- <span class="media-heading"
- i18n>Metadata servers</span>
- <span class="media-text">{{ contentData.fs_map | mdsSummary }}</span>
- </div>
- </div>
- </div>
- </div>
- <div class="col-md-6">
- <div class="well">
- <div class="media">
- <div class="media-left">
- <i class="fa fa-cog fa-fw"></i>
- </div>
- <div class="media-body">
- <span class="media-heading"
- i18n>Manager daemons</span>
- <span class="media-text">{{ contentData.mgr_map | mgrSummary }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <div class="row">
- <!-- USAGE -->
- <div class="col-md-6">
- <div class="well">
- <fieldset class="usage">
- <legend i18n>Usage</legend>
-
- <table class="ceph-chartbox">
- <tr>
- <td>
- <span style="font-size: 45px;">{{ contentData.df.stats.total_objects | dimless }}</span>
- </td>
- <td>
- <div class="center-block pie">
- <cd-health-pie [data]="contentData"
- (prepareFn)="prepareRawUsage($event[0], $event[1])"></cd-health-pie>
- </div>
- </td>
- <td>
- <div class="center-block pie">
- <cd-health-pie [data]="contentData"
- (prepareFn)="preparePoolUsage($event[0], $event[1])"></cd-health-pie>
- </div>
- </td>
- </tr>
- <tr>
- <td i18n>Objects</td>
- <td>
- <ng-container i18n>Raw capacity</ng-container>
- <br>
- <ng-container i18n="disk used">({{ contentData.df.stats.total_used_bytes | dimlessBinary }} used)</ng-container>
- </td>
- <td i18n>Usage by pool</td>
- </tr>
- </table>
- </fieldset>
- </div>
- </div>
-
- <div class="col-md-6">
- <div class="well">
- <fieldset>
- <legend i18n>Pools</legend>
- <table class="table table-condensed">
- <thead>
- <tr>
- <th i18n>Name</th>
- <th i18n>PG status</th>
- <th i18n>Usage</th>
- <th colspan="2"
- i18n>Read</th>
- <th colspan="2"
- i18n>Write</th>
- </tr>
- </thead>
- <tbody>
- <tr *ngFor="let pool of contentData.pools">
- <td>{{ pool.pool_name }}</td>
- <td [ngStyle]="pool.pg_status | pgStatusStyle">
- {{ pool.pg_status | pgStatus }}
- </td>
- <td>
- {{ pool.stats.bytes_used.latest | dimlessBinary }} / {{ pool.stats.max_avail.latest | dimlessBinary }}
- </td>
- <td>
- {{ pool.stats.rd_bytes.rate | dimless }}
- </td>
- <td>
- {{ pool.stats.rd.rate | dimless }} ops
- </td>
- <td>
- {{ pool.stats.wr_bytes.rate | dimless }}
- </td>
- <td>
- {{ pool.stats.wr.rate | dimless }} ops
- </td>
- </tr>
- </tbody>
- </table>
- </fieldset>
- </div>
- </div>
- </div>
-
- <div class="row">
- <div class="col-md-12">
- <!-- LOGS -->
- <div class="well">
- <fieldset>
- <legend i18n>Logs</legend>
-
- <tabset>
- <tab heading="Cluster log"
- class="text-monospace"
- i18n-heading>
- <span *ngFor="let line of contentData.clog">
- {{ line.stamp }} {{ line.priority }}
- <span [ngStyle]="line | logColor">
- {{ line.message }}
- <br>
- </span>
- </span>
- </tab>
- <tab heading="Audit log"
- class="text-monospace"
- i18n-heading>
- <span *ngFor="let line of contentData.audit_log">
- {{ line.stamp }} {{ line.priority }}
- <span [ngStyle]="line | logColor">
- <span style="font-weight: bold;">
- {{ line.message }}
- </span>
- <br>
- </span>
- </span>
- </tab>
- </tabset>
- </fieldset>
- </div>
- </div>
- </div>
-</div>
+++ /dev/null
-table.ceph-chartbox {
- width: 100%;
-
- td {
- text-align: center;
- font-weight: bold;
- }
-}
-
-.center-block {
- width: 120px;
-}
-
-.pie {
- height: 120px;
- width: 120px;
-}
-
-.media {
- display: block;
- min-height: 60px;
- width: 100%;
-
- .media-left {
- border-top-left-radius: 2px;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- border-bottom-left-radius: 2px;
- display: block;
- float: left;
- height: 60px;
- width: 60px;
- text-align: center;
- font-size: 40px;
- line-height: 60px;
- padding-right: 0;
-
- .fa {
- font-size: 45px;
- }
- }
-
- .media-body {
- padding: 5px 10px;
- margin-left: 60px;
-
- .media-heading {
- text-transform: uppercase;
- display: block;
- font-size: 14px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .media-text {
- display: block;
- font-weight: bold;
- font-size: 18px;
- }
- }
-}
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { DashboardService } from '../dashboard.service';
-import { HealthComponent } from './health.component';
-
-describe('HealthComponent', () => {
- let component: HealthComponent;
- let fixture: ComponentFixture<HealthComponent>;
-
- const fakeService = {
- getHealth() {
- return {};
- }
- };
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- providers: [{ provide: DashboardService, useValue: fakeService }],
- imports: [SharedModule],
- declarations: [HealthComponent]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(HealthComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-});
+++ /dev/null
-import { Component, OnDestroy, OnInit } from '@angular/core';
-
-import * as _ from 'lodash';
-
-import { DashboardService } from '../dashboard.service';
-
-@Component({
- selector: 'cd-health',
- templateUrl: './health.component.html',
- styleUrls: ['./health.component.scss']
-})
-export class HealthComponent implements OnInit, OnDestroy {
- contentData: any;
- interval: number;
-
- constructor(private dashboardService: DashboardService) {}
-
- ngOnInit() {
- this.getInfo();
- this.interval = window.setInterval(() => {
- this.getInfo();
- }, 5000);
- }
-
- ngOnDestroy() {
- clearInterval(this.interval);
- }
-
- getInfo() {
- this.dashboardService.getHealth().subscribe((data: any) => {
- this.contentData = data;
- });
- }
-
- prepareRawUsage(chart, data) {
- let rawUsageChartColor;
-
- const rawUsageText =
- Math.round(100 * (data.df.stats.total_used_bytes / data.df.stats.total_bytes)) + '%';
-
- if (data.df.stats.total_used_bytes / data.df.stats.total_bytes >= data.osd_map.full_ratio) {
- rawUsageChartColor = '#ff0000';
- } else if (
- data.df.stats.total_used_bytes / data.df.stats.total_bytes >=
- data.osd_map.backfillfull_ratio
- ) {
- rawUsageChartColor = '#ff6600';
- } else if (
- data.df.stats.total_used_bytes / data.df.stats.total_bytes >=
- data.osd_map.nearfull_ratio
- ) {
- rawUsageChartColor = '#ffc200';
- } else {
- rawUsageChartColor = '#00bb00';
- }
-
- chart.dataset[0].data = [data.df.stats.total_used_bytes, data.df.stats.total_avail_bytes];
- chart.options.center_text = rawUsageText;
- chart.colors = [{ backgroundColor: [rawUsageChartColor, '#424d52'] }];
- chart.labels = ['Raw Used', 'Raw Available'];
- }
-
- preparePoolUsage(chart, data) {
- const colors = [
- '#3366CC',
- '#109618',
- '#990099',
- '#3B3EAC',
- '#0099C6',
- '#DD4477',
- '#66AA00',
- '#B82E2E',
- '#316395',
- '#994499',
- '#22AA99',
- '#AAAA11',
- '#6633CC',
- '#E67300',
- '#8B0707',
- '#329262',
- '#5574A6',
- '#FF9900',
- '#DC3912',
- '#3B3EAC'
- ];
-
- const poolLabels = [];
- const poolData = [];
-
- _.each(data.df.pools, (pool, i) => {
- poolLabels.push(pool['name']);
- poolData.push(pool['stats']['bytes_used']);
- });
-
- chart.dataset[0].data = poolData;
- chart.colors = [{ backgroundColor: colors }];
- chart.labels = poolLabels;
- }
-}
+++ /dev/null
-import { LogColorPipe } from './log-color.pipe';
-
-describe('LogColorPipe', () => {
- it('create an instance', () => {
- const pipe = new LogColorPipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
- name: 'logColor'
-})
-export class LogColorPipe implements PipeTransform {
- transform(value: any, args?: any): any {
- if (value.priority === '[INF]') {
- return ''; // Inherit
- } else if (value.priority === '[WRN]') {
- return {
- color: '#ffa500',
- 'font-weight': 'bold'
- };
- } else if (value.priority === '[ERR]') {
- return { color: '#FF2222' };
- } else {
- return '';
- }
- }
-}
+++ /dev/null
-import { MdsSummaryPipe } from './mds-summary.pipe';
-
-describe('MdsSummaryPipe', () => {
- it('create an instance', () => {
- const pipe = new MdsSummaryPipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-import * as _ from 'lodash';
-
-@Pipe({
- name: 'mdsSummary'
-})
-export class MdsSummaryPipe implements PipeTransform {
- transform(value: any, args?: any): any {
- if (!value) {
- return '';
- }
-
- let standbys = 0;
- let active = 0;
- let standbyReplay = 0;
- _.each(value.standbys, (s, i) => {
- standbys += 1;
- });
-
- if (value.standbys && !value.filesystems) {
- return standbys + ', no filesystems';
- } else if (value.filesystems.length === 0) {
- return 'no filesystems';
- } else {
- _.each(value.filesystems, (fs, i) => {
- _.each(fs.mdsmap.info, (mds, j) => {
- if (mds.state === 'up:standby-replay') {
- standbyReplay += 1;
- } else {
- active += 1;
- }
- });
- });
-
- return active + ' active, ' + (standbys + standbyReplay) + ' standby';
- }
- }
-}
+++ /dev/null
-import { MgrSummaryPipe } from './mgr-summary.pipe';
-
-describe('MgrSummaryPipe', () => {
- it('create an instance', () => {
- const pipe = new MgrSummaryPipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-import * as _ from 'lodash';
-
-@Pipe({
- name: 'mgrSummary'
-})
-export class MgrSummaryPipe implements PipeTransform {
- transform(value: any, args?: any): any {
- if (!value) {
- return '';
- }
-
- let result = 'active: ';
- result += _.isUndefined(value.active_name) ? 'n/a' : value.active_name;
-
- if (value.standbys.length) {
- result += ', ' + value.standbys.length + ' standbys';
- }
-
- return result;
- }
-}
+++ /dev/null
-import { MonSummaryPipe } from './mon-summary.pipe';
-
-describe('MonSummaryPipe', () => {
- it('create an instance', () => {
- const pipe = new MonSummaryPipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
- name: 'monSummary'
-})
-export class MonSummaryPipe implements PipeTransform {
- transform(value: any, args?: any): any {
- if (!value) {
- return '';
- }
-
- let result = value.monmap.mons.length.toString() + ' (quorum ';
- result += value.quorum.join(', ');
- result += ')';
-
- return result;
- }
-}
+++ /dev/null
-import { OsdSummaryPipe } from './osd-summary.pipe';
-
-describe('OsdSummaryPipe', () => {
- it('create an instance', () => {
- const pipe = new OsdSummaryPipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-import * as _ from 'lodash';
-
-@Pipe({
- name: 'osdSummary'
-})
-export class OsdSummaryPipe implements PipeTransform {
- transform(value: any, args?: any): any {
- if (!value) {
- return '';
- }
-
- let inCount = 0;
- let upCount = 0;
- _.each(value.osds, (osd, i) => {
- if (osd.in) {
- inCount++;
- }
- if (osd.up) {
- upCount++;
- }
- });
-
- return value.osds.length + ' (' + upCount + ' up, ' + inCount + ' in)';
- }
-}
+++ /dev/null
-import { PgStatusStylePipe } from './pg-status-style.pipe';
-
-describe('PgStatusStylePipe', () => {
- it('create an instance', () => {
- const pipe = new PgStatusStylePipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-import * as _ from 'lodash';
-
-@Pipe({
- name: 'pgStatusStyle'
-})
-export class PgStatusStylePipe implements PipeTransform {
- transform(pgStatus: any, args?: any): any {
- let warning = false;
- let error = false;
-
- _.each(pgStatus, (value, state) => {
- if (
- state.includes('inconsistent') ||
- state.includes('incomplete') ||
- !state.includes('active')
- ) {
- error = true;
- }
-
- if (
- state !== 'active+clean' &&
- state !== 'active+clean+scrubbing' &&
- state !== 'active+clean+scrubbing+deep'
- ) {
- warning = true;
- }
- });
-
- if (error) {
- return { color: '#FF0000' };
- }
-
- if (warning) {
- return { color: '#FFC200' };
- }
-
- return { color: '#00BB00' };
- }
-}
+++ /dev/null
-import { PgStatusPipe } from './pg-status.pipe';
-
-describe('PgStatusPipe', () => {
- it('create an instance', () => {
- const pipe = new PgStatusPipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-import * as _ from 'lodash';
-
-@Pipe({
- name: 'pgStatus'
-})
-export class PgStatusPipe implements PipeTransform {
- transform(pgStatus: any, args?: any): any {
- const strings = [];
- _.each(pgStatus, (count, state) => {
- strings.push(count + ' ' + state);
- });
-
- return strings.join(', ');
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-
-import { SharedModule } from '../../shared/shared.module';
-import {
- PerformanceCounterComponent
-} from './performance-counter/performance-counter.component';
-import { TablePerformanceCounterService } from './services/table-performance-counter.service';
-import {
- TablePerformanceCounterComponent
-} from './table-performance-counter/table-performance-counter.component';
-
-@NgModule({
- imports: [
- CommonModule,
- SharedModule,
- RouterModule
- ],
- declarations: [
- TablePerformanceCounterComponent,
- PerformanceCounterComponent
- ],
- providers: [
- TablePerformanceCounterService
- ],
- exports: [
- TablePerformanceCounterComponent
- ]
-})
-export class PerformanceCounterModule { }
+++ /dev/null
-<fieldset>
- <legend i18n>Performance Counters</legend>
- <h3>{{ serviceType }}.{{ serviceId }}</h3>
- <cd-table-performance-counter [serviceType]="serviceType"
- [serviceId]="serviceId">
- </cd-table-performance-counter>
-</fieldset>
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { BsDropdownModule } from 'ngx-bootstrap';
-
-import { PerformanceCounterModule } from '../performance-counter.module';
-import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
-import { PerformanceCounterComponent } from './performance-counter.component';
-
-describe('PerformanceCounterComponent', () => {
- let component: PerformanceCounterComponent;
- let fixture: ComponentFixture<PerformanceCounterComponent>;
-
- const fakeService = {
- get: (service_type: string, service_id: string) => {
- return new Promise(function(resolve, reject) {
- return [];
- });
- },
- list: () => {
- return new Promise(function(resolve, reject) {
- return {};
- });
- }
- };
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- imports: [
- PerformanceCounterModule,
- BsDropdownModule.forRoot(),
- RouterTestingModule
- ],
- providers: [{ provide: TablePerformanceCounterService, useValue: fakeService }]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(PerformanceCounterComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnDestroy } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-
-@Component({
- selector: 'cd-performance-counter',
- templateUrl: './performance-counter.component.html',
- styleUrls: ['./performance-counter.component.scss']
-})
-export class PerformanceCounterComponent implements OnDestroy {
- serviceId: string;
- serviceType: string;
- routeParamsSubscribe: any;
-
- constructor(private route: ActivatedRoute) {
- this.routeParamsSubscribe = this.route.params.subscribe(
- (params: { type: string; id: string }) => {
- this.serviceId = params.id;
- this.serviceType = params.type;
- }
- );
- }
-
- ngOnDestroy() {
- this.routeParamsSubscribe.unsubscribe();
- }
-}
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { BsDropdownModule } from 'ngx-bootstrap';
-
-import { TablePerformanceCounterService } from './table-performance-counter.service';
-
-describe('TablePerformanceCounterService', () => {
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [TablePerformanceCounterService],
- imports: [
- HttpClientTestingModule,
- BsDropdownModule.forRoot(),
- HttpClientModule
- ]
- });
- });
-
- it(
- 'should be created',
- inject([TablePerformanceCounterService], (service: TablePerformanceCounterService) => {
- expect(service).toBeTruthy();
- })
- );
-});
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class TablePerformanceCounterService {
-
- private url = 'api/perf_counters';
-
- constructor(private http: HttpClient) { }
-
- list() {
- return this.http.get(this.url)
- .toPromise()
- .then((resp: object): object => {
- return resp;
- });
- }
-
- get(service_type: string, service_id: string) {
- const serviceType = service_type.replace('-', '_');
-
- return this.http.get(`${this.url}/${serviceType}/${service_id}`)
- .toPromise()
- .then((resp: object): Array<object> => {
- return resp['counters'];
- });
- }
-}
+++ /dev/null
-<cd-table [data]="counters"
- [columns]="columns"
- columnMode="flex"
- (fetchData)="getCounters()">
- <ng-template #valueTpl let-row="row">
- {{ row.value | dimless }} {{ row.unit }}
- </ng-template>
-</cd-table>
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { BsDropdownModule } from 'ngx-bootstrap';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
-import { TablePerformanceCounterComponent } from './table-performance-counter.component';
-
-describe('TablePerformanceCounterComponent', () => {
- let component: TablePerformanceCounterComponent;
- let fixture: ComponentFixture<TablePerformanceCounterComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ TablePerformanceCounterComponent ],
- imports: [
- HttpClientTestingModule,
- HttpClientModule,
- BsDropdownModule.forRoot(),
- SharedModule
- ],
- providers: [ TablePerformanceCounterService ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(TablePerformanceCounterComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
-
-import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { TablePerformanceCounterService } from '../services/table-performance-counter.service';
-
-/**
- * Display the specified performance counters in a datatable.
- */
-@Component({
- selector: 'cd-table-performance-counter',
- templateUrl: './table-performance-counter.component.html',
- styleUrls: ['./table-performance-counter.component.scss']
-})
-export class TablePerformanceCounterComponent implements OnInit {
-
- columns: Array<CdTableColumn> = [];
- counters: Array<object> = [];
-
- @ViewChild('valueTpl') public valueTpl: TemplateRef<any>;
-
- /**
- * The service type, e.g. 'rgw', 'mds', 'mon', 'osd', ...
- */
- @Input() serviceType: string;
-
- /**
- * The service identifier.
- */
- @Input() serviceId: string;
-
- constructor(private performanceCounterService: TablePerformanceCounterService) { }
-
- ngOnInit() {
- this.columns = [
- {
- name: 'Name',
- prop: 'name',
- flexGrow: 1
- },
- {
- name: 'Description',
- prop: 'description',
- flexGrow: 1
- },
- {
- name: 'Value',
- cellTemplate: this.valueTpl,
- flexGrow: 1
- }
- ];
- }
-
- getCounters() {
- this.performanceCounterService.get(this.serviceType, this.serviceId)
- .then((resp) => {
- this.counters = resp;
- });
- }
-}
+++ /dev/null
-<tabset *ngIf="selection.hasSingleSelection">
- <tab i18n-heading
- heading="Details">
- <cd-table-key-value [data]="metadata"
- (fetchData)="getMetaData()">
- </cd-table-key-value>
- </tab>
- <tab i18n-heading
- heading="Performance Counters">
- <cd-table-performance-counter serviceType="rgw"
- [serviceId]="serviceId">
- </cd-table-performance-counter>
- </tab>
-</tabset>
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
-import { SharedModule } from '../../../shared/shared.module';
-import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module';
-import { RgwDaemonService } from '../services/rgw-daemon.service';
-import { RgwDaemonDetailsComponent } from './rgw-daemon-details.component';
-
-describe('RgwDaemonDetailsComponent', () => {
- let component: RgwDaemonDetailsComponent;
- let fixture: ComponentFixture<RgwDaemonDetailsComponent>;
-
- const fakeService = {
- get: (id: string) => {
- return new Promise(function(resolve, reject) {
- return [];
- });
- }
- };
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ RgwDaemonDetailsComponent ],
- imports: [
- SharedModule,
- PerformanceCounterModule,
- TabsModule.forRoot()
- ],
- providers: [{ provide: RgwDaemonService, useValue: fakeService }]
- });
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(RgwDaemonDetailsComponent);
- component = fixture.componentInstance;
-
- component.selection = new CdTableSelection();
-
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, Input, OnChanges } from '@angular/core';
-
-import * as _ from 'lodash';
-
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
-import { RgwDaemonService } from '../services/rgw-daemon.service';
-
-@Component({
- selector: 'cd-rgw-daemon-details',
- templateUrl: './rgw-daemon-details.component.html',
- styleUrls: ['./rgw-daemon-details.component.scss']
-})
-export class RgwDaemonDetailsComponent implements OnChanges {
- metadata: any;
- serviceId = '';
-
- @Input() selection: CdTableSelection;
-
- constructor(private rgwDaemonService: RgwDaemonService) {}
-
- ngOnChanges() {
- // Get the service id of the first selected row.
- if (this.selection.hasSelection) {
- this.serviceId = this.selection.first().id;
- }
- }
-
- getMetaData() {
- if (_.isEmpty(this.serviceId)) {
- return;
- }
- this.rgwDaemonService.get(this.serviceId).then(resp => {
- this.metadata = resp['rgw_metadata'];
- });
- }
-}
+++ /dev/null
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">Object Gateway</li>
- </ol>
-</nav>
-
-<cd-table [data]="daemons"
- [columns]="columns"
- columnMode="flex"
- selectionType="single"
- (updateSelection)="updateSelection($event)"
- (fetchData)="getDaemonList()">
- <cd-rgw-daemon-details cdTableDetail
- [selection]="selection">
- </cd-rgw-daemon-details>
-</cd-table>
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { DataTableModule } from '../../../shared/datatable/datatable.module';
-import { PerformanceCounterModule } from '../../performance-counter/performance-counter.module';
-import { RgwDaemonDetailsComponent } from '../rgw-daemon-details/rgw-daemon-details.component';
-import { RgwDaemonService } from '../services/rgw-daemon.service';
-import { RgwDaemonListComponent } from './rgw-daemon-list.component';
-
-describe('RgwDaemonListComponent', () => {
- let component: RgwDaemonListComponent;
- let fixture: ComponentFixture<RgwDaemonListComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ RgwDaemonListComponent, RgwDaemonDetailsComponent ],
- imports: [
- DataTableModule,
- HttpClientTestingModule,
- HttpClientModule,
- TabsModule.forRoot(),
- PerformanceCounterModule
- ],
- providers: [ RgwDaemonService ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(RgwDaemonListComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component } from '@angular/core';
-
-import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { CdTableSelection } from '../../../shared/models/cd-table-selection';
-import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe';
-import { RgwDaemonService } from '../services/rgw-daemon.service';
-
-@Component({
- selector: 'cd-rgw-daemon-list',
- templateUrl: './rgw-daemon-list.component.html',
- styleUrls: ['./rgw-daemon-list.component.scss']
-})
-export class RgwDaemonListComponent {
-
- columns: Array<CdTableColumn> = [];
- daemons: Array<object> = [];
- selection = new CdTableSelection();
-
- constructor(private rgwDaemonService: RgwDaemonService,
- cephShortVersionPipe: CephShortVersionPipe) {
- this.columns = [
- {
- name: 'ID',
- prop: 'id',
- flexGrow: 2
- },
- {
- name: 'Hostname',
- prop: 'server_hostname',
- flexGrow: 2
- },
- {
- name: 'Version',
- prop: 'version',
- flexGrow: 1,
- pipe: cephShortVersionPipe
- }
- ];
- }
-
- getDaemonList() {
- this.rgwDaemonService.list()
- .then((resp) => {
- this.daemons = resp;
- });
- }
-
- updateSelection(selection: CdTableSelection) {
- this.selection = selection;
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { TabsModule } from 'ngx-bootstrap/tabs';
-
-import { SharedModule } from '../../shared/shared.module';
-import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
-import { RgwDaemonDetailsComponent } from './rgw-daemon-details/rgw-daemon-details.component';
-import { RgwDaemonListComponent } from './rgw-daemon-list/rgw-daemon-list.component';
-import { RgwDaemonService } from './services/rgw-daemon.service';
-
-@NgModule({
- entryComponents: [
- RgwDaemonDetailsComponent
- ],
- imports: [
- CommonModule,
- SharedModule,
- PerformanceCounterModule,
- TabsModule.forRoot()
- ],
- exports: [
- RgwDaemonListComponent,
- RgwDaemonDetailsComponent
- ],
- declarations: [
- RgwDaemonListComponent,
- RgwDaemonDetailsComponent
- ],
- providers: [
- RgwDaemonService
- ]
-})
-export class RgwModule { }
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { RgwDaemonService } from './rgw-daemon.service';
-
-describe('RgwDaemonService', () => {
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [RgwDaemonService],
- imports: [HttpClientTestingModule, HttpClientModule]
- });
- });
-
- it(
- 'should be created',
- inject([RgwDaemonService], (service: RgwDaemonService) => {
- expect(service).toBeTruthy();
- })
- );
-});
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class RgwDaemonService {
-
- private url = 'api/rgw/daemon';
-
- constructor(private http: HttpClient) { }
-
- list() {
- return this.http.get(this.url)
- .toPromise()
- .then((resp: any) => {
- return resp;
- });
- }
-
- get(id: string) {
- return this.http.get(`${this.url}/${id}`)
- .toPromise()
- .then((resp: any) => {
- return resp;
- });
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { SharedModule } from '../../shared/shared.module';
-
-import { LoginComponent } from './login/login.component';
-import { LogoutComponent } from './logout/logout.component';
-
-@NgModule({
- imports: [
- CommonModule,
- FormsModule,
- SharedModule
- ],
- declarations: [LoginComponent, LogoutComponent],
- exports: [LogoutComponent]
-})
-export class AuthModule { }
+++ /dev/null
-<div class="login">
- <div class="row full-height vertical-align">
- <div class="col-sm-6 hidden-xs">
- <img src="assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png"
- alt="Ceph"
- class="pull-right">
- </div>
- <div class="col-xs-10 col-sm-4 col-lg-3 col-xs-offset-1 col-sm-offset-0 col-md-offset-0 col-lg-offset-0">
- <h1 i18n="The welcome message on the login page">Welcome to Ceph!</h1>
- <form name="loginForm"
- (ngSubmit)="login()"
- #loginForm="ngForm"
- novalidate>
-
- <!-- Username -->
- <div class="form-group has-feedback"
- [ngClass]="{'has-error': (loginForm.submitted || username.dirty) && username.invalid}">
- <input name="username"
- [(ngModel)]="model.username"
- #username="ngModel"
- type="text"
- placeholder="Enter your username..."
- class="form-control"
- required
- autofocus>
- <div class="help-block"
- *ngIf="(loginForm.submitted || username.dirty) && username.invalid">Username is required</div>
- </div>
-
- <!-- Password -->
- <div class="form-group has-feedback"
- [ngClass]="{'has-error': (loginForm.submitted || password.dirty) && password.invalid}">
- <div class="input-group">
- <input id="password"
- name="password"
- [(ngModel)]="model.password"
- #password="ngModel"
- type="password"
- placeholder="Enter your password..."
- class="form-control"
- required>
- <span class="input-group-btn">
- <button type="button"
- class="btn btn-default btn-password"
- cdPasswordButton="password">
- </button>
- </span>
- </div>
- <div class="help-block"
- *ngIf="(loginForm.submitted || password.dirty) && password.invalid">Password is required
- </div>
- </div>
-
- <!-- Stay signed in -->
- <div class="checkbox checkbox-primary">
- <input id="stay_signed_in"
- name="stay_signed_in"
- type="checkbox"
- [(ngModel)]="model.stay_signed_in">
- <label for="stay_signed_in"
- i18n="A checkbox on the login page to do not expire session on browser close">
- Keep me logged in
- </label>
- </div>
-
- <input type="submit"
- class="btn btn-openattic btn-block"
- [disabled]="loginForm.invalid"
- value="Login">
- </form>
- </div>
- </div>
-</div>
+++ /dev/null
-@import '../../../../defaults';
-
-.login {
- height: 100%;
-
- .row {
- color: #ececec;
- background-color: #474544;
- }
-
- h1 {
- margin-top: 0;
- margin-bottom: 30px;
- }
-
- .btn-password,
- .form-control {
- color: #ececec;
- background-color: #555555;
- }
-
- .btn-password:focus {
- outline-color: #66afe9;
- }
-
- .checkbox-primary input[type="checkbox"]:checked + label::before,
- .checkbox-primary input[type="radio"]:checked + label::before {
- background-color: $oa-color-blue;
- border-color: $oa-color-blue;
- }
-}
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormsModule } from '@angular/forms';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { ToastModule } from 'ng2-toastr';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { LoginComponent } from './login.component';
-
-describe('LoginComponent', () => {
- let component: LoginComponent;
- let fixture: ComponentFixture<LoginComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [
- FormsModule,
- SharedModule,
- RouterTestingModule,
- HttpClientTestingModule,
- ToastModule.forRoot()
- ],
- declarations: [
- LoginComponent
- ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(LoginComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnInit, ViewContainerRef } from '@angular/core';
-import { Router } from '@angular/router';
-
-import { ToastsManager } from 'ng2-toastr';
-
-import { Credentials } from '../../../shared/models/credentials';
-import { AuthStorageService } from '../../../shared/services/auth-storage.service';
-import { AuthService } from '../../../shared/services/auth.service';
-
-@Component({
- selector: 'cd-login',
- templateUrl: './login.component.html',
- styleUrls: ['./login.component.scss']
-})
-export class LoginComponent implements OnInit {
-
- model = new Credentials();
-
- constructor(private authService: AuthService,
- private authStorageService: AuthStorageService,
- private router: Router,
- public toastr: ToastsManager,
- private vcr: ViewContainerRef) {
- this.toastr.setRootViewContainerRef(vcr);
- }
-
- ngOnInit() {
- if (this.authStorageService.isLoggedIn()) {
- this.router.navigate(['']);
- }
- }
-
- login() {
- this.authService.login(this.model).then(() => {
- this.router.navigate(['']);
- });
- }
-}
+++ /dev/null
-<a i18n-title
- title="Sign Out"
- (click)="logout()">
- <i class="fa fa-sign-out"></i>
- <ng-container i18n>Logout</ng-container>
-</a>
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { LogoutComponent } from './logout.component';
-
-describe('LogoutComponent', () => {
- let component: LogoutComponent;
- let fixture: ComponentFixture<LogoutComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [
- SharedModule,
- RouterTestingModule,
- HttpClientTestingModule
- ],
- declarations: [
- LogoutComponent
- ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(LogoutComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnInit } from '@angular/core';
-import { Router } from '@angular/router';
-
-import { AuthService } from '../../../shared/services/auth.service';
-
-@Component({
- selector: 'cd-logout',
- templateUrl: './logout.component.html',
- styleUrls: ['./logout.component.scss']
-})
-export class LogoutComponent implements OnInit {
-
- constructor(private authService: AuthService,
- private router: Router) { }
-
- ngOnInit() {
- }
-
- logout() {
- this.authService.logout().then(() => {
- this.router.navigate(['/login']);
- });
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { AuthModule } from './auth/auth.module';
-import { NavigationModule } from './navigation/navigation.module';
-import { NotFoundComponent } from './not-found/not-found.component';
-
-@NgModule({
- imports: [
- CommonModule,
- NavigationModule,
- AuthModule
- ],
- exports: [NavigationModule],
- declarations: [NotFoundComponent]
-})
-export class CoreModule { }
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-
-import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
-
-import { AppRoutingModule } from '../../app-routing.module';
-import { SharedModule } from '../../shared/shared.module';
-import { AuthModule } from '../auth/auth.module';
-import { NavigationComponent } from './navigation/navigation.component';
-
-@NgModule({
- imports: [
- CommonModule,
- AuthModule,
- BsDropdownModule.forRoot(),
- AppRoutingModule,
- SharedModule,
- RouterModule
- ],
- declarations: [NavigationComponent],
- exports: [NavigationComponent]
-})
-export class NavigationModule {}
+++ /dev/null
-<nav class="navbar navbar-default navbar-openattic">
- <!-- Brand and toggle get grouped for better mobile display -->
-
- <div class="navbar-header tc_logo_component">
- <a class="navbar-brand"
- href="#">
- <img src="assets/Ceph_Logo_Standard_RGB_White_120411_fa.png"
- alt="Ceph">
- </a>
-
- <button type="button"
- class="navbar-toggle collapsed"
- data-toggle="collapse"
- data-target="#bs-example-navbar-collapse-1">
- <span i18n
- class="sr-only">Toggle navigation
- </span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </button>
- </div>
-
- <!-- Collect the nav links, forms, and other content for toggling -->
- <div class="collapse navbar-collapse"
- id="bs-example-navbar-collapse-1">
- <ul class="nav navbar-nav navbar-primary">
-
- <!-- Dashboard -->
- <li routerLinkActive="active"
- class="tc_menuitem tc_menuitem_dashboard">
- <a i18n
- routerLink="/dashboard">
- <i class="fa fa-heartbeat fa-fw"
- [ngStyle]="summaryData?.health_status | healthColor"></i>
- <span>Dashboard</span>
- </a>
- </li>
-
- <!-- Cluster -->
- <li dropdown
- routerLinkActive="active"
- class="dropdown tc_menuitem tc_menuitem_cluster">
- <a dropdownToggle
- class="dropdown-toggle"
- data-toggle="dropdown">
- <ng-container i18n>Cluster</ng-container>
- <span class="caret"></span>
- </a>
- <ul *dropdownMenu
- class="dropdown-menu">
- <li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_hosts">
- <a i18n
- class="dropdown-item"
- routerLink="/hosts">Hosts
- </a>
- </li>
- <li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_cluster_monitor">
- <a i18n
- class="dropdown-item"
- routerLink="/monitor/"> Monitors
- </a>
- </li>
- <li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_hosts">
- <a i18n
- class="dropdown-item"
- routerLink="/osd">OSDs
- </a>
- </li>
- <li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_configuration">
- <a i18n
- class="dropdown-item"
- routerLink="/configuration">Configuration Doc.
- </a>
- </li>
- </ul>
- </li>
-
- <!-- Block -->
- <li dropdown
- routerLinkActive="active"
- class="dropdown tc_menuitem tc_menuitem_block">
- <a dropdownToggle
- class="dropdown-toggle"
- data-toggle="dropdown"
- [ngStyle]="blockHealthColor()">
- <ng-container i18n>Block</ng-container>
- <span class="caret"></span>
- </a>
-
- <ul class="dropdown-menu">
- <li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_block_mirroring">
- <a i18n
- class="dropdown-item"
- routerLink="/mirroring/"> Mirroring
- <small *ngIf="summaryData?.rbd_mirroring?.warnings !== 0"
- class="label label-warning">{{ summaryData?.rbd_mirroring?.warnings }}</small>
- <small *ngIf="summaryData?.rbd_mirroring?.errors !== 0"
- class="label label-danger">{{ summaryData?.rbd_mirroring?.errors }}</small>
- </a>
- </li>
-
- <li routerLinkActive="active">
- <a i18n
- class="dropdown-item"
- routerLink="/block/iscsi">iSCSI</a>
- </li>
-
- <li class="dropdown-submenu">
- <a class="dropdown-toggle"
- data-toggle="dropdown">Pools</a>
- <ul *dropdownMenu
- class="dropdown-menu">
- <li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_pools"
- *ngFor="let rbdPool of rbdPools">
- <a i18n
- class="dropdown-item"
- routerLink="/block/pool/{{ rbdPool }}">{{ rbdPool }}
- </a>
- </li>
- <li class="tc_submenuitem tc_submenuitem_cephfs_nofs"
- *ngIf="rbdPools.length === 0">
- <a class="dropdown-item disabled"
- i18n>There are no pools</a>
- </li>
- </ul>
- </li>
- </ul>
- </li>
-
- <!-- Filesystem -->
- <li dropdown
- routerLinkActive="active"
- class="dropdown tc_menuitem tc_menuitem_cephs">
- <a dropdownToggle
- class="dropdown-toggle"
- data-toggle="dropdown">
- <ng-container i18n>Filesystems</ng-container>
- <span class="caret"></span>
- </a>
- <ul *dropdownMenu
- class="dropdown-menu">
- <li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_cephfs_fs"
- *ngFor="let fs of summaryData?.filesystems">
- <a i18n
- class="dropdown-item"
- routerLink="/cephfs/{{fs.id}}">{{ fs.name }}
- </a>
- </li>
- <li class="tc_submenuitem tc_submenuitem_cephfs_nofs"
- *ngIf="summaryData.filesystems.length === 0">
- <span i18n>There are no filesystems</span>
- </li>
- </ul>
- </li>
- <!--
- <li routerLinkActive="active"
- class="tc_menuitem tc_menuitem_ceph_osds">
- <a i18n
- routerLink="/cephOsds">OSDs
- </a>
- </li>
- <li routerLinkActive="active"
- class="tc_menuitem tc_menuitem_ceph_pools">
- <a i18n
- routerLink="/cephPools">Pools
- </a>
- </li>
- -->
-
- <!-- Object Gateway -->
- <li routerLinkActive="active"
- class="tc_menuitem tc_menuitem_rgw">
- <a i18n
- routerLink="/rgw">Object Gateway
- </a>
- </li>
-
- <!--<li class="dropdown tc_menuitem tc_menuitem_ceph_rgw">
- <a href=""
- class="dropdown-toggle"
- data-toggle="dropdown">
- <ng-container i18n>Object Gateway</ng-container>
- <span class="caret"></span>
- </a>
- <ul *dropdownMenu
- class="dropdown-menu">
- <li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_rgw_users">
- <a i18n
- class="dropdown-item"
- routerLink="/rgw-users">Users
- </a>
- </li>
- <li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_rgw_buckets">
- <a i18n
- class="dropdown-item"
- routerLink="/rgw-buckets">Buckets
- </a>
- </li>
- </ul>
- </li>
- <li routerLinkActive="active"
- class="tc_menuitem tc_submenuitem_settings">
- <a i18n
- routerLink="/settings">Settings
- </a>
- </li> -->
- </ul>
- <!-- /.navbar-primary -->
-
- <ul class="nav navbar-nav navbar-utility">
- <li class="tc_logout">
- <cd-logout class="oa-navbar"></cd-logout>
- </li>
- </ul>
- <!-- /.navbar-utility -->
- </div>
- <!-- /.navbar-collapse -->
-</nav>
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { SharedModule } from '../../../shared/shared.module';
-import { LogoutComponent } from '../../auth/logout/logout.component';
-import { NavigationComponent } from './navigation.component';
-
-describe('NavigationComponent', () => {
- let component: NavigationComponent;
- let fixture: ComponentFixture<NavigationComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [
- SharedModule,
- RouterTestingModule,
- HttpClientTestingModule
- ],
- declarations: [
- NavigationComponent,
- LogoutComponent
- ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(NavigationComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnInit } from '@angular/core';
-import { SummaryService } from '../../../shared/services/summary.service';
-
-@Component({
- selector: 'cd-navigation',
- templateUrl: './navigation.component.html',
- styleUrls: ['./navigation.component.scss']
-})
-export class NavigationComponent implements OnInit {
- summaryData: any;
- rbdPools: Array<any> = [];
-
- constructor(private summaryService: SummaryService) {}
-
- ngOnInit() {
- this.summaryService.summaryData$.subscribe((data: any) => {
- this.summaryData = data;
- this.rbdPools = data.rbd_pools;
- });
- }
-
- blockHealthColor() {
- if (this.summaryData && this.summaryData.rbd_mirroring) {
- if (this.summaryData.rbd_mirroring.errors > 0) {
- return { color: '#d9534f' };
- } else if (this.summaryData.rbd_mirroring.warnings > 0) {
- return { color: '#f0ad4e' };
- }
- }
- }
-}
+++ /dev/null
-<div class="row">
- <div class="col-md-12 text-center">
- <h1 i18n>Sorry, we could not find what you were looking for</h1>
-
- <img class="img-responsive center-block img-rounded"
- src="/assets/1280px-Mimic_Octopus2.jpg">
- <span>
- "<a href="https://www.flickr.com/photos/37707866@N00/4838953223">Mimic Octopus</a>" by prilfish is licensed under
- <a rel="nofollow"
- class="external text"
- href="https://creativecommons.org/licenses/by/2.0/">CC BY 2.0</a>
- </span>
- </div>
-</div>
+++ /dev/null
-h1 {
- font-size: -webkit-xxx-large;
-}
-
-h2 {
- font-size: xx-large;
-}
-
-*{
- font-family: monospace;
-}
-
-img{
- width: 50vw;
-}
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { NotFoundComponent } from './not-found.component';
-
-describe('NotFoundComponent', () => {
- let component: NotFoundComponent;
- let fixture: ComponentFixture<NotFoundComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ NotFoundComponent ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(NotFoundComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component } from '@angular/core';
-
-@Component({
- selector: 'cd-not-found',
- templateUrl: './not-found.component.html',
- styleUrls: ['./not-found.component.scss']
-})
-export class NotFoundComponent {
- constructor() {}
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { ChartsModule } from 'ng2-charts/ng2-charts';
-import { AlertModule } from 'ngx-bootstrap';
-
-import { SparklineComponent } from './sparkline/sparkline.component';
-import { ViewCacheComponent } from './view-cache/view-cache.component';
-
-@NgModule({
- imports: [
- CommonModule,
- AlertModule.forRoot(),
- ChartsModule
- ],
- declarations: [
- ViewCacheComponent,
- SparklineComponent
- ],
- providers: [],
- exports: [
- ViewCacheComponent,
- SparklineComponent
- ]
-})
-export class ComponentsModule { }
+++ /dev/null
-<div class="chart-container"
- [ngStyle]="style">
- <canvas baseChart #sparkCanvas
- [labels]="labels"
- [datasets]="datasets"
- [options]="options"
- [colors]="colors"
- [chartType]="'line'">
- </canvas>
- <div class="chartjs-tooltip" #sparkTooltip>
- <table></table>
- </div>
-</div>
+++ /dev/null
-@import '../../../../styles/chart-tooltip.scss';
-
-.chart-container {
- position: static !important;
-}
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { AppModule } from '../../../app.module';
-import { SparklineComponent } from './sparkline.component';
-
-describe('SparklineComponent', () => {
- let component: SparklineComponent;
- let fixture: ComponentFixture<SparklineComponent>;
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- imports: [AppModule]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(SparklineComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, ElementRef, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
-import { Input } from '@angular/core';
-
-import { ChartTooltip } from '../../../shared/models/chart-tooltip';
-
-@Component({
- selector: 'cd-sparkline',
- templateUrl: './sparkline.component.html',
- styleUrls: ['./sparkline.component.scss']
-})
-export class SparklineComponent implements OnInit, OnChanges {
- @ViewChild('sparkCanvas') chartCanvasRef: ElementRef;
- @ViewChild('sparkTooltip') chartTooltipRef: ElementRef;
-
- @Input() data: any;
- @Input()
- style = {
- height: '30px',
- width: '100px'
- };
-
- public colors: Array<any> = [
- {
- backgroundColor: 'rgba(40,140,234,0.2)',
- borderColor: 'rgba(40,140,234,1)',
- pointBackgroundColor: 'rgba(40,140,234,1)',
- pointBorderColor: '#fff',
- pointHoverBackgroundColor: '#fff',
- pointHoverBorderColor: 'rgba(40,140,234,0.8)'
- }
- ];
-
- options = {
- animation: {
- duration: 0
- },
- responsive: true,
- maintainAspectRatio: false,
- legend: {
- display: false
- },
- elements: {
- line: {
- borderWidth: 1
- }
- },
- tooltips: {
- enabled: false,
- mode: 'index',
- intersect: false,
- custom: undefined
- },
- scales: {
- yAxes: [
- {
- display: false
- }
- ],
- xAxes: [
- {
- display: false
- }
- ]
- }
- };
-
- public datasets: Array<any> = [
- {
- data: []
- }
- ];
-
- public labels: Array<any> = [];
-
- constructor() {}
-
- ngOnInit() {
- const getStyleTop = (tooltip, positionY) => {
- return (tooltip.caretY - tooltip.height - tooltip.yPadding - 5) + 'px';
- };
-
- const getStyleLeft = (tooltip, positionX) => {
- return positionX + tooltip.caretX + 'px';
- };
-
- const chartTooltip = new ChartTooltip(
- this.chartCanvasRef,
- this.chartTooltipRef,
- getStyleLeft,
- getStyleTop
- );
-
- chartTooltip.customColors = {
- backgroundColor: this.colors[0].pointBackgroundColor,
- borderColor: this.colors[0].pointBorderColor
- };
-
- this.options.tooltips.custom = tooltip => {
- chartTooltip.customTooltips(tooltip);
- };
- }
-
- ngOnChanges(changes: SimpleChanges) {
- this.datasets[0].data = changes['data'].currentValue;
- this.datasets = [...this.datasets];
- this.labels = [...Array(changes['data'].currentValue.length)];
- }
-}
+++ /dev/null
-<alert i18n
- type="info"
- *ngIf="status === vcs.ValueNone">
- Retrieving data, please wait.
-</alert>
-
-<alert i18n
- type="warning"
- *ngIf="status === vcs.ValueStale">
- Displaying previously cached data.
-</alert>
-
-<alert i18n
- type="danger"
- *ngIf="status === vcs.ValueException">
- Could not load data. Please check the cluster health.
-</alert>
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { AlertModule } from 'ngx-bootstrap';
-
-import { ViewCacheComponent } from './view-cache.component';
-
-describe('ViewCacheComponent', () => {
- let component: ViewCacheComponent;
- let fixture: ComponentFixture<ViewCacheComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ ViewCacheComponent ],
- imports: [AlertModule.forRoot()]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(ViewCacheComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, Input, OnInit } from '@angular/core';
-
-import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
-
-@Component({
- selector: 'cd-view-cache',
- templateUrl: './view-cache.component.html',
- styleUrls: ['./view-cache.component.scss']
-})
-export class ViewCacheComponent implements OnInit {
- @Input() status: ViewCacheStatus;
- vcs = ViewCacheStatus;
-
- constructor() {}
-
- ngOnInit() {}
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
-import { RouterModule } from '@angular/router';
-
-import { NgxDatatableModule } from '@swimlane/ngx-datatable';
-import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
-
-import { ComponentsModule } from '../components/components.module';
-import { PipesModule } from '../pipes/pipes.module';
-import { TableKeyValueComponent } from './table-key-value/table-key-value.component';
-import { TableComponent } from './table/table.component';
-
-@NgModule({
- imports: [
- CommonModule,
- NgxDatatableModule,
- FormsModule,
- BsDropdownModule.forRoot(),
- PipesModule,
- ComponentsModule,
- RouterModule
- ],
- declarations: [
- TableComponent,
- TableKeyValueComponent
- ],
- exports: [
- TableComponent,
- NgxDatatableModule,
- TableKeyValueComponent
- ]
-})
-export class DataTableModule { }
+++ /dev/null
-<cd-table [data]="tableData"
- [columns]="columns"
- columnMode="flex"
- [toolHeader]="false"
- [header]="false"
- [footer]="false"
- [limit]="0"
- (fetchData)="reloadData()">
-</cd-table>
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormsModule } from '@angular/forms';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { NgxDatatableModule } from '@swimlane/ngx-datatable';
-
-import { ComponentsModule } from '../../components/components.module';
-import { TableComponent } from '../table/table.component';
-import { TableKeyValueComponent } from './table-key-value.component';
-
-describe('TableKeyValueComponent', () => {
- let component: TableKeyValueComponent;
- let fixture: ComponentFixture<TableKeyValueComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ TableComponent, TableKeyValueComponent ],
- imports: [ FormsModule, NgxDatatableModule, ComponentsModule, RouterTestingModule ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(TableKeyValueComponent);
- component = fixture.componentInstance;
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('should make key value object pairs out of arrays with length two', () => {
- component.data = [
- ['someKey', 0],
- [3, 'something'],
- ];
- component.ngOnInit();
- expect(component.tableData.length).toBe(2);
- expect(component.tableData[0].key).toBe('someKey');
- expect(component.tableData[1].value).toBe('something');
- });
-
- it('should transform arrays', () => {
- component.data = [
- ['someKey', [1, 2, 3]],
- [3, 'something']
- ];
- component.ngOnInit();
- expect(component.tableData.length).toBe(2);
- expect(component.tableData[0].key).toBe('someKey');
- expect(component.tableData[0].value).toBe('1, 2, 3');
- expect(component.tableData[1].value).toBe('something');
- });
-
- it('should remove pure object values', () => {
- component.data = [
- [3, 'something'],
- ['will be removed', { a: 3, b: 4, c: 5}]
- ];
- component.ngOnInit();
- expect(component.tableData.length).toBe(1);
- expect(component.tableData[0].value).toBe('something');
- });
-
- it('should make key value object pairs out of an object', () => {
- component.data = {
- 3: 'something',
- someKey: 0
- };
- component.ngOnInit();
- expect(component.tableData.length).toBe(2);
- expect(component.tableData[0].value).toBe('something');
- expect(component.tableData[1].key).toBe('someKey');
- });
-
- it('should make do nothing if data is correct', () => {
- component.data = [
- {
- key: 3,
- value: 'something'
- },
- {
- key: 'someKey',
- value: 0
- }
- ];
- component.ngOnInit();
- expect(component.tableData.length).toBe(2);
- expect(component.tableData[0].value).toBe('something');
- expect(component.tableData[1].key).toBe('someKey');
- });
-
- it('should throw error if miss match', () => {
- component.data = 38;
- expect(() => component.ngOnInit()).toThrowError('Wrong data format');
- component.data = [['someKey', 0, 3]];
- expect(() => component.ngOnInit()).toThrowError('Wrong array format');
- });
-});
+++ /dev/null
-import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
-
-import * as _ from 'lodash';
-
-import { CellTemplate } from '../../enum/cell-template.enum';
-import { CdTableColumn } from '../../models/cd-table-column';
-
-/**
- * Display the given data in a 2 column data table. The left column
- * shows the 'key' attribute, the right column the 'value' attribute.
- * The data table has the following characteristics:
- * - No header and footer is displayed
- * - The relation of the width for the columns 'key' and 'value' is 1:3
- * - The 'key' column is displayed in bold text
- */
-@Component({
- selector: 'cd-table-key-value',
- templateUrl: './table-key-value.component.html',
- styleUrls: ['./table-key-value.component.scss']
-})
-export class TableKeyValueComponent implements OnInit, OnChanges {
-
- columns: Array<CdTableColumn> = [];
-
- @Input() data: any;
-
- tableData: {
- key: string,
- value: any
- }[];
-
- /**
- * The function that will be called to update the input data.
- */
- @Output() fetchData = new EventEmitter();
-
- constructor() { }
-
- ngOnInit() {
- this.columns = [
- {
- prop: 'key',
- flexGrow: 1,
- cellTransformation: CellTemplate.bold
- },
- {
- prop: 'value',
- flexGrow: 3
- }
- ];
- this.useData();
- }
-
- ngOnChanges(changes) {
- this.useData();
- }
-
- useData() {
- let temp = [];
- if (!this.data) {
- return; // Wait for data
- } else if (_.isArray(this.data)) {
- const first = this.data[0];
- if (_.isPlainObject(first) && _.has(first, 'key') && _.has(first, 'value')) {
- temp = [...this.data];
- } else {
- if (_.isArray(first)) {
- if (first.length === 2) {
- temp = this.data.map(a => ({
- key: a[0],
- value: a[1]
- }));
- } else {
- throw new Error('Wrong array format');
- }
- }
- }
- } else if (_.isPlainObject(this.data)) {
- temp = Object.keys(this.data).map(k => ({
- key: k,
- value: this.data[k]
- }));
- } else {
- throw new Error('Wrong data format');
- }
- this.tableData = temp.map(o => {
- if (_.isArray(o.value)) {
- o.value = o.value.join(', ');
- } else if (_.isObject(o.value)) {
- return;
- }
- return o;
- }).filter(o => o); // Filters out undefined
- }
-
- reloadData() {
- // Forward event triggered by the 'cd-table' datatable.
- this.fetchData.emit();
- }
-}
+++ /dev/null
-<div class="dataTables_wrapper">
- <div class="dataTables_header clearfix"
- *ngIf="toolHeader">
- <!-- actions -->
- <div class="oadatatableactions">
- <ng-content select="table-actions"></ng-content>
- </div>
- <!-- end actions -->
-
- <!-- search -->
- <div class="input-group">
- <span class="input-group-addon">
- <i class="glyphicon glyphicon-search"></i>
- </span>
- <input class="form-control"
- type="text"
- [(ngModel)]="search"
- (keyup)='updateFilter($event)'>
- <span class="input-group-btn">
- <button type="button"
- class="btn btn-default clear-input tc_clearInputBtn"
- (click)="updateFilter()">
- <i class="icon-prepend fa fa-remove"></i>
- </button>
- </span>
- </div>
- <!-- end search -->
-
- <!-- pagination limit -->
- <div class="input-group dataTables_paginate">
- <input class="form-control"
- type="number"
- min="1"
- max="9999"
- [value]="limit"
- (click)="setLimit($event)"
- (keyup)="setLimit($event)"
- (blur)="setLimit($event)">
- </div>
- <!-- end pagination limit-->
-
- <!-- show hide columns -->
- <div class="widget-toolbar">
- <div dropdown
- class="dropdown tc_menuitem tc_menuitem_cluster">
- <a dropdownToggle
- class="btn btn-sm btn-default dropdown-toggle tc_columnBtn"
- data-toggle="dropdown">
- <i class="fa fa-lg fa-table"></i>
- </a>
- <ul *dropdownMenu
- class="dropdown-menu">
- <li *ngFor="let column of columns">
- <label>
- <input type="checkbox"
- (change)="toggleColumn($event)"
- [name]="column.prop"
- [checked]="!column.isHidden">
- <span>{{ column.name }}</span>
- </label>
- </li>
- </ul>
- </div>
- </div>
- <!-- end show hide columns -->
-
- <!-- refresh button -->
- <div class="widget-toolbar tc_refreshBtn">
- <a (click)="refreshBtn()">
- <i class="fa fa-lg fa-refresh"
- [class.fa-spin]="updating || loadingIndicator"></i>
- </a>
- </div>
- <!-- end refresh button -->
- </div>
- <ngx-datatable #table
- class="bootstrap oadatatable"
- [cssClasses]="paginationClasses"
- [selectionType]="selectionType"
- [selected]="selection.selected"
- (select)="onSelect()"
- [sorts]="sorts"
- [columns]="tableColumns"
- [columnMode]="columnMode"
- [rows]="rows"
- [rowClass]="getRowClass()"
- [headerHeight]="header ? 'auto' : 0"
- [footerHeight]="footer ? 'auto' : 0"
- [limit]="limit > 0 ? limit : undefined"
- [loadingIndicator]="loadingIndicator"
- [rowIdentity]="rowIdentity()"
- [rowHeight]="'auto'">
- </ngx-datatable>
-</div>
-
-<!-- Table Details -->
-<ng-content select="[cdTableDetail]"></ng-content>
-
-<!-- cell templates that can be accessed from outside -->
-<ng-template #tableCellBoldTpl
- let-value="value">
- <strong>{{ value }}</strong>
-</ng-template>
-
-<ng-template #sparklineTpl
- let-value="value">
- <cd-sparkline [data]="value"></cd-sparkline>
-</ng-template>
-
-<ng-template #routerLinkTpl
- let-row="row"
- let-value="value">
- <a [routerLink]="[row.cdLink]">{{ value }}</a>
-</ng-template>
-
-<ng-template #perSecondTpl
- let-row="row"
- let-value="value">
- {{ value }} /s
-</ng-template>
+++ /dev/null
-@import '../../../../defaults';
-
-.dataTables_wrapper {
- margin-bottom: 25px;
- .separator {
- height: 30px;
- border-left: 1px solid rgba(0,0,0,.09);
- padding-left: 5px;
- margin-left: 5px;
- display: inline-block;
- vertical-align: middle;
- }
- .widget-toolbar {
- display: inline-block;
- float: right;
- width: auto;
- height: 30px;
- line-height: 28px;
- position: relative;
- border-left: 1px solid rgba(0,0,0,.09);
- cursor: pointer;
- padding: 0 8px;
- text-align: center;
- }
- .dropdown-menu {
- white-space: nowrap;
- & > li {
- cursor: pointer;
- & > label {
- width: 100%;
- margin-bottom: 0;
- padding-left: 20px;
- padding-right: 20px;
- cursor: pointer;
- &:hover {
- background-color: #f5f5f5;
- }
- & > input {
- cursor: pointer;
- }
- }
- }
- }
- th.oadatatablecheckbox {
- width: 16px;
- }
- .dataTables_length>input {
- line-height: 25px;
- text-align: right;
- }
-}
-.dataTables_header {
- background-color: #f6f6f6;
- border: 1px solid #d1d1d1;
- border-bottom: none;
- padding: 5px;
- position: relative;
- .oadatatableactions {
- display: inline-block;
- }
- .form-group {
- padding-left: 8px;
- }
- .input-group {
- float: right;
- border-left: 1px solid rgba(0,0,0,.09);
- padding-left: 8px;
- width: 40%;
- max-width: 350px;
- .form-control {
- height: 30px;
- }
- .clear-input {
- height: 30px;
- i {
- vertical-align: text-top;
- }
- }
- }
- .input-group.dataTables_paginate {
- width: 8%;
- min-width: 85px;
- }
-}
-
-::ng-deep .oadatatable {
- border: $border-color;
- margin-bottom: 0;
- max-width: none!important;
- .progress-linear {
- display: block;
- position: relative;
- width: 100%;
- height: 5px;
- padding: 0;
- margin: 0;
- .container {
- background-color: $oa-color-light-blue;
- .bar {
- left: 0;
- height: 100%;
- width: 100%;
- position: absolute;
- overflow: hidden;
- background-color: $oa-color-light-blue;
- }
- .bar:before{
- display: block;
- position: absolute;
- content: "";
- left: -200px;
- width: 200px;
- height: 100%;
- background-color: $oa-color-blue;
- animation: progress-loading 3s linear infinite;
- }
- }
- }
- .datatable-header {
- background-clip: padding-box;
- background-color: #f9f9f9;
- background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
- background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
- background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0);
- .sort-asc, .sort-desc {
- color: $oa-color-blue;
- }
- .datatable-header-cell{
- @include table-cell;
- text-align: left;
- font-weight: bold;
- .datatable-header-cell-label {
- &:after {
- font-family: FontAwesome;
- font-weight: 400;
- height: 9px;
- left: 10px;
- line-height: 12px;
- position: relative;
- vertical-align: baseline;
- width: 12px;
- }
- }
- &.sortable {
- .datatable-header-cell-label:after {
- content: " \f0dc";
- }
- &.sort-active {
- &.sort-asc .datatable-header-cell-label:after {
- content: " \f160";
- }
- &.sort-desc .datatable-header-cell-label:after {
- content: " \f161";
- }
- }
- }
- &:first-child {
- border-left: none;
- }
- }
- }
- .datatable-body {
- .empty-row {
- background-color: $warning-background-color;
- text-align: center;
- font-weight: bold;
- font-style: italic;
- padding-top: 5px;
- padding-bottom: 5px;
- }
- .datatable-body-row {
- &.clickable:hover .datatable-row-group {
- background-color: #eee;
- transition-property: background;
- transition-duration: .3s;
- transition-timing-function: linear;
- }
- &.datatable-row-even {
- background-color: #ffffff;
- }
- &.datatable-row-odd {
- background-color: #f6f6f6;
- }
- &.active, &.active:hover {
- background-color: $bg-color-light-blue;
- }
- .datatable-body-cell{
- @include table-cell;
- &:first-child {
- border-left: none;
- }
- .datatable-body-cell-label {
- display: block;
- }
- }
- }
- }
- .datatable-footer {
- .selected-count, .page-count {
- font-style: italic;
- padding-left: 5px;
- }
- .datatable-pager .pager {
- margin-right: 5px;
- .pages {
- & > a, & > span {
- display: inline-block;
- padding: 5px 10px;
- margin-bottom: 5px;
- border: none;
- }
- a:hover {
- background-color: $oa-color-light-blue;
- }
- &.active > a {
- background-color: $bg-color-light-blue;
- }
- }
- }
- }
-}
-
-@keyframes progress-loading {
- from {
- left: -200px;
- width: 15%;
- }
- 50% {
- width: 30%;
- }
- 70% {
- width: 70%;
- }
- 80% {
- left: 50%;
- }
- 95% {
- left: 120%;
- }
- to {
- left: 100%;
- }
-}
+++ /dev/null
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormsModule } from '@angular/forms';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { NgxDatatableModule, TableColumn } from '@swimlane/ngx-datatable';
-
-import { ComponentsModule } from '../../components/components.module';
-import { TableComponent } from './table.component';
-
-describe('TableComponent', () => {
- let component: TableComponent;
- let fixture: ComponentFixture<TableComponent>;
- const columns: TableColumn[] = [];
- const createFakeData = (n) => {
- const data = [];
- for (let i = 0; i < n; i++) {
- data.push({
- a: i,
- b: i * i,
- c: -(i % 10)
- });
- }
- return data;
- };
-
- beforeEach(
- async(() => {
- TestBed.configureTestingModule({
- declarations: [TableComponent],
- imports: [NgxDatatableModule, FormsModule, ComponentsModule, RouterTestingModule]
- }).compileComponents();
- })
- );
-
- beforeEach(() => {
- fixture = TestBed.createComponent(TableComponent);
- component = fixture.componentInstance;
- });
-
- beforeEach(() => {
- component.data = createFakeData(100);
- component.useData();
- component.columns = [
- {prop: 'a'},
- {prop: 'b'},
- {prop: 'c'}
- ];
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('should have rows', () => {
- expect(component.data.length).toBe(100);
- expect(component.rows.length).toBe(component.data.length);
- });
-
- it('should have an int in setLimit parsing a string', () => {
- expect(component.limit).toBe(10);
- expect(component.limit).toEqual(jasmine.any(Number));
-
- const e = {target: {value: '1'}};
- component.setLimit(e);
- expect(component.limit).toBe(1);
- expect(component.limit).toEqual(jasmine.any(Number));
- e.target.value = '-20';
- component.setLimit(e);
- expect(component.limit).toBe(1);
- });
-
- it('should search for 13', () => {
- component.search = '13';
- expect(component.rows.length).toBe(100);
- component.updateFilter(true);
- expect(component.rows[0].a).toBe(13);
- expect(component.rows[1].b).toBe(1369);
- expect(component.rows[2].b).toBe(3136);
- expect(component.rows.length).toBe(3);
- });
-
- it('should restore full table after search', () => {
- component.search = '13';
- expect(component.rows.length).toBe(100);
- component.updateFilter(true);
- expect(component.rows.length).toBe(3);
- component.updateFilter();
- expect(component.rows.length).toBe(100);
- });
-
- describe('after ngInit', () => {
- const toggleColumn = (prop, checked) => {
- component.toggleColumn({
- target: {
- name: prop,
- checked: checked
- }
- });
- };
-
- beforeEach(() => {
- component.ngOnInit();
- component.table.sorts = component.sorts;
- });
-
- it('should have updated the column definitions', () => {
- expect(component.columns[0].flexGrow).toBe(1);
- expect(component.columns[1].flexGrow).toBe(2);
- expect(component.columns[2].flexGrow).toBe(2);
- expect(component.columns[2].resizeable).toBe(false);
- });
-
- it('should have table columns', () => {
- expect(component.tableColumns.length).toBe(3);
- expect(component.tableColumns).toEqual(component.columns);
- });
-
- it('should have a unique identifier which is search for', () => {
- expect(component.identifier).toBe('a');
- expect(component.sorts[0].prop).toBe('a');
- expect(component.sorts).toEqual(component.createSortingDefinition('a'));
- });
-
- it('should remove column "a"', () => {
- toggleColumn('a', false);
- expect(component.table.sorts[0].prop).toBe('b');
- expect(component.tableColumns.length).toBe(2);
- });
-
- it('should not be able to remove all columns', () => {
- toggleColumn('a', false);
- toggleColumn('b', false);
- toggleColumn('c', false);
- expect(component.table.sorts[0].prop).toBe('c');
- expect(component.tableColumns.length).toBe(1);
- });
-
- it('should enable column "a" again', () => {
- toggleColumn('a', false);
- toggleColumn('a', true);
- expect(component.table.sorts[0].prop).toBe('b');
- expect(component.tableColumns.length).toBe(3);
- });
- });
-});
+++ /dev/null
-import {
- AfterContentChecked,
- Component,
- EventEmitter,
- Input,
- OnChanges,
- OnDestroy,
- OnInit,
- Output,
- TemplateRef,
- Type,
- ViewChild
-} from '@angular/core';
-import {
- DatatableComponent,
- SortDirection,
- SortPropDir,
- TableColumnProp
-} from '@swimlane/ngx-datatable';
-
-import * as _ from 'lodash';
-import 'rxjs/add/observable/timer';
-import { Observable } from 'rxjs/Observable';
-
-import { CdTableColumn } from '../../models/cd-table-column';
-import { CdTableSelection } from '../../models/cd-table-selection';
-
-@Component({
- selector: 'cd-table',
- templateUrl: './table.component.html',
- styleUrls: ['./table.component.scss']
-})
-export class TableComponent implements AfterContentChecked, OnInit, OnChanges, OnDestroy {
- @ViewChild(DatatableComponent) table: DatatableComponent;
- @ViewChild('tableCellBoldTpl') tableCellBoldTpl: TemplateRef<any>;
- @ViewChild('sparklineTpl') sparklineTpl: TemplateRef<any>;
- @ViewChild('routerLinkTpl') routerLinkTpl: TemplateRef<any>;
- @ViewChild('perSecondTpl') perSecondTpl: TemplateRef<any>;
-
- // This is the array with the items to be shown.
- @Input() data: any[];
- // Each item -> { prop: 'attribute name', name: 'display name' }
- @Input() columns: CdTableColumn[];
- // Each item -> { prop: 'attribute name', dir: 'asc'||'desc'}
- @Input() sorts?: SortPropDir[];
- // Method used for setting column widths.
- @Input() columnMode ?= 'flex';
- // Display the tool header, including reload button, pagination and search fields?
- @Input() toolHeader ?= true;
- // Display the table header?
- @Input() header ?= true;
- // Display the table footer?
- @Input() footer ?= true;
- // Page size to show. Set to 0 to show unlimited number of rows.
- @Input() limit ?= 10;
-
- /**
- * Auto reload time in ms - per default every 5s
- * You can set it to 0, undefined or false to disable the auto reload feature in order to
- * trigger 'fetchData' if the reload button is clicked.
- */
- @Input() autoReload: any = 5000;
-
- // Which row property is unique for a row
- @Input() identifier = 'id';
- // Allows other components to specify which type of selection they want,
- // e.g. 'single' or 'multi'.
- @Input() selectionType: string = undefined;
-
- /**
- * Should be a function to update the input data if undefined nothing will be triggered
- *
- * Sometimes it's useful to only define fetchData once.
- * Example:
- * Usage of multiple tables with data which is updated by the same function
- * What happens:
- * The function is triggered through one table and all tables will update
- */
- @Output() fetchData = new EventEmitter();
-
- /**
- * This should be defined if you need access to the selection object.
- *
- * Each time the table selection changes, this will be triggered and
- * the new selection object will be sent.
- *
- * @memberof TableComponent
- */
- @Output() updateSelection = new EventEmitter();
-
- /**
- * Use this variable to access the selected row(s).
- */
- selection = new CdTableSelection();
-
- tableColumns: CdTableColumn[];
- cellTemplates: {
- [key: string]: TemplateRef<any>
- } = {};
- search = '';
- rows = [];
- loadingIndicator = true;
- paginationClasses = {
- pagerLeftArrow: 'i fa fa-angle-double-left',
- pagerRightArrow: 'i fa fa-angle-double-right',
- pagerPrevious: 'i fa fa-angle-left',
- pagerNext: 'i fa fa-angle-right'
- };
- private subscriber;
- private updating = false;
-
- // Internal variable to check if it is necessary to recalculate the
- // table columns after the browser window has been resized.
- private currentWidth: number;
-
- constructor() {}
-
- ngOnInit() {
- this._addTemplates();
- if (!this.sorts) {
- this.identifier = this.columns.some(c => c.prop === this.identifier) ?
- this.identifier :
- this.columns[0].prop + '';
- this.sorts = this.createSortingDefinition(this.identifier);
- }
- this.columns.map(c => {
- if (c.cellTransformation) {
- c.cellTemplate = this.cellTemplates[c.cellTransformation];
- }
- if (!c.flexGrow) {
- c.flexGrow = c.prop + '' === this.identifier ? 1 : 2;
- }
- if (!c.resizeable) {
- c.resizeable = false;
- }
- return c;
- });
- this.tableColumns = this.columns.filter(c => !c.isHidden);
- if (this.autoReload) { // Also if nothing is bound to fetchData nothing will be triggered
- // Force showing the loading indicator because it has been set to False in
- // useData() when this method was triggered by ngOnChanges().
- this.loadingIndicator = true;
- this.subscriber = Observable.timer(0, this.autoReload).subscribe(x => {
- return this.reloadData();
- });
- }
- }
-
- ngOnDestroy() {
- if (this.subscriber) {
- this.subscriber.unsubscribe();
- }
- }
-
- ngAfterContentChecked() {
- // If the data table is not visible, e.g. another tab is active, and the
- // browser window gets resized, the table and its columns won't get resized
- // automatically if the tab gets visible again.
- // https://github.com/swimlane/ngx-datatable/issues/193
- // https://github.com/swimlane/ngx-datatable/issues/193#issuecomment-329144543
- if (this.table && this.table.element.clientWidth !== this.currentWidth) {
- this.currentWidth = this.table.element.clientWidth;
- this.table.recalculate();
- }
- }
-
- _addTemplates() {
- this.cellTemplates.bold = this.tableCellBoldTpl;
- this.cellTemplates.sparkline = this.sparklineTpl;
- this.cellTemplates.routerLink = this.routerLinkTpl;
- this.cellTemplates.perSecond = this.perSecondTpl;
- }
-
- ngOnChanges(changes) {
- this.useData();
- }
-
- setLimit(e) {
- const value = parseInt(e.target.value, 10);
- if (value > 0) {
- this.limit = value;
- }
- }
-
- reloadData() {
- if (!this.updating) {
- this.fetchData.emit();
- this.updating = true;
- }
- }
-
- refreshBtn () {
- this.loadingIndicator = true;
- this.reloadData();
- }
-
- rowIdentity() {
- return (row) => {
- const id = row[this.identifier];
- if (_.isUndefined(id)) {
- throw new Error(`Wrong identifier "${this.identifier}" -> "${id}"`);
- }
- return id;
- };
- }
-
- useData() {
- if (!this.data) {
- return; // Wait for data
- }
- this.rows = [...this.data];
- if (this.search.length > 0) {
- this.updateFilter(true);
- }
- this.loadingIndicator = false;
- this.updating = false;
- }
-
- onSelect() {
- this.selection.update();
- this.updateSelection.emit(_.clone(this.selection));
- }
-
- toggleColumn($event: any) {
- const prop: TableColumnProp = $event.target.name;
- const hide = !$event.target.checked;
- if (hide && this.tableColumns.length === 1) {
- $event.target.checked = true;
- return;
- }
- _.find(this.columns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
- this.updateColumns();
- }
-
- updateColumns () {
- this.tableColumns = this.columns.filter(c => !c.isHidden);
- const sortProp = this.table.sorts[0].prop;
- if (!_.find(this.tableColumns, (c: CdTableColumn) => c.prop === sortProp)) {
- this.table.onColumnSort({sorts: this.createSortingDefinition(this.tableColumns[0].prop)});
- }
- this.table.recalculate();
- }
-
- createSortingDefinition (prop: TableColumnProp): SortPropDir[] {
- return [
- {
- prop: prop,
- dir: SortDirection.asc
- }
- ];
- }
-
- updateFilter(event?) {
- if (!event) {
- this.search = '';
- }
- const val = this.search.toLowerCase();
- const columns = this.columns;
- // update the rows
- this.rows = this.data.filter((d) => {
- return (
- columns.filter(c => {
- return (
- (_.isString(d[c.prop]) || _.isNumber(d[c.prop])) &&
- (d[c.prop] + '').toLowerCase().indexOf(val) !== -1
- );
- }).length > 0
- );
- });
- // Whenever the filter changes, always go back to the first page
- this.table.offset = 0;
- }
-
- getRowClass() {
- // Return the function used to populate a row's CSS classes.
- return () => {
- return {
- clickable: !_.isUndefined(this.selectionType)
- };
- };
- }
-}
+++ /dev/null
-import { PasswordButtonDirective } from './password-button.directive';
-
-describe('PasswordButtonDirective', () => {
- it('should create an instance', () => {
- const directive = new PasswordButtonDirective(null, null);
- expect(directive).toBeTruthy();
- });
-});
+++ /dev/null
-import { Directive, ElementRef, HostListener, Input, OnInit, Renderer2 } from '@angular/core';
-
-@Directive({
- selector: '[cdPasswordButton]'
-})
-export class PasswordButtonDirective implements OnInit {
- private inputElement: any;
- private iElement: any;
-
- @Input('cdPasswordButton') private cdPasswordButton: string;
-
- constructor(private el: ElementRef, private renderer: Renderer2) { }
-
- ngOnInit() {
- this.inputElement = document.getElementById(this.cdPasswordButton);
- this.iElement = this.renderer.createElement('i');
- this.renderer.addClass(this.iElement, 'icon-prepend');
- this.renderer.addClass(this.iElement, 'fa');
- this.renderer.appendChild(this.el.nativeElement, this.iElement);
- this.update();
- }
-
- private update() {
- if (this.inputElement.type === 'text') {
- this.renderer.removeClass(this.iElement, 'fa-eye');
- this.renderer.addClass(this.iElement, 'fa-eye-slash');
- } else {
- this.renderer.removeClass(this.iElement, 'fa-eye-slash');
- this.renderer.addClass(this.iElement, 'fa-eye');
- }
- }
-
- @HostListener('click')
- onClick() {
- // Modify the type of the input field.
- this.inputElement.type = (this.inputElement.type === 'password') ? 'text' : 'password';
- // Update the button icon/tooltip.
- this.update();
- }
-}
+++ /dev/null
-export enum CellTemplate {
- bold = 'bold',
- sparkline = 'sparkline',
- perSecond = 'perSecond',
- routerLink = 'routerLink'
-}
+++ /dev/null
-export enum ViewCacheStatus {
- ValueOk = 0,
- ValueStale = 1,
- ValueNone = 2,
- ValueException = 3
-}
+++ /dev/null
-import { TableColumn } from '@swimlane/ngx-datatable';
-import { CellTemplate } from '../enum/cell-template.enum';
-
-export interface CdTableColumn extends TableColumn {
- cellTransformation?: CellTemplate;
- isHidden?: boolean;
-}
+++ /dev/null
-export class CdTableSelection {
- selected: any[] = [];
- hasMultiSelection: boolean;
- hasSingleSelection: boolean;
- hasSelection: boolean;
-
- constructor() {
- this.update();
- }
-
- /**
- * Recalculate the variables based on the current number
- * of selected rows.
- */
- update() {
- this.hasSelection = this.selected.length > 0;
- this.hasSingleSelection = this.selected.length === 1;
- this.hasMultiSelection = this.selected.length > 1;
- }
-
- /**
- * Get the first selected row.
- * @return {any | null}
- */
- first() {
- return this.hasSelection ? this.selected[0] : null;
- }
-}
+++ /dev/null
-import { ElementRef } from '@angular/core';
-
-import * as _ from 'lodash';
-
-export class ChartTooltip {
- tooltipEl: any;
- chartEl: any;
- getStyleLeft: Function;
- getStyleTop: Function;
- customColors = {
- backgroundColor: undefined,
- borderColor: undefined
- };
- checkOffset = false;
-
- /**
- * Creates an instance of ChartTooltip.
- * @param {ElementRef} chartCanvas Canvas Element
- * @param {ElementRef} chartTooltip Tooltip Element
- * @param {Function} getStyleLeft Function that calculates the value of Left
- * @param {Function} getStyleTop Function that calculates the value of Top
- * @memberof ChartTooltip
- */
- constructor(
- chartCanvas: ElementRef,
- chartTooltip: ElementRef,
- getStyleLeft: Function,
- getStyleTop: Function
- ) {
- this.chartEl = chartCanvas.nativeElement;
- this.getStyleLeft = getStyleLeft;
- this.getStyleTop = getStyleTop;
- this.tooltipEl = chartTooltip.nativeElement;
- }
-
- /**
- * Implementation of a ChartJS custom tooltip function.
- *
- * @param {any} tooltip
- * @memberof ChartTooltip
- */
- customTooltips(tooltip) {
- // Hide if no tooltip
- if (tooltip.opacity === 0) {
- this.tooltipEl.style.opacity = 0;
- return;
- }
-
- // Set caret Position
- this.tooltipEl.classList.remove('above', 'below', 'no-transform');
- if (tooltip.yAlign) {
- this.tooltipEl.classList.add(tooltip.yAlign);
- } else {
- this.tooltipEl.classList.add('no-transform');
- }
-
- // Set Text
- if (tooltip.body) {
- const titleLines = tooltip.title || [];
- const bodyLines = tooltip.body.map(bodyItem => {
- return bodyItem.lines;
- });
-
- let innerHtml = '<thead>';
-
- titleLines.forEach(title => {
- innerHtml += '<tr><th>' + this.getTitle(title) + '</th></tr>';
- });
- innerHtml += '</thead><tbody>';
-
- bodyLines.forEach((body, i) => {
- const colors = tooltip.labelColors[i];
- let style = 'background:' + (this.customColors.backgroundColor || colors.backgroundColor);
- style += '; border-color:' + (this.customColors.borderColor || colors.borderColor);
- style += '; border-width: 2px';
- const span = '<span class="chartjs-tooltip-key" style="' + style + '"></span>';
- innerHtml += '<tr><td nowrap>' + span + this.getBody(body) + '</td></tr>';
- });
- innerHtml += '</tbody>';
-
- const tableRoot = this.tooltipEl.querySelector('table');
- tableRoot.innerHTML = innerHtml;
- }
-
- const positionY = this.chartEl.offsetTop;
- const positionX = this.chartEl.offsetLeft;
-
- // Display, position, and set styles for font
- if (this.checkOffset) {
- const halfWidth = tooltip.width / 2;
- this.tooltipEl.classList.remove('transform-left');
- this.tooltipEl.classList.remove('transform-right');
- if (tooltip.caretX - halfWidth < 0) {
- this.tooltipEl.classList.add('transform-left');
- } else if (tooltip.caretX + halfWidth > this.chartEl.width) {
- this.tooltipEl.classList.add('transform-right');
- }
- }
-
- this.tooltipEl.style.left = this.getStyleLeft(tooltip, positionX);
- this.tooltipEl.style.top = this.getStyleTop(tooltip, positionY);
-
- this.tooltipEl.style.opacity = 1;
- this.tooltipEl.style.fontFamily = tooltip._fontFamily;
- this.tooltipEl.style.fontSize = tooltip.fontSize;
- this.tooltipEl.style.fontStyle = tooltip._fontStyle;
- this.tooltipEl.style.padding = tooltip.yPadding + 'px ' + tooltip.xPadding + 'px';
- }
-
- getBody(body) {
- return body;
- }
-
- getTitle(title) {
- return title;
- }
-}
+++ /dev/null
-export class Credentials {
- username: string;
- password: string;
- stay_signed_in = false;
-}
+++ /dev/null
-import { CephShortVersionPipe } from './ceph-short-version.pipe';
-
-describe('CephShortVersionPipe', () => {
- it('create an instance', () => {
- const pipe = new CephShortVersionPipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
- name: 'cephShortVersion'
-})
-export class CephShortVersionPipe implements PipeTransform {
- transform(value: any, args?: any): any {
- // Expect "ceph version 1.2.3-g9asdasd (as98d7a0s8d7)"
- const result = /ceph version\s+([^ ]+)\s+\(.+\)/.exec(value);
- if (result) {
- // Return the "1.2.3-g9asdasd" part
- return result[1];
- } else {
- // Unexpected format, pass it through
- return value;
- }
- }
-}
+++ /dev/null
-import { FormatterService } from '../services/formatter.service';
-import { DimlessBinaryPipe } from './dimless-binary.pipe';
-
-describe('DimlessBinaryPipe', () => {
- it('create an instance', () => {
- const formatterService = new FormatterService();
- const pipe = new DimlessBinaryPipe(formatterService);
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-import { FormatterService } from '../services/formatter.service';
-
-@Pipe({
- name: 'dimlessBinary'
-})
-export class DimlessBinaryPipe implements PipeTransform {
- constructor(private formatter: FormatterService) {}
-
- transform(value: any, args?: any): any {
- return this.formatter.format_number(value, 1024, [
- 'B',
- 'KiB',
- 'MiB',
- 'GiB',
- 'TiB',
- 'PiB'
- ]);
- }
-}
+++ /dev/null
-import { FormatterService } from '../services/formatter.service';
-import { DimlessPipe } from './dimless.pipe';
-
-describe('DimlessPipe', () => {
- it('create an instance', () => {
- const formatterService = new FormatterService();
- const pipe = new DimlessPipe(formatterService);
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-import { FormatterService } from '../services/formatter.service';
-
-@Pipe({
- name: 'dimless'
-})
-export class DimlessPipe implements PipeTransform {
- constructor(private formatter: FormatterService) {}
-
- transform(value: any, args?: any): any {
- return this.formatter.format_number(value, 1000, [
- ' ',
- 'k',
- 'M',
- 'G',
- 'T',
- 'P'
- ]);
- }
-}
+++ /dev/null
-import { FilterPipe } from './filter.pipe';
-
-describe('FilterPipe', () => {
- it('create an instance', () => {
- const pipe = new FilterPipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
- name: 'filter'
-})
-export class FilterPipe implements PipeTransform {
- transform(value: any, args?: any): any {
- return value.filter(row => {
- let result = true;
-
- args.forEach(filter => {
- if (!filter.value) {
- return;
- }
-
- result = result && filter.applyFilter(row, filter.value);
- if (!result) {
- return result;
- }
- });
-
- return result;
- });
- }
-}
+++ /dev/null
-import { HealthColorPipe } from './health-color.pipe';
-
-describe('HealthColorPipe', () => {
- it('create an instance', () => {
- const pipe = new HealthColorPipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
- name: 'healthColor'
-})
-export class HealthColorPipe implements PipeTransform {
- transform(value: any, args?: any): any {
- if (value === 'HEALTH_OK') {
- return { color: '#00bb00' };
- } else if (value === 'HEALTH_WARN') {
- return { color: '#ffa500' };
- } else if (value === 'HEALTH_ERR') {
- return { color: '#ff0000' };
- } else {
- return null;
- }
- }
-}
+++ /dev/null
-import { ListPipe } from './list.pipe';
-
-describe('ListPipe', () => {
- it('create an instance', () => {
- const pipe = new ListPipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-@Pipe({
- name: 'list'
-})
-export class ListPipe implements PipeTransform {
- transform(value: any, args?: any): any {
- return value.join(', ');
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { CephShortVersionPipe } from './ceph-short-version.pipe';
-import { DimlessBinaryPipe } from './dimless-binary.pipe';
-import { DimlessPipe } from './dimless.pipe';
-import { FilterPipe } from './filter.pipe';
-import { HealthColorPipe } from './health-color.pipe';
-import { ListPipe } from './list.pipe';
-import { RelativeDatePipe } from './relative-date.pipe';
-
-@NgModule({
- imports: [CommonModule],
- declarations: [
- DimlessBinaryPipe,
- HealthColorPipe,
- DimlessPipe,
- CephShortVersionPipe,
- RelativeDatePipe,
- ListPipe,
- FilterPipe
- ],
- exports: [
- DimlessBinaryPipe,
- HealthColorPipe,
- DimlessPipe,
- CephShortVersionPipe,
- RelativeDatePipe,
- ListPipe,
- FilterPipe
- ],
- providers: [
- CephShortVersionPipe,
- DimlessBinaryPipe,
- DimlessPipe,
- RelativeDatePipe,
- ListPipe
- ]
-})
-export class PipesModule {}
+++ /dev/null
-import { RelativeDatePipe } from './relative-date.pipe';
-
-describe('RelativeDatePipe', () => {
- it('create an instance', () => {
- const pipe = new RelativeDatePipe();
- expect(pipe).toBeTruthy();
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-import * as moment from 'moment';
-
-@Pipe({
- name: 'relativeDate'
-})
-export class RelativeDatePipe implements PipeTransform {
- constructor() {}
-
- transform(value: any, args?: any): any {
- if (!value) {
- return 'unknown';
- }
- return moment(value * 1000).fromNow();
- }
-}
+++ /dev/null
-import { Injectable } from '@angular/core';
-import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
-
-import { AuthStorageService } from './auth-storage.service';
-
-@Injectable()
-export class AuthGuardService implements CanActivate {
-
- constructor(private router: Router, private authStorageService: AuthStorageService) {
- }
-
- canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
- if (this.authStorageService.isLoggedIn()) {
- return true;
- }
- this.router.navigate(['/login']);
- return false;
- }
-}
+++ /dev/null
-import {
- HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest,
- HttpResponse
-} from '@angular/common/http';
-import { Injectable } from '@angular/core';
-import { Router } from '@angular/router';
-
-import { ToastsManager } from 'ng2-toastr';
-import 'rxjs/add/operator/do';
-import { Observable } from 'rxjs/Observable';
-
-import { AuthStorageService } from './auth-storage.service';
-
-@Injectable()
-export class AuthInterceptorService implements HttpInterceptor {
-
- constructor(private router: Router,
- private authStorageService: AuthStorageService,
- public toastr: ToastsManager) {
- }
-
- intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
- return next.handle(request).do((event: HttpEvent<any>) => {
- if (event instanceof HttpResponse) {
- // do nothing
- }
- }, (err: any) => {
- if (err instanceof HttpErrorResponse) {
- if (err.status === 404) {
- this.router.navigate(['/404']);
- return;
- }
-
- this.toastr.error(err.error.detail || '', `${err.status} - ${err.statusText}`);
- if (err.status === 401) {
- this.authStorageService.remove();
- this.router.navigate(['/login']);
- }
- }
- });
- }
-}
+++ /dev/null
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class AuthStorageService {
-
- constructor() {
- }
-
- set(username: string) {
- localStorage.setItem('dashboard_username', username);
- }
-
- remove() {
- localStorage.removeItem('dashboard_username');
- }
-
- isLoggedIn() {
- return localStorage.getItem('dashboard_username') !== null;
- }
-
-}
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-import { Credentials } from '../models/credentials';
-import { AuthStorageService } from './auth-storage.service';
-
-@Injectable()
-export class AuthService {
-
- constructor(private authStorageService: AuthStorageService,
- private http: HttpClient) {
- }
-
- login(credentials: Credentials) {
- return this.http.post('api/auth', credentials).toPromise().then((resp: Credentials) => {
- this.authStorageService.set(resp.username);
- });
- }
-
- logout() {
- return this.http.delete('api/auth').toPromise().then(() => {
- this.authStorageService.remove();
- });
- }
-}
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { ConfigurationService } from './configuration.service';
-
-describe('ConfigurationService', () => {
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [ConfigurationService],
- imports: [HttpClientTestingModule, HttpClientModule]
- });
- });
-
- it(
- 'should be created',
- inject([ConfigurationService], (service: ConfigurationService) => {
- expect(service).toBeTruthy();
- })
- );
-});
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class ConfigurationService {
- constructor(private http: HttpClient) {}
-
- getConfigData() {
- return this.http.get('api/cluster_conf/');
- }
-}
+++ /dev/null
-import { inject, TestBed } from '@angular/core/testing';
-
-import { FormatterService } from './formatter.service';
-
-describe('FormatterService', () => {
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [FormatterService]
- });
- });
-
- it('should be created', inject([FormatterService], (service: FormatterService) => {
- expect(service).toBeTruthy();
- }));
-});
+++ /dev/null
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class FormatterService {
- constructor() {}
-
- truncate(n, maxWidth) {
- const stringized = n.toString();
- const parts = stringized.split('.');
- if (parts.length === 1) {
- // Just an int
- return stringized;
- } else {
- const fractionalDigits = maxWidth - parts[0].length - 1;
- if (fractionalDigits <= 0) {
- // No width available for the fractional part, drop
- // it and the decimal point
- return parts[0];
- } else {
- return stringized.substring(0, maxWidth);
- }
- }
- }
-
- format_number(n, divisor, units) {
- const width = 4;
- let unit = 0;
-
- if (n == null) {
- // People shouldn't really be passing null, but let's
- // do something sensible instead of barfing.
- return '-';
- }
-
- while (Math.floor(n / divisor ** unit).toString().length > width - 1) {
- unit = unit + 1;
- }
-
- let truncatedFloat;
- if (unit > 0) {
- truncatedFloat = this.truncate(
- (n / Math.pow(divisor, unit)).toString(),
- width
- );
- } else {
- truncatedFloat = this.truncate(n, width);
- }
-
- return truncatedFloat === '' ? '-' : (truncatedFloat + units[unit]);
- }
-}
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class HostService {
-
- constructor(private http: HttpClient) {
- }
-
- list() {
- return this.http.get('api/host').toPromise().then((resp: any) => {
- return resp;
- });
- }
-}
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class PoolService {
-
- constructor(private http: HttpClient) {
- }
-
- rbdPoolImages(pool) {
- return this.http.get(`api/rbd/${pool}`).toPromise().then((resp: any) => {
- return resp;
- });
- }
-}
+++ /dev/null
-import { HttpClientModule } from '@angular/common/http';
-import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { RbdMirroringService } from './rbd-mirroring.service';
-
-describe('RbdMirroringService', () => {
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [RbdMirroringService],
- imports: [HttpClientTestingModule, HttpClientModule]
- });
- });
-
- it('should be created', inject([RbdMirroringService], (service: RbdMirroringService) => {
- expect(service).toBeTruthy();
- }));
-});
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class RbdMirroringService {
- constructor(private http: HttpClient) {}
-
- get() {
- return this.http.get('api/rbdmirror');
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { ConfigurationService } from './configuration.service';
-import { FormatterService } from './formatter.service';
-import { RbdMirroringService } from './rbd-mirroring.service';
-import { SummaryService } from './summary.service';
-import { TcmuIscsiService } from './tcmu-iscsi.service';
-
-@NgModule({
- imports: [CommonModule],
- declarations: [],
- providers: [
- FormatterService,
- SummaryService,
- TcmuIscsiService,
- ConfigurationService,
- RbdMirroringService
- ]
-})
-export class ServicesModule { }
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { inject, TestBed } from '@angular/core/testing';
-
-import { SharedModule } from '../shared.module';
-import { SummaryService } from './summary.service';
-
-describe('SummaryService', () => {
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [SummaryService],
- imports: [HttpClientTestingModule, SharedModule]
- });
- });
-
- it(
- 'should be created',
- inject([SummaryService], (service: SummaryService) => {
- expect(service).toBeTruthy();
- })
- );
-});
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-import { Subject } from 'rxjs/Subject';
-
-import { AuthStorageService } from './auth-storage.service';
-
-@Injectable()
-export class SummaryService {
- // Observable sources
- private summaryDataSource = new Subject();
-
- // Observable streams
- summaryData$ = this.summaryDataSource.asObservable();
-
- constructor(private http: HttpClient, private authStorageService: AuthStorageService) {
- this.refresh();
- }
-
- refresh() {
- if (this.authStorageService.isLoggedIn()) {
- this.http.get('api/summary').subscribe(data => {
- this.summaryDataSource.next(data);
- });
- }
-
- setTimeout(() => {
- this.refresh();
- }, 5000);
- }
-}
+++ /dev/null
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-@Injectable()
-export class TcmuIscsiService {
-
- constructor(private http: HttpClient) {
- }
-
- tcmuiscsi() {
- return this.http.get('api/tcmuiscsi').toPromise().then((resp: any) => {
- return resp;
- });
- }
-}
+++ /dev/null
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-
-import { ComponentsModule } from './components/components.module';
-import { DataTableModule } from './datatable/datatable.module';
-import { PasswordButtonDirective } from './directives/password-button.directive';
-import { PipesModule } from './pipes/pipes.module';
-import { AuthGuardService } from './services/auth-guard.service';
-import { AuthStorageService } from './services/auth-storage.service';
-import { AuthService } from './services/auth.service';
-import { FormatterService } from './services/formatter.service';
-import { HostService } from './services/host.service';
-import { PoolService } from './services/pool.service';
-import { ServicesModule } from './services/services.module';
-
-@NgModule({
- imports: [
- CommonModule,
- PipesModule,
- ComponentsModule,
- ServicesModule,
- DataTableModule
- ],
- declarations: [
- PasswordButtonDirective
- ],
- exports: [
- ComponentsModule,
- PipesModule,
- ServicesModule,
- PasswordButtonDirective,
- DataTableModule
- ],
- providers: [
- AuthService,
- AuthStorageService,
- AuthGuardService,
- PoolService,
- FormatterService,
- HostService
- ],
-})
-export class SharedModule {}
+++ /dev/null
-$warning-background-color: #fff3cd;
-$oa-color-blue: #288cea;
-$oa-color-light-blue: #afd9ee;
-$bg-color-light-blue: #d9edf7;
-$border-color: 1px solid #d1d1d1;
-@mixin table-cell {
- padding: 5px;
- border: none;
- border-left: $border-color;
- border-bottom: $border-color;
-}
+++ /dev/null
-export const environment = {
- production: true
-};
+++ /dev/null
-// The file contents for the current environment will overwrite these during build.
-// The build system defaults to the dev environment which uses `environment.ts`, but if you do
-// `ng build --env=prod` then `environment.prod.ts` will be used instead.
-// The list of which env maps to which file can be found in `.angular-cli.json`.
-
-export const environment = {
- production: false
-};
+++ /dev/null
-<!doctype html>
-<html lang="en">
-<head>
- <meta charset="utf-8">
- <title>Ceph</title>
-
- <script>
- document.write('<base href="' + document.location+ '" />');
- </script>
-
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <link rel="icon" type="image/x-icon" href="favicon.ico">
-</head>
-<body>
- <noscript>
- <div class="noscript container"
- ng-if="false">
- <div class="jumbotron alert alert-danger">
- <h2 i18n>JavaScript required!</h2>
- <p i18n>A browser with JavaScript enabled is required in order to use this service.</p>
- <p i18n>When using Internet Explorer, please check your security settings and add this address to your trusted sites.</p>
- </div>
- </div>
- </noscript>
-
- <cd-root></cd-root>
-</body>
-</html>
+++ /dev/null
-import { enableProdMode } from '@angular/core';
-import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
-
-import { AppModule } from './app/app.module';
-import { environment } from './environments/environment';
-
-if (environment.production) {
- enableProdMode();
-}
-
-platformBrowserDynamic().bootstrapModule(AppModule)
- .catch(err => console.log(err));
+++ /dev/null
-/*
- Basics
- Branding
- Breadcrumb
- Buttons
- Dropdown
- Grid
- Modal
- Navbar
- Navs
- Notifications
- Pagination
- Panel
- Table
- Typo
-
- Login
- Statistics
-
- ApiRecorder
- Caret
- Datatables
- Feedback
- FlexElement
- Grafana
- Graph
- Progressbar
- TagForm
- Trees
- CSS Fix
-*/
-
-@import 'defaults';
-
-$fa-font-path: "../node_modules/font-awesome/fonts";
-@import "../node_modules/font-awesome/scss/font-awesome";
-
-/* Basics */
-html {
- background-color: #ffffff;
-}
-html,
-body {
- width: 100%;
- height: 100%;
- font-size: 12px;
-}
-optgroup {
- font-weight: bold;
- font-style: italic;
-}
-option {
- font-weight: normal;
- font-style: normal;
-}
-.full-height {
- height: 100%;
-}
-.vertical-align {
- display: flex;
- align-items: center;
-}
-.loading {
- position: absolute;
- top: 50%;
- left: 50%;
-}
-.bg-color-darken {
- background-color: #404040!important;
-}
-.bg-color-greenLight {
- background-color: #71843f!important;
-}
-.bg-color-red {
- background-color: #a90329!important;
-}
-.no-margin {
- margin: 0;
-}
-.margin-left-md {
- margin-left: 15px
-}
-.margin-right-md {
- margin-right: 15px
-}
-.margin-right-sm {
- margin-right: 10px
-}
-.margin-bottom-md {
- margin-bottom: 15px
-}
-.no-padding {
- padding: 0;
-}
-.small-padding {
- padding: 5px;
-}
-.no-border {
- border: 0px;
- box-shadow: 0px 0px 0px !important;
-}
-.no-wrap {
- white-space: nowrap;
-}
-.strikethrough {
- text-decoration: line-through;
-}
-.italic {
- font-style: italic;
-}
-.bold {
- font-weight: bold;
-}
-.text-right {
- text-align: right;
-}
-.text-monospace {
- font-family: monospace;
-}
-
-/* Branding */
-.navbar-openattic .navbar-brand,
-.navbar-openattic .navbar-brand:hover{
- color: #ececec;
- height: auto;
- margin: 15px 0 15px 20px;
- padding: 0;
- -webkit-align-self: flex-start;
- align-self: flex-start;
-}
-.navbar-openattic .navbar-brand>img {
- height: 25px;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- padding: 8px 0;
- background-color: transparent;
- border-radius: 0;
-}
-.breadcrumb>li+li:before {
- padding: 0 5px 0 7px;
- color: #474544;
- font-family: "FontAwesome";
- content: "\f101";
-}
-.breadcrumb>li>span {
- color: #474544;
-}
-
-/* Icons */
-.icon-warning {
- color: #f0ad4e;
-}
-.icon-danger {
- color: #c9302c;
-}
-
-/* Buttons */
-.btn-openattic {
- color: #ececec;
- background-color: $oa-color-blue;
- border-color: $oa-color-blue;
-}
-.btn-primary {
- color: #ececec;
- background-color: $oa-color-blue;
- border-color: #2172bf;
-}
-.btn-primary:hover,
-.btn-primary:focus,
-.btn-primary:active,
-.btn-primary.active,
-.open .dropdown-toggle.btn-primary {
- color: #ececec;
- background-color: #2582D9;
- border-color: #2172bf;
-}
-.btn-primary:active,
-.btn-primary.active,
-.open .dropdown-toggle.btn-primary {
- background-image: none;
-}
-.btn-primary.disabled,
-.btn-primary[disabled],
-fieldset[disabled] .btn-primary,
-.btn-primary.disabled:hover,
-.btn-primary[disabled]:hover,
-fieldset[disabled] .btn-primary:hover,
-.btn-primary.disabled:focus,
-.btn-primary[disabled]:focus,
-fieldset[disabled] .btn-primary:focus,
-.btn-primary.disabled:active,
-.btn-primary[disabled]:active,
-fieldset[disabled] .btn-primary:active,
-.btn-primary.disabled.active,
-.btn-primary[disabled].active,
-fieldset[disabled] .btn-primary.active {
- background-color: $oa-color-blue;
- border-color: #2172bf;
-}
-.btn-primary .badge {
- color: $oa-color-blue;
- background-color: #ececec;
-}
-.btn-primary .caret {
- color: #ececec;
-}
-.btn-group>.btn>i.fa,
-button.btn.btn-label>i.fa {
- /** Add space between icon and text */
- padding-right: 5px;
-}
-
-/* Dropdown */
-.dropdown-menu {
- min-width: 50px;
-}
-.dropdown-menu>li>a {
- color: #474544;
- cursor: pointer;
-}
-.dropdown-menu>li>a>i.fa {
- /** Add space between icon and text */
- padding-right: 5px;
-}
-.dropdown-menu>.active>a {
- color: #ececec;
- background-color: $oa-color-blue;
-}
-.dataTables_wrapper .dropdown-menu>li.divider {
- cursor: auto;
-}
-
-/* Grid */
-.container,
-.container-fluid {
- padding-left: 30px;
- padding-right: 30px;
-}
-.row {
- margin-left: -30px;
- margin-right: -30px;
-}
-.col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9,
-.col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9,
-.col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9,
-.col-xs-1, .col-xs-10, .col-xs-11, .col-xs-12, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9 {
- padding-left: 30px;
- padding-right: 30px;
-}
-
-/* Modal */
-.modal-dialog {
- margin: 30px auto !important;
-}
-.modal .modal-content .openattic-modal-header,
-.modal .modal-content .openattic-modal-content,
-.modal .modal-content .openattic-modal-footer {
- padding: 10px 20px;
-}
-.modal .modal-content .openattic-modal-header {
- border-bottom: 1px solid #cccccc;
- border-radius: 5px 5px 0 0;
- background-color: #f5f5f5;
-}
-.modal .modal-content .openattic-modal-content {
- padding: 20px 20px 10px 20px;
- overflow-x: auto;
- max-height: 70vh;
-}
-.modal .modal-content .openattic-modal-content p {
- margin-bottom: 10px;
-}
-.modal .modal-content .openattic-modal-content legend {
- font-size: 1.833em;
-}
-.modal .modal-content .openattic-modal-footer {
- border-top: 1px solid #cccccc;
- border-radius: 0 0 5px 5px;
- background-color: #f5f5f5;
-}
-.modal .modal-content .openattic-modal-header span {
- display: block;
- font-size: 16px; /* Same as .panel-title */
-}
-
-/* Modal Table (Task Queue) */
-table.task-queue-table thead {
- display: flex;
- flex-flow: row;
-}
-table.task-queue-table thead tr {
- display: flex;
- align-items: stretch;
- width: 100%;
-}
-table.task-queue-table tbody {
- display: flex;
- flex-flow: row wrap;
-}
-table.task-queue-table tbody tr {
- display: flex;
- width: 100%
-}
-table.task-queue-table > * > tr > * {
- flex: 1;
-}
-table.task-queue-table > * > tr > .oadatatablecheckbox {
- flex: 0;
-}
-div.task-queue-modal-content {
- height: 40em;
-}
-div.openattic-modal-content div.modal-scroll {
- max-height: 26em;
- overflow: auto;
- border-bottom: 1px solid #e1e1e1;
-}
-div.task-queue-modal-content div.dataTables_wrapper {
- margin-bottom: 0;
-}
-div.task-queue-modal-content div.dataTables_wrapper th.oadatatablecheckbox {
- width: 100%;
-}
-div.task-queue-modal-content div.dataTables_wrapper div.widget-toolbar.tc_refreshBtn{
- width: 36px;
-}
-ul.task-queue-pagination {
- display: table;
- margin: auto;
- padding-top: 10px;
-}
-
-/* Navbar */
-.navbar-openattic {
- margin-bottom: 0;
- background: #474544;
- border: 0;
- border-radius: 0;
- border-top: 4px solid $oa-color-blue;
- font-size: 1.2em;
-}
-.navbar-openattic .navbar-header {
- display: flex;
- float: none;
-}
-.navbar-openattic .navbar-toggle {
- margin-left: auto;
- border: 0;
-}
-.navbar-openattic .navbar-toggle:focus,
-.navbar-openattic .navbar-toggle:hover {
- background-color: transparent;
- outline: 0;
-}
-.navbar-openattic .navbar-toggle .icon-bar {
- background-color: #ececec;
-}
-.navbar-openattic .navbar-toggle:focus .icon-bar,
-.navbar-openattic .navbar-toggle:hover .icon-bar {
- -webkit-box-shadow: 0 0 3px #fff;
- box-shadow: 0 0 3px #fff;
-}
-.navbar-openattic .navbar-collapse {
- padding: 0;
-}
-.navbar-openattic .navbar-nav>li>a,
-.navbar-openattic .navbar-nav>li>.oa-navbar>a {
- color: #ececec;
- line-height: 1;
- padding: 10px 20px;
- position: relative;
- display: block;
- text-decoration: none;
-}
-.navbar-openattic .navbar-nav>li>a:focus,
-.navbar-openattic .navbar-nav>li>a:hover,
-.navbar-openattic .navbar-nav>li>.oa-navbar>a:focus,
-.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover {
- color: #ececec;
-}
-.navbar-openattic .navbar-nav>li>a:hover,
-.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover {
- background-color: #505050;
-}
-.navbar-openattic .navbar-nav>.open>a,
-.navbar-openattic .navbar-nav>.open>a:hover,
-.navbar-openattic .navbar-nav>.open>a:focus,
-.navbar-openattic .navbar-nav>.open>.oa-navbar>a,
-.navbar-openattic .navbar-nav>.open>.oa-navbar>a:hover,
-.navbar-openattic .navbar-nav>.open>.oa-navbar>a:focus {
- color: #ececec;
- border-color: transparent;
- background-color: transparent;
-}
-.navbar-openattic .navbar-primary>li>a {
- border: 0;
-}
-.navbar-openattic .navbar-primary>.active>a,
-.navbar-openattic .navbar-primary>.active>a:hover,
-.navbar-openattic .navbar-primary>.active>a:focus {
- color: #ececec;
- background-color: $oa-color-blue;
- border: 0;
-}
-.navbar-openattic .navbar-utility a,
-.navbar-openattic .navbar-utility .fa{
- font-size: 1.0em;
-}
-.navbar-openattic .navbar-utility>.active>a {
- color: #ececec;
- background-color: #505050;
-}
-.navbar-openattic .navbar-utility>li>.open>a,
-.navbar-openattic .navbar-utility>li>.open>a:hover,
-.navbar-openattic .navbar-utility>li>.open>a:focus {
- color: #ececec;
- border-color: transparent;
- background-color: transparent;
-}
-@media (min-width: 768px) {
- .navbar-openattic .navbar-primary>li>a {
- border-bottom: 4px solid transparent;
- }
- .navbar-openattic .navbar-primary>.active>a,
- .navbar-openattic .navbar-primary>.active>a:hover,
- .navbar-openattic .navbar-primary>.active>a:focus {
- background-color: transparent;
- border-bottom: 4px solid $oa-color-blue;
- }
- .navbar-openattic .navbar-utility {
- border-bottom: 0;
- font-size: 11px;
- position: absolute;
- right: 0;
- top: 0;
- }
-}
-@media (max-width: 767px) {
- .navbar-openattic .navbar-nav {
- margin: 0;
- }
- .navbar-openattic .navbar-collapse,
- .navbar-openattic .navbar-form {
- border-color: #ececec;
- }
- .navbar-openattic .navbar-collapse {
- padding: 0;
- }
- .navbar-nav .open .dropdown-menu {
- padding-top: 0;
- padding-bottom: 0;
- background-color: #505050;
- }
- .navbar-nav .open .dropdown-menu .dropdown-header,
- .navbar-nav .open .dropdown-menu>li>a {
- padding: 5px 15px 5px 35px;
- }
- .navbar-openattic .navbar-nav .open .dropdown-menu>li>a {
- color: #ececec;
- }
- .navbar-openattic .navbar-nav .open .dropdown-menu>.active>a {
- color: #ececec;
- background-color: $oa-color-blue;
- }
- .navbar-openattic .navbar-nav>li>a:hover {
- background-color: $oa-color-blue;
- }
- .navbar-openattic .navbar-utility {
- border-top: 1px solid #ececec;
- }
- .navbar-openattic .navbar-primary>.active>a,
- .navbar-openattic .navbar-primary>.active>a:hover,
- .navbar-openattic .navbar-primary>.active>a:focus {
- background-color: $oa-color-blue;
- }
-}
-
-/* Navs */
-.nav-tabs {
- margin-bottom: 15px;
-}
-.nav-tabs-openattic {
- margin-top: -15px;
- margin-bottom: 15px;
-}
-.nav-tabs-openattic>li>a {
- padding: 7px 15px 4px 15px;
-}
-.nav-tabs-openattic>li.active>a,
-.nav-tabs-openattic>li.active>a:active,
-.nav-tabs-openattic>li.active>a:focus,
-.nav-tabs-openattic>li.active>a:hover {
- border: 0!important;
- border-bottom: 3px solid $oa-color-blue!important;
-}
-
-/* Notifications */
-#toasty .toast.toasty-theme-bootstrap {
- opacity: 1
-}
-
-/* Pagination */
-.pagination {
- display: block;
- margin: 0;
-}
-.pagination>.disabled>a,
-.pagination>.disabled>a:focus,
-.pagination>.disabled>a:hover,
-.pagination>.disabled>span,
-.pagination>.disabled>span:focus,
-.pagination>.disabled>span:hover {
- -webkit-box-shadow: none;
- box-shadow: none;
- cursor: not-allowed;
- background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
- background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
- background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
-}
-.pagination>.active>a,
-.pagination>.active>a:focus,
-.pagination>.active>a:hover,
-.pagination>.active>span,
-.pagination>.active>span:focus,
-.pagination>.active>span:hover,
-.pagination>.disabled>a,
-.pagination>.disabled>a:focus,
-.pagination>.disabled>a:hover,
-.pagination>.disabled>span,
-.pagination>.disabled>span:focus,
-.pagination>.disabled>span:hover,
-.pagination>li>a,
-.pagination>li>span,
-.panel-group
-.panel-heading {
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0);
-}
-.pagination>li>a,
-.pagination>li>span {
- background-color: #eee;
- background-image: -webkit-linear-gradient(top,#fafafa 0,#ededed 100%);
- background-image: -o-linear-gradient(top,#fafafa 0,#ededed 100%);
- background-image: linear-gradient(to bottom,#fafafa 0,#ededed 100%);
- border-color: #b7b7b7;
- color: #4d5258;
- cursor: pointer;
- font-weight: 600;
- padding: 2px 10px;
-}
-.pagination>.active>span,
-.pagination>.active>span:focus,
-.pagination>.active>span:hover {
- color: $oa-color-blue;
- border-color: #fff #e1e1e1 #f4f4f4;
- border-width: 0 1px;
-}
-
-/* Panel */
-.panel .panel-toolbar {
- float: right;
-}
-.panel .panel-toolbar div {
- display: inline-block;
-}
-.panel .panel-toolbar>a,
-.panel .panel-toolbar>.dropdown>a {
- padding-left: 5px;
-}
-.panel-dashboard {
- height: 100%;
- padding-top: 60px;
-}
-.panel-dashboard>.panel-heading {
- cursor: move;
- position: relative;
- margin-top: -60px;
- width: 100%;
-}
-.panel-dashboard>.panel-heading>.panel-title {
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- -o-text-overflow: ellipsis;
-}
-.panel-dashboard>.panel-heading>.toolbar a {
- text-decoration: none;
-}
-.panel-dashboard>.panel-body {
- height: 100%;
- overflow: auto;
-}
-.panel-dashboard>.panel-body .indent {
- margin-top: 10px;
- margin-left: 10px;
-}
-.panel-dashboard .overlay {
- position: absolute;
- bottom: 5px;
- right: 5px;
- z-index: 10;
-}
-.panel-dashboard .max-height {
- height: 100%;
-}
-.panel-dashboard .max-height.alert-is-shown {
- height: 85%;
-}
-.panel-dashboard .fa-2x{
- vertical-align: middle;
- margin-right: 0.5em;
-}
-.panel-dashboard .alert.bottom-margin-zero {
- margin-bottom: 0;
-}
-.panel-openattic {
- border: $border-color;
- border-top: 0;
- border-radius: 0;
-}
-.panel-openattic>.panel-heading {
- border-top: 2px solid $oa-color-blue;
- border-radius: 0;
- padding: 20px 15px;
-}
-.panel-openattic>.panel-heading>.panel-title {
- color: #333333;
- font-size: 1.333em;
- margin: 0;
- padding: 0;
-}
-.panel-openattic>.panel-body {
- background: #ffffff;
- border-top: $border-color;
- padding: 10px 15px;
-}
-.panel-openattic>.panel-footer {
- background: #ffffff;
- border-top: $border-color;
-}
-
-/* Typo */
-a {
- color: $oa-color-blue;
-}
-a:hover,
-a:focus{
- color: #474544;
-}
-h1 {
- letter-spacing: -1px;
- font-size: 2em;
-}
-h2 {
- letter-spacing: -1px;
- font-size: 1.833em;
-}
-h3{
- display: block;
- font-size: 1.583em;
- font-weight: 400;
-}
-h3.sub-title {
- color: #666666;
- margin-left: 15px;
-}
-h4{
- font-size: 1.5em;
- line-height: normal
-}
-h5{
- font-size: 1.417em;
- font-weight: 300;
- line-height: normal;
-}
-h6{
- font-size: 1.25em;
- font-weight: 700;
- line-height: normal;
-}
-
-/*************************************************************/
-
-/* Statistics */
-.statistics-content {
- margin: 0 -20px;
-}
-
-/*************************************************************/
-
-/* ApiRecorder */
-.apirecorder {
- resize: none;
- width:100%;
-}
-.apirecorder-enabled {
- color: red;
-}
-
-/* Caret */
-.caret {
- color: $oa-color-blue;
-}
-
-/* Feedback */
-#feedback .feedback-button {
- position: fixed;
- top: 50%;
- right: 0;
- padding: 2px 16px;
- cursor: pointer;
- color: #ffffff;
- font-size: 1.2em;
- font-weight: 700;
- background-color: $oa-color-blue;
- border-radius: 5px 5px 0 0;
- z-index: 99999;
-}
-#feedback .feedback-button:hover {
- background-color: #2172bf;
-}
-#feedback .feedback-button-transform {
- -webkit-transform: rotate(-90deg) translate(50%, -100%);
- -moz-transform: rotate(-90deg) translate(50%, -100%);
- -ms-transform: rotate(-90deg) translate(50%, -100%);
- -o-transform: rotate(-90deg) translate(50%, -100%);
- transform: rotate(-90deg) translate(50%, -100%);
- -webkit-transform-origin: top right;
- -moz-transform-origin: top right;
- -ms-transform-origin: top right;
- -o-transform-origin: top right;
- transform-origin: top right;
-}
-#feedback .feedback-button-active {
- right: 299px;
-}
-#feedback .feedback-button .fa,
-#feedback .feedback-button .glyphicon{
- padding-right: 6px;
-}
-#feedback .feedback-panel {
- position: fixed;
- top: 0;
- right: -300px;
- padding: 20px;
- width: 300px;
- height: 100%;
- background-color: #ffffff;
- border-left: 5px solid $oa-color-blue;
- z-index: 99999;
- overflow-y: auto;
-}
-#feedback .feedback-panel-active {
- right: 0;
-}
-#feedback .feedback-transition {
- transition: right 150ms cubic-bezier(0.0, 0.0, 0.2, 1);
-}
-
-/* FlexElement */
-/* Container */
-.flex-container {
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
-}
-.flex-wrap {
- -webkit-flex-wrap: wrap;
- -ms-flex-wrap: wrap;
- flex-wrap: wrap;
-}
-.flex-nowrap {
- -webkit-flex-wrap: nowrap;
- -ms-flex-wrap: nowrap;
- flex-wrap: nowrap;
-}
-.flex-row {
- -webkit-flex-direction: row;
- -ms-flex-direction: row;
- flex-direction: row;
-}
-.flex-column {
- -webkit-flex-direction: column;
- -ms-flex-direction: column;
- flex-direction: column;
-}
-/* Items */
-.flex-item {
- margin-bottom: 10px;
- padding: 15px;
-}
-.flex-item-1 { -webkit-flex: 1; -moz-flex: 1; -ms-flex: 1; flex: 1; padding: 0 10px; }
-.flex-item-2 { -webkit-flex: 2; -moz-flex: 2; -ms-flex: 2; flex: 2; padding: 0 10px; }
-.flex-item-3 { -webkit-flex: 3; -moz-flex: 3; -ms-flex: 3; flex: 3; padding: 0 10px; }
-.flex-item-4 { -webkit-flex: 4; -moz-flex: 4; -ms-flex: 4; flex: 4; padding: 0 10px; }
-.flex-item-5 { -webkit-flex: 5; -moz-flex: 5; -ms-flex: 5; flex: 5; padding: 0 10px; }
-.flex-item-6 { -webkit-flex: 6; -moz-flex: 6; -ms-flex: 6; flex: 6; padding: 0 10px; }
-.flex-item-7 { -webkit-flex: 7; -moz-flex: 7; -ms-flex: 7; flex: 7; padding: 0 10px; }
-.flex-item-8 { -webkit-flex: 8; -moz-flex: 8; -ms-flex: 8; flex: 8; padding: 0 10px; }
-.flex-item-9 { -webkit-flex: 9; -moz-flex: 9; -ms-flex: 9; flex: 9; padding: 0 10px; }
-
-/* Grafana */
-.grafana-container {
- margin-top: 20px;
- height: 64px;
- background:url(./assets/loading.gif) center center no-repeat;
-}
-.grafana {
- width: 100%;
- min-height: 600px;
-}
-
-/* Progressbar */
-.progress-bar {
- background-image: none !important;
-}
-.progress-bar-info {
- background-color: $oa-color-blue;
-}
-.progress-bar-freespace {
- background-color: #ddd;
-}
-.progress-bar-stolenspace {
- background-color: #aaa;
-}
-.progress-bar-outer{
- margin-top: 5px !important;
-}
-.progress-bar-outer div {
- border-radius: 31px;
- background-color: #ffffff;
- border: 1px solid #ccc;
- box-shadow: 0 0 0 0;
- -webkit-box-shadow: 0 0 0 0;
- -moz-box-shadow: 0 0 0 0;
- margin: 0;
- height: 16px;
-}
-.progress-bar-outer div div {
- background-color: #0091d9;
-}
-.progress-bar-outer div div span {
- position: relative;
- top: -3px;
-}
-.oaprogress {
- position: relative;
- margin-bottom: 0;
-}
-.oaprogress div.progress-bar {
- position: static;
-}
-.oaprogress span {
- position: absolute;
- display: block;
- width: 100%;
- color: black;
- font-weight: normal;
-}
-
-tags-input .tags {
- border-radius: 4px;
- border: 1px solid #ccc;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-/* TagForm */
-.tag-form label {
- display: block;
- margin-bottom: 6px;
- line-height: 19px;
- font-weight: 400;
- font-size: 13px;
- color: #333;
- text-align: left;
- white-space: normal;
-}
-
-/* Trees */
-.tree {
- min-height: 20px;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
-}
-.tree>ul {
- padding-left: 0;
-}
-.tree ul ul {
- padding-left: 34px;
- padding-top: 10px;
-}
-.tree li {
- list-style-type: none;
- margin: 0;
- padding: 5px;
- position: relative;
-}
-.tree li span {
- -moz-border-radius: 5px;
- -webkit-border-radius: 5px;
- border: 1px dotted #999;
- border-radius: 5px;
- display: inline-block;
- padding: 3px 8px;
- text-decoration: none;
- -webkit-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
- -moz-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
- -o-transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
- transition: color .2s ease .1s,background-color .2s ease .1s,border-color .3s ease .2s;
-}
-.tree>ul>li::after,
-.tree>ul>li:before {
- border: 0;
-}
-.tree li:after,
-.tree li:before {
- content: '';
- left: -20px;
- position: absolute;
- right: auto;
-}
-.tree li:before {
- border-left: 1px solid #999;
- bottom: 50px;
- height: 100%;
- top: -11px;
- width: 1px;
- -webkit-transition: "border-color 0.1s ease 0.1s";
- -moz-transition: "border-color 0.1s ease 0.1s";
- -o-transition: "border-color 0.1s ease 0.1s";
- transition: "border-color 0.1s ease 0.1s";
-}
-.tree li:after {
- border-top: 1px solid #999;
- height: 20px;
- top: 18px;
- width: 25px;
-}
-.tree li:last-child::before {
- height: 30px;
-}
-
-.scrollable-menu {
- height: auto;
- max-height: 200px;
- overflow-x: hidden;
-}
-
-.toggle, .toggle-on, .toggle-off {
- border-radius: 20px;
-}
-
-.toggle .toggle-handle {
- border-radius: 20px;
-}
-
-/* CSS Fix */
-a {
- cursor: pointer;
-}
-form .input-group-addon {
- color: #a2a2a2 !important;
- background-color: transparent;
-}
-uib-accordion .panel-title,
-.panel .accordion-title {
- font-size: 14px !important;
-}
-.panel-body h2:first-child {
- margin-top: 0;
-}
-.actions {
- padding-bottom: 10px;
-}
-.pull-left {
- float: left;
-}
-.code-clogs {
- display: block;
- padding: 9px;
- margin: 0 0 10px;
- font-size: 13px;
- line-height: 1.42857143;
- color: #333;
- word-break: break-all;
- word-wrap: break-word;
- background-color: #f5f5f5;
- border: 1px solid #ccc;
- border-radius: 4px;
- font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
-}
-.degree-sign:after {
- content: "\00B0 C"!important;
-}
-.formactions.well {
- overflow: auto;
- padding: 10px 20px;
-}
-.disabled {
- pointer-events: none;
-}
-.clickable {
- cursor: pointer;
-}
-.non-clickable {
- cursor: initial;
-}
-.locked {
- cursor: default!important;
-}
-.list-nomargin {
- margin: 0;
-}
-
-.has-error .has-error-btn {
- background-color: #f2dede;
- border-color: #a94442;
-}
-
-.has-error .has-error-btn:disabled:hover {
- background-color: #f2dede;
- border-color: #a94442;
-}
-
-/* If javascript is disabled. */
-.noscript {
- padding-top: 5em;
-}
-.noscript p {
- color: #777;
-}
-
-/* Notifications */
-
-.notification div.img-circle {
- width: 50px;
- height: 50px;
- position: relative;
-}
-.notification.info div.img-circle {
- background-color: #5bc0de;
-}
-.notification.error div.img-circle {
- background-color: #d9534f;
-}
-.notification.success div.img-circle {
- background-color: #5cb85c;
-}
-.notification.warning div.img-circle {
- background-color: #f0ad4e;
-}
-
-.notification .icon {
- background-repeat: no-repeat;
- background-image: url('./assets/notification-icons.png') !important;
- height: 36px;
- width: 36px;
- position: absolute;
- margin: 7px;
-}
-.notification.info .icon {
- background-position: -36px 0;
-}
-.notification.error .icon {
- background-position: -108px 0;
-}
-.notification.success .icon {
- background-position: 0 0;
-}
-.notification.warning .icon {
- background-position: -72px 0;
-}
-
-.required {
- color: #d04437;
-}
-
-/* oa-helper */
-oa-helper i {
- color: $oa-color-blue;
- cursor: pointer;
-}
-
-.page-footer {
- font-size: 12px;
- color: #777;
- text-align: center;
- margin-left: 150px;
- margin-right: 150px;
- margin-top: 50px;
- margin-bottom: 50px;
-}
-
-hr.oa-hr-small {
- margin-top: 5px;
- margin-bottom: 5px;
-}
-
-.table>thead>tr>th.rbd-striping-object{
- min-width: 60px;
-}
-.table>thead>tr>th.rbd-striping-stripe {
- min-width: 100px;
-}
-.rbd-striping-column-separator {
- width: 1px;
-}
-
-.table>tbody>tr>td.rbd-striping-cell-top {
- border-top: 1px solid #ccc;
- border-left: 1px solid #ccc;
- border-right: 1px solid #ccc;
-}
-.table>tbody>tr>td.rbd-striping-cell-center {
- border-top: 1px dashed #ccc;
- border-left: 1px solid #ccc;
- border-right: 1px solid #ccc;
-}
-.table>tbody>tr>td.rbd-striping-cell-bottom {
- border-bottom: 1px solid #ccc;
- border-left: 1px solid #ccc;
- border-right: 1px solid #ccc;
-}
-
-.dropdown-submenu {
- position: relative;
-}
-
-.dropdown-submenu>.dropdown-menu {
- top: 0;
- left: 100%;
- margin-top: -6px;
- margin-left: -1px;
- -webkit-border-radius: 0 6px 6px 6px;
- -moz-border-radius: 0 6px 6px;
- border-radius: 0 6px 6px 6px;
-}
-
-.dropdown-submenu:hover>.dropdown-menu {
- display: block;
-}
-
-.dropdown-submenu>a:after {
- display: block;
- content: " ";
- float: right;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- border-width: 5px 0 5px 5px;
- border-left-color: $oa-color-blue;
- margin-top: 5px;
- margin-right: -10px;
-}
-
-.dropdown-submenu:hover>a:after {
- border-left-color: $oa-color-blue;
-}
-
-.dropdown-submenu.pull-left {
- float: none;
-}
-
-.dropdown-submenu.pull-left>.dropdown-menu {
- left: -100%;
- margin-left: 10px;
- -webkit-border-radius: 6px 0 6px 6px;
- -moz-border-radius: 6px 0 6px 6px;
- border-radius: 6px 0 6px 6px;
-}
-
-/* Forms */
-.form-group>.control-label>span.required {
- @extend .fa;
- @extend .fa-asterisk;
- @extend .required;
- font-size: 6px;
- padding-left: 4px;
- vertical-align: text-top;
-}
+++ /dev/null
-/**
- * This file includes polyfills needed by Angular and is loaded before the app.
- * You can add your own extra polyfills to this file.
- *
- * This file is divided into 2 sections:
- * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
- * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
- * file.
- *
- * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
- * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
- * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
- *
- * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
- */
-
-/***************************************************************************************************
- * BROWSER POLYFILLS
- */
-
-/** IE9, IE10 and IE11 requires all of the following polyfills. **/
-import 'core-js/es6/array';
-import 'core-js/es6/date';
-import 'core-js/es6/function';
-import 'core-js/es6/map';
-import 'core-js/es6/math';
-import 'core-js/es6/number';
-import 'core-js/es6/object';
-import 'core-js/es6/parse-float';
-import 'core-js/es6/parse-int';
-import 'core-js/es6/regexp';
-import 'core-js/es6/set';
-import 'core-js/es6/string';
-import 'core-js/es6/symbol';
-import 'core-js/es6/weak-map';
-import 'core-js/es7/object';
-
-/** IE10 and IE11 requires the following for NgClass support on SVG elements */
-// import 'classlist.js'; // Run `npm install --save classlist.js`.
-
-/** IE10 and IE11 requires the following for the Reflect API. */
-// import 'core-js/es6/reflect';
-
-/** Evergreen browsers require these. **/
-// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
-import 'core-js/es7/reflect';
-
-/**
- * Required to support Web Animations `@angular/platform-browser/animations`.
- * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
- **/
-// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
-
-/***************************************************************************************************
- * Zone JS is required by Angular itself.
- */
-import 'zone.js/dist/zone'; // Included with Angular CLI.
-
-/***************************************************************************************************
- * APPLICATION IMPORTS
- */
-
-/**
- * Date, currency, decimal and percent pipes.
- * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
- */
-// import 'intl'; // Run `npm install --save intl`.
-/**
- * Need to import at least one locale-data with intl.
- */
-// import 'intl/locale-data/jsonp/en';
+++ /dev/null
-/* You can add global styles to this file, and also import other style files */
-@import './openattic-theme.scss';
+++ /dev/null
-.chart-container {
- position: absolute;
- margin: auto;
- cursor: pointer;
- overflow: visible;
-}
-
-canvas {
- -moz-user-select: none;
- -webkit-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-
-.chartjs-tooltip {
- opacity: 0;
- position: absolute;
- background: rgba(0, 0, 0, 0.7);
- color: white;
- border-radius: 3px;
- -webkit-transition: all 0.1s ease;
- transition: all 0.1s ease;
- pointer-events: none;
- font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif !important;
-
- -webkit-transform: translate(-50%, 0);
- transform: translate(-50%, 0);
-
- &.transform-left {
- transform: translate(-10%, 0);
-
- &::after {
- left: 10%;
- }
- }
-
- &.transform-right {
- transform: translate(-90%, 0);
-
- &::after {
- left: 90%;
- }
- }
-}
-
-.chartjs-tooltip::after {
- content: ' ';
- position: absolute;
- top: 100%; /* At the bottom of the tooltip */
- left: 50%;
- margin-left: -5px;
- border-width: 5px;
- border-style: solid;
- border-color: black transparent transparent transparent;
-}
-
-::ng-deep .chartjs-tooltip-key {
- display: inline-block;
- width: 10px;
- height: 10px;
- margin-right: 10px;
-}
+++ /dev/null
-/* tslint:disable:ordered-imports */
-// This file is required by karma.conf.js and loads recursively all the .spec and framework files
-
-import 'zone.js/dist/long-stack-trace-zone';
-import 'zone.js/dist/proxy.js';
-import 'zone.js/dist/sync-test';
-import 'zone.js/dist/jasmine-patch';
-import 'zone.js/dist/async-test';
-import 'zone.js/dist/fake-async-test';
-import { getTestBed } from '@angular/core/testing';
-import {
- BrowserDynamicTestingModule,
- platformBrowserDynamicTesting
-} from '@angular/platform-browser-dynamic/testing';
-
-// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
-declare const __karma__: any;
-declare const require: any;
-
-// Prevent Karma from running prematurely.
-__karma__.loaded = function () {};
-
-// First, initialize the Angular testing environment.
-getTestBed().initTestEnvironment(
- BrowserDynamicTestingModule,
- platformBrowserDynamicTesting()
-);
-// Then we find all the tests.
-const context = require.context('./', true, /\.spec\.ts$/);
-// And load the modules.
-context.keys().map(context);
-// Finally, start Karma to run the tests.
-__karma__.start();
+++ /dev/null
-{
- "extends": "../tsconfig.json",
- "compilerOptions": {
- "outDir": "../out-tsc/app",
- "baseUrl": "./",
- "module": "es2015",
- "types": []
- },
- "exclude": [
- "test.ts",
- "**/*.spec.ts"
- ]
-}
+++ /dev/null
-{
- "extends": "../tsconfig.json",
- "compilerOptions": {
- "outDir": "../out-tsc/spec",
- "baseUrl": "./",
- "module": "commonjs",
- "target": "es5",
- "types": [
- "jasmine",
- "node"
- ]
- },
- "files": [
- "test.ts"
- ],
- "include": [
- "**/*.spec.ts",
- "**/*.d.ts"
- ]
-}
+++ /dev/null
-/* SystemJS module definition */
-declare var module: NodeModule;
-interface NodeModule {
- id: string;
-}
+++ /dev/null
-{
- "compileOnSave": false,
- "compilerOptions": {
- "outDir": "./dist/out-tsc",
- "sourceMap": true,
- "declaration": false,
- "moduleResolution": "node",
- "emitDecoratorMetadata": true,
- "experimentalDecorators": true,
- "target": "es5",
- "typeRoots": [
- "node_modules/@types"
- ],
- "lib": [
- "es2017",
- "dom"
- ]
- }
-}
+++ /dev/null
-{
- "rulesDirectory": [
- "node_modules/codelyzer"
- ],
- "extends": [
- "tslint-eslint-rules"
- ],
- "rules": {
- "no-consecutive-blank-lines": true,
- "arrow-return-shorthand": true,
- "callable-types": true,
- "class-name": true,
- "comment-format": [
- true,
- "check-space"
- ],
- "curly": true,
- "eofline": true,
- "forin": true,
- "import-blacklist": [
- true,
- "rxjs",
- "rxjs/Rx"
- ],
- "import-spacing": true,
- "indent": [
- true,
- "spaces"
- ],
- "interface-over-type-literal": true,
- "label-position": true,
- "max-line-length": [
- true,
- 100
- ],
- "member-access": false,
- "member-ordering": [
- true,
- {
- "order": [
- "static-field",
- "instance-field",
- "static-method",
- "instance-method"
- ]
- }
- ],
- "no-arg": true,
- "no-bitwise": true,
- "no-console": [
- true,
- "debug",
- "info",
- "time",
- "timeEnd",
- "trace"
- ],
- "no-construct": true,
- "no-debugger": true,
- "no-duplicate-super": true,
- "no-empty": false,
- "no-empty-interface": true,
- "no-eval": true,
- "no-inferrable-types": [
- true,
- "ignore-params"
- ],
- "no-misused-new": true,
- "no-non-null-assertion": true,
- "no-shadowed-variable": true,
- "no-string-literal": false,
- "no-string-throw": true,
- "no-switch-case-fall-through": true,
- "no-trailing-whitespace": true,
- "no-unnecessary-initializer": true,
- "no-unused-expression": true,
- "no-use-before-declare": true,
- "no-var-keyword": true,
- "object-literal-sort-keys": false,
- "one-line": [
- true,
- "check-open-brace",
- "check-catch",
- "check-else",
- "check-whitespace"
- ],
- "prefer-const": true,
- "quotemark": [
- true,
- "single"
- ],
- "radix": true,
- "semicolon": [
- true,
- "always"
- ],
- "triple-equals": [
- true,
- "allow-null-check"
- ],
- "typedef-whitespace": [
- true,
- {
- "call-signature": "nospace",
- "index-signature": "nospace",
- "parameter": "nospace",
- "property-declaration": "nospace",
- "variable-declaration": "nospace"
- }
- ],
- "unified-signatures": true,
- "variable-name": [
- true,
- "check-format",
- "allow-snake-case"
- ],
- "whitespace": [
- true,
- "check-branch",
- "check-decl",
- "check-operator",
- "check-separator",
- "check-type",
- "check-module"
- ],
- "directive-selector": [
- true,
- "attribute",
- "cd",
- "camelCase"
- ],
- "component-selector": [
- true,
- "element",
- "cd",
- "kebab-case"
- ],
- "angular-whitespace": [true, "check-interpolation", "check-semicolon"],
- "no-output-on-prefix": true,
- "use-input-property-decorator": true,
- "use-output-property-decorator": true,
- "use-host-property-decorator": true,
- "no-attribute-parameter-decorator": true,
- "no-input-rename": true,
- "no-output-rename": true,
- "use-life-cycle-interface": true,
- "use-pipe-transform-interface": true,
- "component-class-suffix": true,
- "directive-class-suffix": true,
- "no-forward-ref": true,
- "no-output-named-after-standard-event": true,
- "ordered-imports": true,
- "no-extra-semi": true,
- "ter-no-irregular-whitespace": true,
- "no-multi-spaces": true,
- "brace-style": [
- true,
- "1tbs",
- {
- "allowSingleLine": false
- }
- ],
- "ter-indent": [
- true,
- 2,
- {
- "SwitchCase": 1,
- "FunctionDeclaration": {
- "body": 1,
- "parameters": "first"
- },
- "FunctionExpression": {
- "body": 1,
- "parameters": "first"
- }
- }
- ],
- "space-in-parens": [true, "never"]
- }
-}
+++ /dev/null
-# -*- coding: utf-8 -*-
-"""
-openATTIC mgr plugin (based on CherryPy)
-"""
-from __future__ import absolute_import
-
-import errno
-import os
-import socket
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin
-try:
- import cherrypy
-except ImportError:
- # To be picked up and reported by .can_run()
- cherrypy = None
-
-from mgr_module import MgrModule, MgrStandbyModule
-
-if 'COVERAGE_ENABLED' in os.environ:
- import coverage
- _cov = coverage.Coverage(config_file="{}/.coveragerc".format(os.path.dirname(__file__)))
- _cov.start()
-
-# pylint: disable=wrong-import-position
-from . import logger, mgr
-from .controllers.auth import Auth
-from .tools import load_controllers, json_error_page, SessionExpireAtBrowserCloseTool, \
- NotificationQueue, RequestLoggingTool
-from .settings import options_command_list, handle_option_command
-
-
-# cherrypy likes to sys.exit on error. don't let it take us down too!
-# pylint: disable=W0613
-def os_exit_noop(*args):
- pass
-
-
-# pylint: disable=W0212
-os._exit = os_exit_noop
-
-
-def prepare_url_prefix(url_prefix):
- """
- return '' if no prefix, or '/prefix' without slash in the end.
- """
- url_prefix = urljoin('/', url_prefix)
- return url_prefix.rstrip('/')
-
-
-class Module(MgrModule):
- """
- dashboard module entrypoint
- """
-
- COMMANDS = [
- {
- 'cmd': 'dashboard set-login-credentials '
- 'name=username,type=CephString '
- 'name=password,type=CephString',
- 'desc': 'Set the login credentials',
- 'perm': 'w'
- },
- {
- 'cmd': 'dashboard set-session-expire '
- 'name=seconds,type=CephInt',
- 'desc': 'Set the session expire timeout',
- 'perm': 'w'
- }
- ]
- COMMANDS.extend(options_command_list())
-
- @property
- def url_prefix(self):
- return self._url_prefix
-
- def __init__(self, *args, **kwargs):
- super(Module, self).__init__(*args, **kwargs)
- mgr.init(self)
- self._url_prefix = ''
-
- @classmethod
- def can_run(cls):
- if cherrypy is None:
- return False, "Missing dependency: cherrypy"
-
- if not os.path.exists(cls.get_frontend_path()):
- return False, "Frontend assets not found: incomplete build?"
-
- return True, ""
-
- @classmethod
- def get_frontend_path(cls):
- current_dir = os.path.dirname(os.path.abspath(__file__))
- return os.path.join(current_dir, 'frontend/dist')
-
- def configure_cherrypy(self):
- server_addr = self.get_localized_config('server_addr', '::')
- server_port = self.get_localized_config('server_port', '8080')
- if server_addr is None:
- raise RuntimeError(
- 'no server_addr configured; '
- 'try "ceph config-key put mgr/{}/{}/server_addr <ip>"'
- .format(self.module_name, self.get_mgr_id()))
- self.log.info('server_addr: %s server_port: %s', server_addr,
- server_port)
-
- self._url_prefix = prepare_url_prefix(self.get_config('url_prefix',
- default=''))
-
- # Initialize custom handlers.
- cherrypy.tools.authenticate = cherrypy.Tool('before_handler', Auth.check_auth)
- cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
- cherrypy.tools.request_logging = RequestLoggingTool()
-
- # Apply the 'global' CherryPy configuration.
- config = {
- 'engine.autoreload.on': False,
- 'server.socket_host': server_addr,
- 'server.socket_port': int(server_port),
- 'error_page.default': json_error_page,
- 'tools.request_logging.on': True
- }
- cherrypy.config.update(config)
-
- config = {
- '/': {
- 'tools.staticdir.on': True,
- 'tools.staticdir.dir': self.get_frontend_path(),
- 'tools.staticdir.index': 'index.html'
- }
- }
-
- # Publish the URI that others may use to access the service we're
- # about to start serving
- self.set_uri("http://{0}:{1}{2}/".format(
- socket.getfqdn() if server_addr == "::" else server_addr,
- server_port,
- self.url_prefix
- ))
-
- cherrypy.tree.mount(Module.ApiRoot(self), '{}/api'.format(self.url_prefix))
- cherrypy.tree.mount(Module.StaticRoot(), '{}/'.format(self.url_prefix), config=config)
-
- def serve(self):
- if 'COVERAGE_ENABLED' in os.environ:
- _cov.start()
- self.configure_cherrypy()
-
- cherrypy.engine.start()
- NotificationQueue.start_queue()
- logger.info('Waiting for engine...')
- cherrypy.engine.block()
- if 'COVERAGE_ENABLED' in os.environ:
- _cov.stop()
- _cov.save()
- logger.info('Engine done')
-
- def shutdown(self):
- super(Module, self).shutdown()
- logger.info('Stopping server...')
- NotificationQueue.stop()
- cherrypy.engine.exit()
- logger.info('Stopped server')
-
- def handle_command(self, cmd):
- res = handle_option_command(cmd)
- if res[0] != -errno.ENOSYS:
- return res
- if cmd['prefix'] == 'dashboard set-login-credentials':
- Auth.set_login_credentials(cmd['username'], cmd['password'])
- return 0, 'Username and password updated', ''
- elif cmd['prefix'] == 'dashboard set-session-expire':
- self.set_config('session-expire', str(cmd['seconds']))
- return 0, 'Session expiration timeout updated', ''
-
- return (-errno.EINVAL, '', 'Command not found \'{0}\''
- .format(cmd['prefix']))
-
- def notify(self, notify_type, notify_id):
- NotificationQueue.new_notification(notify_type, notify_id)
-
- class ApiRoot(object):
-
- _cp_config = {
- 'tools.sessions.on': True,
- 'tools.authenticate.on': True
- }
-
- def __init__(self, mgrmod):
- self.ctrls = load_controllers()
- logger.debug('Loaded controllers: %s', self.ctrls)
-
- first_level_ctrls = [ctrl for ctrl in self.ctrls
- if '/' not in ctrl._cp_path_]
- multi_level_ctrls = set(self.ctrls).difference(first_level_ctrls)
-
- for ctrl in first_level_ctrls:
- logger.info('Adding controller: %s -> /api/%s', ctrl.__name__,
- ctrl._cp_path_)
- inst = ctrl()
- setattr(Module.ApiRoot, ctrl._cp_path_, inst)
-
- for ctrl in multi_level_ctrls:
- path_parts = ctrl._cp_path_.split('/')
- path = '/'.join(path_parts[:-1])
- key = path_parts[-1]
- parent_ctrl_classes = [c for c in self.ctrls
- if c._cp_path_ == path]
- if len(parent_ctrl_classes) != 1:
- logger.error('No parent controller found for %s! '
- 'Please check your path in the ApiController '
- 'decorator!', ctrl)
- else:
- inst = ctrl()
- setattr(parent_ctrl_classes[0], key, inst)
-
- @cherrypy.expose
- def index(self):
- tpl = """API Endpoints:<br>
- <ul>
- {lis}
- </ul>
- """
- endpoints = ['<li><a href="{}">{}</a></li>'.format(ctrl._cp_path_, ctrl.__name__) for
- ctrl in self.ctrls]
- return tpl.format(lis='\n'.join(endpoints))
-
- class StaticRoot(object):
- pass
-
-
-class StandbyModule(MgrStandbyModule):
- def serve(self):
- server_addr = self.get_localized_config('server_addr', '::')
- server_port = self.get_localized_config('server_port', '7000')
- if server_addr is None:
- msg = 'no server_addr configured; try "ceph config-key set ' \
- 'mgr/dashboard/server_addr <ip>"'
- raise RuntimeError(msg)
- self.log.info("server_addr: %s server_port: %s",
- server_addr, server_port)
- cherrypy.config.update({
- 'server.socket_host': server_addr,
- 'server.socket_port': int(server_port),
- 'engine.autoreload.on': False
- })
-
- module = self
-
- class Root(object):
- @cherrypy.expose
- def index(self):
- active_uri = module.get_active_uri()
- if active_uri:
- module.log.info("Redirecting to active '%s'", active_uri)
- raise cherrypy.HTTPRedirect(active_uri)
- else:
- template = """
- <html>
- <!-- Note: this is only displayed when the standby
- does not know an active URI to redirect to, otherwise
- a simple redirect is returned instead -->
- <head>
- <title>Ceph</title>
- <meta http-equiv="refresh" content="{delay}">
- </head>
- <body>
- No active ceph-mgr instance is currently running
- the dashboard. A failover may be in progress.
- Retrying in {delay} seconds...
- </body>
- </html>
- """
- return template.format(delay=5)
-
- url_prefix = prepare_url_prefix(self.get_config('url_prefix',
- default=''))
- cherrypy.tree.mount(Root(), "{}/".format(url_prefix), {})
- self.log.info("Starting engine...")
- cherrypy.engine.start()
- self.log.info("Waiting for engine...")
- cherrypy.engine.wait(state=cherrypy.engine.states.STOPPED)
- self.log.info("Engine done.")
-
- def shutdown(self):
- self.log.info("Stopping server...")
- cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
- cherrypy.engine.stop()
- self.log.info("Stopped server")
+++ /dev/null
-astroid==1.6.1
-attrs==17.4.0
-backports.functools-lru-cache==1.4
-cheroot==6.0.0
-CherryPy==13.1.0
-configparser==3.5.0
-coverage==4.4.2
-enum34==1.1.6
-funcsigs==1.0.2
-isort==4.2.15
-lazy-object-proxy==1.3.1
-mccabe==0.6.1
-mock==2.0.0
-more-itertools==4.1.0
-pbr==3.1.1
-pluggy==0.6.0
-portend==2.2
-py==1.5.2
-pycodestyle==2.3.1
-pycparser==2.18
-pylint==1.8.2
-pytest==3.3.2
-pytest-cov==2.5.1
-python-bcrypt==0.3.2
-pytz==2017.3
-requests==2.18.4
-singledispatch==3.4.0.3
-six==1.11.0
-tempora==1.10
-tox==2.9.1
-virtualenv==15.1.0
-wrapt==1.10.11
+++ /dev/null
-#!/usr/bin/env bash
-
-# run from ./
-
-# creating temp directory to store virtualenv and teuthology
-TEMP_DIR=`mktemp -d`
-
-get_cmake_variable() {
- local variable=$1
- grep "$variable" CMakeCache.txt | cut -d "=" -f 2
-}
-
-read -r -d '' TEUTHOLOFY_PY_REQS <<EOF
-apache-libcloud==2.2.1 \
-asn1crypto==0.22.0 \
-bcrypt==3.1.4 \
-certifi==2018.1.18 \
-cffi==1.10.0 \
-chardet==3.0.4 \
-configobj==5.0.6 \
-cryptography==2.1.4 \
-enum34==1.1.6 \
-gevent==1.2.2 \
-greenlet==0.4.13 \
-idna==2.5 \
-ipaddress==1.0.18 \
-Jinja2==2.9.6 \
-manhole==1.5.0 \
-MarkupSafe==1.0 \
-netaddr==0.7.19 \
-packaging==16.8 \
-paramiko==2.4.0 \
-pexpect==4.4.0 \
-psutil==5.4.3 \
-ptyprocess==0.5.2 \
-pyasn1==0.2.3 \
-pycparser==2.17 \
-PyNaCl==1.2.1 \
-pyparsing==2.2.0 \
-python-dateutil==2.6.1 \
-PyYAML==3.12 \
-requests==2.18.4 \
-six==1.10.0 \
-urllib3==1.22
-EOF
-
-
-CURR_DIR=`pwd`
-
-cd $TEMP_DIR
-
-virtualenv --python=/usr/bin/python venv
-source venv/bin/activate
-eval pip install $TEUTHOLOFY_PY_REQS
-pip install -r $CURR_DIR/requirements.txt
-deactivate
-
-git clone https://github.com/ceph/teuthology.git
-
-cd $CURR_DIR
-cd ../../../../build
-
-CEPH_MGR_PY_VERSION_MAJOR=$(get_cmake_variable MGR_PYTHON_VERSION | cut -d '.' -f1)
-if [ -n "$CEPH_MGR_PY_VERSION_MAJOR" ]; then
- CEPH_PY_VERSION_MAJOR=${CEPH_MGR_PY_VERSION_MAJOR}
-else
- if [ $(get_cmake_variable WITH_PYTHON2) = ON ]; then
- CEPH_PY_VERSION_MAJOR=2
- else
- CEPH_PY_VERSION_MAJOR=3
- fi
-fi
-
-export COVERAGE_ENABLED=true
-export COVERAGE_FILE=.coverage.mgr.dashboard
-
-MGR=2 RGW=1 ../src/vstart.sh -n -d
-sleep 10
-
-source $TEMP_DIR/venv/bin/activate
-BUILD_DIR=`pwd`
-
-if [ "$#" -gt 0 ]; then
- TEST_CASES=""
- for t in "$@"; do
- TEST_CASES="$TESTS_CASES $t"
- done
-else
- TEST_CASES=`for i in \`ls $BUILD_DIR/../qa/tasks/mgr/dashboard_v2/test_*\`; do F=$(basename $i); M="${F%.*}"; echo -n " tasks.mgr.dashboard_v2.$M"; done`
- TEST_CASES="tasks.mgr.test_dashboard_v2 $TEST_CASES"
-fi
-
-export PATH=$BUILD_DIR/bin:$PATH
-export LD_LIBRARY_PATH=$BUILD_DIR/lib/cython_modules/lib.${CEPH_PY_VERSION_MAJOR}/:$BUILD_DIR/lib
-export PYTHONPATH=$TEMP_DIR/teuthology:$BUILD_DIR/../qa:$BUILD_DIR/lib/cython_modules/lib.${CEPH_PY_VERSION_MAJOR}/
-eval python ../qa/tasks/vstart_runner.py $TEST_CASES
-
-deactivate
-killall ceph-mgr
-sleep 10
-../src/stop.sh
-sleep 5
-
-cd $CURR_DIR
-rm -rf $TEMP_DIR
-
+++ /dev/null
-#!/usr/bin/env bash
-
-set -e
-
-cd $CEPH_ROOT/src/pybind/mgr/dashboard_v2/frontend
-
-npm run build -- --prod
-npm run test -- --browsers PhantomJS --watch=false
-npm run lint
+++ /dev/null
-#!/usr/bin/env bash
-
-# run from ./ or from ../
-: ${MGR_DASHBOARD_V2_VIRTUALENV:=/tmp/mgr-dashboard_v2-virtualenv}
-: ${WITH_PYTHON3:=ON}
-test -d dashboard_v2 && cd dashboard_v2
-
-if [ -e tox.ini ]; then
- TOX_PATH=`readlink -f tox.ini`
-else
- TOX_PATH=`readlink -f $(dirname $0)/tox.ini`
-fi
-
-if [ -z $CEPH_BUILD_DIR ]; then
- export CEPH_BUILD_DIR=$(dirname ${TOX_PATH})
-fi
-
-source ${MGR_DASHBOARD_V2_VIRTUALENV}/bin/activate
-
-if [ "$WITH_PYTHON3" = "ON" ]; then
- ENV_LIST="cov-init,py27,py3,cov-report,lint"
-else
- ENV_LIST="cov-init,py27,cov-report,lint"
-fi
-
-tox -c ${TOX_PATH} -e $ENV_LIST
-
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import time
-import collections
-from collections import defaultdict
-
-from .. import mgr
-
-
-class CephService(object):
- @classmethod
- def get_service_map(cls, service_name):
- service_map = {}
- for server in mgr.list_servers():
- for service in server['services']:
- if service['type'] == service_name:
- if server['hostname'] not in service_map:
- service_map[server['hostname']] = {
- 'server': server,
- 'services': []
- }
- inst_id = service['id']
- metadata = mgr.get_metadata(service_name, inst_id)
- status = mgr.get_daemon_status(service_name, inst_id)
- service_map[server['hostname']]['services'].append({
- 'id': inst_id,
- 'type': service_name,
- 'hostname': server['hostname'],
- 'metadata': metadata,
- 'status': status
- })
- return service_map
-
- @classmethod
- def get_service_list(cls, service_name):
- service_map = cls.get_service_map(service_name)
- return [svc for _, svcs in service_map.items() for svc in svcs['services']]
-
- @classmethod
- def get_service(cls, service_name, service_id):
- for server in mgr.list_servers():
- for service in server['services']:
- if service['type'] == service_name:
- inst_id = service['id']
- if inst_id == service_id:
- metadata = mgr.get_metadata(service_name, inst_id)
- status = mgr.get_daemon_status(service_name, inst_id)
- return {
- 'id': inst_id,
- 'type': service_name,
- 'hostname': server['hostname'],
- 'metadata': metadata,
- 'status': status
- }
- return None
-
- @classmethod
- def get_pool_list(cls, application=None):
- osd_map = mgr.get('osd_map')
- if not application:
- return osd_map['pools']
- return [pool for pool in osd_map['pools']
- if application in pool.get('application_metadata', {})]
-
- @classmethod
- def get_pool_list_with_stats(cls, application=None):
- # pylint: disable=too-many-locals
- pools = cls.get_pool_list(application)
-
- 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))
-
- for pool in pools:
- pool['pg_status'] = pg_summary['by_pool'][pool['pool'].__str__()]
- stats = pool_stats[pool['pool']]
- s = {}
-
- def get_rate(series):
- if len(series) >= 2:
- return (float(series[0][1]) - float(series[1][1])) / \
- (float(series[0][0]) - float(series[1][0]))
- return 0
-
- for stat_name, stat_series in stats.items():
- s[stat_name] = {
- 'latest': stat_series[0][1],
- 'rate': get_rate(stat_series),
- 'series': [i for i in stat_series]
- }
- pool['stats'] = s
- pools_w_stats.append(pool)
- return pools_w_stats
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import errno
-import inspect
-from six import add_metaclass
-
-from . import mgr
-
-
-class Options(object):
- """
- If you need to store some configuration value please add the config option
- name as a class attribute to this class.
-
- Example::
-
- GRAFANA_API_HOST = ('localhost', str)
- GRAFANA_API_PORT = (3000, int)
- """
- pass
-
-
-class SettingsMeta(type):
- def __getattr__(cls, attr):
- default, stype = getattr(Options, attr)
- return stype(mgr.get_config(attr, default))
-
- def __setattr__(cls, attr, value):
- if not attr.startswith('_') and hasattr(Options, attr):
- mgr.set_config(attr, str(value))
- else:
- setattr(SettingsMeta, attr, value)
-
-
-# pylint: disable=no-init
-@add_metaclass(SettingsMeta)
-class Settings(object):
- pass
-
-
-def _options_command_map():
- def filter_attr(member):
- return not inspect.isroutine(member)
-
- cmd_map = {}
- for option, value in inspect.getmembers(Options, filter_attr):
- if option.startswith('_'):
- continue
- key_get = 'dashboard get-{}'.format(option.lower().replace('_', '-'))
- key_set = 'dashboard set-{}'.format(option.lower().replace('_', '-'))
- cmd_map[key_get] = {'name': option, 'type': None}
- cmd_map[key_set] = {'name': option, 'type': value[1]}
- return cmd_map
-
-
-_OPTIONS_COMMAND_MAP = _options_command_map()
-
-
-def options_command_list():
- """
- This function generates a list of ``get`` and ``set`` commands
- for each declared configuration option in class ``Options``.
- """
- def py2ceph(pytype):
- if pytype == str:
- return 'CephString'
- elif pytype == int:
- return 'CephInt'
- return 'CephString'
-
- cmd_list = []
- for cmd, opt in _OPTIONS_COMMAND_MAP.items():
- if not opt['type']:
- cmd_list.append({
- 'cmd': '{}'.format(cmd),
- 'desc': 'Get the {} option value'.format(opt['name']),
- 'perm': 'r'
- })
- else:
- cmd_list.append({
- 'cmd': '{} name=value,type={}'
- .format(cmd, py2ceph(opt['type'])),
- 'desc': 'Set the {} option value'.format(opt['name']),
- 'perm': 'w'
- })
-
- return cmd_list
-
-
-def handle_option_command(cmd):
- if cmd['prefix'] not in _OPTIONS_COMMAND_MAP:
- return (-errno.ENOSYS, '', "Command not found '{}'".format(cmd['prefix']))
-
- opt = _OPTIONS_COMMAND_MAP[cmd['prefix']]
- if not opt['type']:
- # get option
- return 0, str(getattr(Settings, opt['name'])), ''
-
- # set option
- setattr(Settings, opt['name'], opt['type'](cmd['value']))
- return 0, 'Option {} updated'.format(opt['name']), ''
+++ /dev/null
-# -*- coding: utf-8 -*-
-# pylint: disable=W0212
-from __future__ import absolute_import
-
-import json
-
-import cherrypy
-from cherrypy.test import helper
-
-from ..controllers.auth import Auth
-from ..tools import json_error_page, SessionExpireAtBrowserCloseTool
-
-
-class ControllerTestCase(helper.CPWebCase):
- def __init__(self, *args, **kwargs):
- cherrypy.tools.authenticate = cherrypy.Tool('before_handler', Auth.check_auth)
- cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
- cherrypy.config.update({'error_page.default': json_error_page})
- super(ControllerTestCase, self).__init__(*args, **kwargs)
-
- def _request(self, url, method, data=None):
- if not data:
- b = None
- h = None
- else:
- b = json.dumps(data)
- h = [('Content-Type', 'application/json'),
- ('Content-Length', str(len(b)))]
- self.getPage(url, method=method, body=b, headers=h)
-
- def _get(self, url):
- self._request(url, 'GET')
-
- def _post(self, url, data=None):
- self._request(url, 'POST', data)
-
- def _delete(self, url, data=None):
- self._request(url, 'DELETE', data)
-
- def _put(self, url, data=None):
- self._request(url, 'PUT', data)
-
- def jsonBody(self):
- body_str = self.body.decode('utf-8') if isinstance(self.body, bytes) else self.body
- return json.loads(body_str)
-
- def assertJsonBody(self, data, msg=None):
- """Fail if value != self.body."""
- json_body = self.jsonBody()
- if data != json_body:
- if msg is None:
- msg = 'expected body:\n%r\n\nactual body:\n%r' % (
- data, json_body)
- self._handlewebError(msg)
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import random
-import time
-import unittest
-
-
-from ..tools import NotificationQueue
-
-
-class Listener(object):
- def __init__(self):
- NotificationQueue.register(self.log_type1, 'type1')
- NotificationQueue.register(self.log_type2, 'type2')
- NotificationQueue.register(self.log_type1_3, ['type1', 'type3'])
- NotificationQueue.register(self.log_all)
- self.type1 = []
- self.type2 = []
- self.type1_3 = []
- self.all = []
-
- # these should be ignored by the queue
- NotificationQueue.register(self.log_type1, 'type1')
- NotificationQueue.register(self.log_type1_3, ['type1', 'type3'])
- NotificationQueue.register(self.log_all)
-
- def log_type1(self, val):
- self.type1.append(val)
-
- def log_type2(self, val):
- self.type2.append(val)
-
- def log_type1_3(self, val):
- self.type1_3.append(val)
-
- def log_all(self, val):
- self.all.append(val)
-
- def clear(self):
- self.type1 = []
- self.type2 = []
- self.type1_3 = []
- self.all = []
-
-
-class NotificationQueueTest(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
- cls.listener = Listener()
-
- def setUp(self):
- self.listener.clear()
-
- def test_invalid_register(self):
- with self.assertRaises(Exception) as ctx:
- NotificationQueue.register(None, 1)
- self.assertEqual(str(ctx.exception),
- "types param is neither a string nor a list")
-
- def test_notifications(self):
- NotificationQueue.start_queue()
- NotificationQueue.new_notification('type1', 1)
- NotificationQueue.new_notification('type2', 2)
- NotificationQueue.new_notification('type3', 3)
- NotificationQueue.stop()
- self.assertEqual(self.listener.type1, [1])
- self.assertEqual(self.listener.type2, [2])
- self.assertEqual(self.listener.type1_3, [1, 3])
- self.assertEqual(self.listener.all, [1, 2, 3])
-
- def test_notifications2(self):
- NotificationQueue.start_queue()
- for i in range(0, 600):
- typ = "type{}".format(i % 3 + 1)
- if random.random() < 0.5:
- time.sleep(0.002)
- NotificationQueue.new_notification(typ, i)
- NotificationQueue.stop()
- for i in range(0, 500):
- typ = i % 3 + 1
- if typ == 1:
- self.assertIn(i, self.listener.type1)
- self.assertIn(i, self.listener.type1_3)
- elif typ == 2:
- self.assertIn(i, self.listener.type2)
- elif typ == 3:
- self.assertIn(i, self.listener.type1_3)
- self.assertIn(i, self.listener.all)
-
- self.assertEqual(len(self.listener.type1), 200)
- self.assertEqual(len(self.listener.type2), 200)
- self.assertEqual(len(self.listener.type1_3), 400)
- self.assertEqual(len(self.listener.all), 600)
+++ /dev/null
-from __future__ import absolute_import
-
-import json
-import mock
-
-import cherrypy
-
-from .. import mgr
-from ..controllers.summary import Summary
-from ..controllers.rbd_mirroring import RbdMirror
-from .helper import ControllerTestCase
-
-
-mock_list_servers = [{
- 'hostname': 'ceph-host',
- 'services': [{'id': 3, 'type': 'rbd-mirror'}]
-}]
-
-mock_get_metadata = {
- 'id': 1,
- 'instance_id': 3,
- 'ceph_version': 'ceph version 13.0.0-5719 mimic (dev)'
-}
-
-_status = {
- 1: {
- 'callouts': {},
- 'image_local_count': 5,
- 'image_remote_count': 6,
- 'image_error_count': 7,
- 'image_warning_count': 8,
- 'name': 'pool_name'
- }
-}
-
-mock_get_daemon_status = {
- 'json': json.dumps(_status)
-}
-
-mock_osd_map = {
- 'pools': [{
- 'pool_name': 'rbd',
- 'application_metadata': {'rbd'}
- }]
-}
-
-
-class RbdMirroringControllerTest(ControllerTestCase):
-
- @classmethod
- def setup_server(cls):
- mgr.list_servers.return_value = mock_list_servers
- mgr.get_metadata.return_value = mock_get_metadata
- mgr.get_daemon_status.return_value = mock_get_daemon_status
- mgr.get.side_effect = lambda key: {
- 'osd_map': mock_osd_map,
- 'health': {'json': '{"status": 1}'},
- 'fs_map': {'filesystems': []},
-
- }[key]
- mgr.url_prefix = ''
- mgr.get_mgr_id.return_value = 0
- mgr.have_mon_connection.return_value = True
-
- RbdMirror._cp_config['tools.authenticate.on'] = False # pylint: disable=protected-access
-
- Summary._cp_config['tools.authenticate.on'] = False # pylint: disable=protected-access
-
- cherrypy.tree.mount(RbdMirror(), '/api/test/rbdmirror')
- cherrypy.tree.mount(Summary(), '/api/test/summary')
-
- @mock.patch('dashboard_v2.controllers.rbd_mirroring.rbd')
- def test_default(self, rbd_mock): # pylint: disable=W0613
- self._get('/api/test/rbdmirror')
- result = self.jsonBody()
- self.assertStatus(200)
- self.assertEqual(result['status'], 0)
- for k in ['daemons', 'pools', 'image_error', 'image_syncing', 'image_ready']:
- self.assertIn(k, result['content_data'])
-
- @mock.patch('dashboard_v2.controllers.rbd_mirroring.rbd')
- def test_summary(self, rbd_mock): # pylint: disable=W0613
- """We're also testing `summary`, as it also uses code from `rbd_mirroring.py`"""
- self._get('/api/test/summary')
- self.assertStatus(200)
-
- summary = self.jsonBody()['rbd_mirroring']
- self.assertEqual(summary, {'errors': 0, 'warnings': 1})
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import errno
-import unittest
-
-from .. import mgr
-from .. import settings
-from ..settings import Settings, handle_option_command
-
-
-class SettingsTest(unittest.TestCase):
- CONFIG_KEY_DICT = {}
-
- @classmethod
- def setUpClass(cls):
- # pylint: disable=protected-access
- settings.Options.GRAFANA_API_HOST = ('localhost', str)
- settings.Options.GRAFANA_API_PORT = (3000, int)
- settings._OPTIONS_COMMAND_MAP = settings._options_command_map()
-
- @classmethod
- def mock_set_config(cls, attr, val):
- cls.CONFIG_KEY_DICT[attr] = val
-
- @classmethod
- def mock_get_config(cls, attr, default):
- return cls.CONFIG_KEY_DICT.get(attr, default)
-
- def setUp(self):
- self.CONFIG_KEY_DICT.clear()
- mgr.set_config.side_effect = self.mock_set_config
- mgr.get_config.side_effect = self.mock_get_config
- if Settings.GRAFANA_API_HOST != 'localhost':
- Settings.GRAFANA_API_HOST = 'localhost'
- if Settings.GRAFANA_API_PORT != 3000:
- Settings.GRAFANA_API_PORT = 3000
-
- def test_get_setting(self):
- self.assertEqual(Settings.GRAFANA_API_HOST, 'localhost')
-
- def test_set_setting(self):
- Settings.GRAFANA_API_HOST = 'grafanahost'
- self.assertEqual(Settings.GRAFANA_API_HOST, 'grafanahost')
-
- def test_get_cmd(self):
- r, out, err = handle_option_command(
- {'prefix': 'dashboard get-grafana-api-port'})
- self.assertEqual(r, 0)
- self.assertEqual(out, '3000')
- self.assertEqual(err, '')
-
- def test_set_cmd(self):
- r, out, err = handle_option_command(
- {'prefix': 'dashboard set-grafana-api-port',
- 'value': '4000'})
- self.assertEqual(r, 0)
- self.assertEqual(out, 'Option GRAFANA_API_PORT updated')
- self.assertEqual(err, '')
-
- def test_inv_cmd(self):
- r, out, err = handle_option_command(
- {'prefix': 'dashboard get-non-existent-option'})
- self.assertEqual(r, -errno.ENOSYS)
- self.assertEqual(out, '')
- self.assertEqual(err, "Command not found "
- "'dashboard get-non-existent-option'")
-
- def test_sync(self):
- Settings.GRAFANA_API_PORT = 5000
- r, out, err = handle_option_command(
- {'prefix': 'dashboard get-grafana-api-port'})
- self.assertEqual(r, 0)
- self.assertEqual(out, '5000')
- self.assertEqual(err, '')
- r, out, err = handle_option_command(
- {'prefix': 'dashboard set-grafana-api-host',
- 'value': 'new-local-host'})
- self.assertEqual(r, 0)
- self.assertEqual(out, 'Option GRAFANA_API_HOST updated')
- self.assertEqual(err, '')
- self.assertEqual(Settings.GRAFANA_API_HOST, 'new-local-host')
-
- def test_attribute_error(self):
- with self.assertRaises(AttributeError) as ctx:
- _ = Settings.NON_EXISTENT_OPTION
-
- self.assertEqual(str(ctx.exception),
- "type object 'Options' has no attribute 'NON_EXISTENT_OPTION'")
+++ /dev/null
-from __future__ import absolute_import
-
-import cherrypy
-
-from .. import mgr
-from ..controllers.tcmu_iscsi import TcmuIscsi
-from .helper import ControllerTestCase
-
-mocked_servers = [{
- 'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)',
- 'hostname': 'ceph-dev',
- 'services': [{'id': 'a:b', 'type': 'tcmu-runner'}]
-}]
-
-mocked_metadata = {
- 'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)',
- 'pool_name': 'pool1',
- 'image_name': 'image1',
- 'image_id': '42',
- 'optimized_since': 100.0,
-}
-
-mocked_get_daemon_status = {
- 'lock_owner': 'true',
-}
-
-mocked_get_counter = {
- 'librbd-42-pool1-image1.lock_acquired_time': [[10000.0, 10000.0]],
- 'librbd-42-pool1-image1.rd': 43,
- 'librbd-42-pool1-image1.wr': 44,
- 'librbd-42-pool1-image1.rd_bytes': 45,
- 'librbd-42-pool1-image1.wr_bytes': 46,
-}
-
-mocked_get_rate = 47
-
-
-class TcmuIscsiControllerTest(ControllerTestCase):
-
- @classmethod
- def setup_server(cls):
- mgr.list_servers.return_value = mocked_servers
- mgr.get_metadata.return_value = mocked_metadata
- mgr.get_daemon_status.return_value = mocked_get_daemon_status
- mgr.get_counter.return_value = mocked_get_counter
- mgr.get_rate.return_value = mocked_get_rate
- mgr.url_prefix = ''
- TcmuIscsi._cp_config['tools.authenticate.on'] = False # pylint: disable=protected-access
-
- cherrypy.tree.mount(TcmuIscsi(), "/api/test/tcmu")
-
- def test_list(self):
- self._get('/api/test/tcmu')
- self.assertStatus(200)
- self.assertJsonBody({
- 'daemons': [{
- 'server_hostname': 'ceph-dev',
- 'version': 'ceph version 13.0.0-5083- () mimic (dev)',
- 'optimized_paths': 1, 'non_optimized_paths': 0}],
- 'images': [{
- 'device_id': 'b',
- 'pool_name': 'pool1',
- 'name': 'image1',
- 'id': '42', 'optimized_paths': ['ceph-dev'],
- 'non_optimized_paths': [],
- 'optimized_since': 1e-05,
- 'stats': {'rd': 47, 'rd_bytes': 47, 'wr': 47, 'wr_bytes': 47},
- 'stats_history': {
- 'rd': 43, 'wr': 44, 'rd_bytes': 45, 'wr_bytes': 46}
- }]
- })
+++ /dev/null
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-
-import cherrypy
-from cherrypy.lib.sessions import RamSession
-from mock import patch
-
-from .helper import ControllerTestCase
-from ..tools import RESTController
-
-
-# pylint: disable=W0613
-class FooResource(RESTController):
- elems = []
-
- def list(self, *vpath, **params):
- return FooResource.elems
-
- def create(self, data, *args, **kwargs):
- FooResource.elems.append(data)
- return data
-
- def get(self, key, *args, **kwargs):
- if args:
- return {'detail': (key, args)}
- return FooResource.elems[int(key)]
-
- def delete(self, key):
- del FooResource.elems[int(key)]
-
- def bulk_delete(self):
- FooResource.elems = []
-
- def set(self, data, key):
- FooResource.elems[int(key)] = data
- return dict(key=key, **data)
-
-
-class FooArgs(RESTController):
- @RESTController.args_from_json
- def set(self, code, name):
- return {'code': code, 'name': name}
-
-
-# pylint: disable=C0102
-class Root(object):
- foo = FooResource()
- fooargs = FooArgs()
-
-
-class RESTControllerTest(ControllerTestCase):
-
- @classmethod
- def setup_server(cls):
- cherrypy.tree.mount(Root())
-
- def test_empty(self):
- self._delete("/foo")
- self.assertStatus(204)
- self._get("/foo")
- self.assertStatus('200 OK')
- self.assertHeader('Content-Type', 'application/json')
- self.assertBody('[]')
-
- def test_fill(self):
- sess_mock = RamSession()
- with patch('cherrypy.session', sess_mock, create=True):
- data = {'a': 'b'}
- for _ in range(5):
- self._post("/foo", data)
- self.assertJsonBody(data)
- self.assertStatus(201)
- self.assertHeader('Content-Type', 'application/json')
-
- self._get("/foo")
- self.assertStatus('200 OK')
- self.assertHeader('Content-Type', 'application/json')
- self.assertJsonBody([data] * 5)
-
- self._put('/foo/0', {'newdata': 'newdata'})
- self.assertStatus('200 OK')
- self.assertHeader('Content-Type', 'application/json')
- self.assertJsonBody({'newdata': 'newdata', 'key': '0'})
-
- def test_not_implemented(self):
- self._put("/foo")
- self.assertStatus(405)
- body = self.jsonBody()
- self.assertIsInstance(body, dict)
- assert body['detail'] == 'Method not implemented.'
- assert '405' in body['status']
- assert 'traceback' in body
-
- def test_args_from_json(self):
- self._put("/fooargs/hello", {'name': 'world'})
- self.assertJsonBody({'code': 'hello', 'name': 'world'})
-
- def test_detail_route(self):
- self._get('/foo/1/detail')
- self.assertJsonBody({'detail': ['1', ['detail']]})
-
- self._post('/foo/1/detail', 'post-data')
- self.assertStatus(405)
+++ /dev/null
-# -*- coding: utf-8 -*-
-# pylint: disable=W0212
-from __future__ import absolute_import
-
-import collections
-import datetime
-import importlib
-import inspect
-import json
-import os
-import pkgutil
-import sys
-import time
-import threading
-
-import cherrypy
-
-from . import logger
-
-
-def ApiController(path):
- def decorate(cls):
- cls._cp_controller_ = True
- cls._cp_path_ = path
- config = {
- 'tools.sessions.on': True,
- 'tools.sessions.name': Session.NAME,
- 'tools.session_expire_at_browser_close.on': True
- }
- if not hasattr(cls, '_cp_config'):
- cls._cp_config = {}
- if 'tools.authenticate.on' not in cls._cp_config:
- config['tools.authenticate.on'] = False
- cls._cp_config.update(config)
- return cls
- return decorate
-
-
-def AuthRequired(enabled=True):
- def decorate(cls):
- if not hasattr(cls, '_cp_config'):
- cls._cp_config = {
- 'tools.authenticate.on': enabled
- }
- else:
- cls._cp_config['tools.authenticate.on'] = enabled
- return cls
- return decorate
-
-
-def load_controllers():
- # setting sys.path properly when not running under the mgr
- dashboard_dir = os.path.dirname(os.path.realpath(__file__))
- mgr_dir = os.path.dirname(dashboard_dir)
- if mgr_dir not in sys.path:
- sys.path.append(mgr_dir)
-
- controllers = []
- ctrls_path = '{}/controllers'.format(dashboard_dir)
- mods = [mod for _, mod, _ in pkgutil.iter_modules([ctrls_path])]
- for mod_name in mods:
- mod = importlib.import_module('.controllers.{}'.format(mod_name),
- package='dashboard_v2')
- for _, cls in mod.__dict__.items():
- # Controllers MUST be derived from the class BaseController.
- if inspect.isclass(cls) and issubclass(cls, BaseController) and \
- hasattr(cls, '_cp_controller_'):
- controllers.append(cls)
-
- return controllers
-
-
-def json_error_page(status, message, traceback, version):
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps(dict(status=status, detail=message, traceback=traceback,
- version=version))
-
-
-class BaseController(object):
- """
- Base class for all controllers providing API endpoints.
- """
-
-
-class RequestLoggingTool(cherrypy.Tool):
- def __init__(self):
- cherrypy.Tool.__init__(self, 'before_handler', self.request_begin,
- priority=95)
-
- def _setup(self):
- cherrypy.Tool._setup(self)
- cherrypy.request.hooks.attach('on_end_request', self.request_end,
- priority=5)
- cherrypy.request.hooks.attach('after_error_response', self.request_error,
- priority=5)
-
- def _get_user(self):
- if hasattr(cherrypy.serving, 'session'):
- return cherrypy.session.get(Session.USERNAME)
- return None
-
- def request_begin(self):
- req = cherrypy.request
- user = self._get_user()
- if user:
- logger.debug("[%s:%s] [%s] [%s] %s", req.remote.ip,
- req.remote.port, req.method, user, req.path_info)
- else:
- logger.debug("[%s:%s] [%s] %s", req.remote.ip,
- req.remote.port, req.method, req.path_info)
-
- def request_error(self):
- self._request_log(logger.error)
- logger.error(cherrypy.response.body)
-
- def request_end(self):
- status = cherrypy.response.status[:3]
- if status in ["401"]:
- # log unauthorized accesses
- self._request_log(logger.warning)
- else:
- self._request_log(logger.info)
-
- def _format_bytes(self, num):
- units = ['B', 'K', 'M', 'G']
-
- format_str = "{:.0f}{}"
- for i, unit in enumerate(units):
- div = 2**(10*i)
- if num < 2**(10*(i+1)):
- if num % div == 0:
- format_str = "{}{}"
- else:
- div = float(div)
- format_str = "{:.1f}{}"
- return format_str.format(num/div, unit[0])
-
- # content-length bigger than 1T!! return value in bytes
- return "{}B".format(num)
-
- def _request_log(self, logger_fn):
- req = cherrypy.request
- res = cherrypy.response
- lat = time.time() - res.time
- user = self._get_user()
- status = res.status[:3] if isinstance(res.status, str) else res.status
- if 'Content-Length' in res.headers:
- length = self._format_bytes(res.headers['Content-Length'])
- else:
- length = self._format_bytes(0)
- if user:
- logger_fn("[%s:%s] [%s] [%s] [%s] [%s] [%s] %s", req.remote.ip,
- req.remote.port, req.method, status,
- "{0:.3f}s".format(lat), user, length, req.path_info)
- else:
- logger_fn("[%s:%s] [%s] [%s] [%s] [%s] %s", req.remote.ip,
- req.remote.port, req.method, status,
- "{0:.3f}s".format(lat), length, req.path_info)
-
-
-# pylint: disable=too-many-instance-attributes
-class ViewCache(object):
- VALUE_OK = 0
- VALUE_STALE = 1
- VALUE_NONE = 2
- VALUE_EXCEPTION = 3
-
- class GetterThread(threading.Thread):
- def __init__(self, view, fn, args, kwargs):
- super(ViewCache.GetterThread, self).__init__()
- self._view = view
- self.event = threading.Event()
- self.fn = fn
- self.args = args
- self.kwargs = kwargs
-
- # pylint: disable=broad-except
- def run(self):
- try:
- t0 = time.time()
- val = self.fn(*self.args, **self.kwargs)
- t1 = time.time()
- except Exception as ex:
- logger.exception("Error while calling fn=%s ex=%s", self.fn,
- str(ex))
- self._view.value = None
- self._view.value_when = None
- self._view.getter_thread = None
- self._view.exception = ex
- else:
- with self._view.lock:
- self._view.latency = t1 - t0
- self._view.value = val
- self._view.value_when = datetime.datetime.now()
- self._view.getter_thread = None
- self._view.exception = None
-
- self.event.set()
-
- class RemoteViewCache(object):
- # Return stale data if
- STALE_PERIOD = 1.0
-
- def __init__(self, timeout):
- self.getter_thread = None
- # Consider data within 1s old to be sufficiently fresh
- self.timeout = timeout
- self.event = threading.Event()
- self.value_when = None
- self.value = None
- self.latency = 0
- self.exception = None
- self.lock = threading.Lock()
-
- def run(self, fn, args, kwargs):
- """
- If data less than `stale_period` old is available, return it
- immediately.
- If an attempt to fetch data does not complete within `timeout`, then
- return the most recent data available, with a status to indicate that
- it is stale.
-
- Initialization does not count towards the timeout, so the first call
- on one of these objects during the process lifetime may be slower
- than subsequent calls.
-
- :return: 2-tuple of value status code, value
- """
- with self.lock:
- now = datetime.datetime.now()
- if self.value_when and now - self.value_when < datetime.timedelta(
- seconds=self.STALE_PERIOD):
- return ViewCache.VALUE_OK, self.value
-
- if self.getter_thread is None:
- self.getter_thread = ViewCache.GetterThread(self, fn, args,
- kwargs)
- self.getter_thread.start()
-
- ev = self.getter_thread.event
-
- success = ev.wait(timeout=self.timeout)
-
- with self.lock:
- if success:
- # We fetched the data within the timeout
- if self.exception:
- # execution raised an exception
- return ViewCache.VALUE_EXCEPTION, self.exception
- return ViewCache.VALUE_OK, self.value
- elif self.value_when is not None:
- # We have some data, but it doesn't meet freshness requirements
- return ViewCache.VALUE_STALE, self.value
- # We have no data, not even stale data
- return ViewCache.VALUE_NONE, None
-
- def __init__(self, timeout=5):
- self.timeout = timeout
- self.cache_by_args = {}
-
- def __call__(self, fn):
- def wrapper(*args, **kwargs):
- rvc = self.cache_by_args.get(args, None)
- if not rvc:
- rvc = ViewCache.RemoteViewCache(self.timeout)
- self.cache_by_args[args] = rvc
- return rvc.run(fn, args, kwargs)
- return wrapper
-
-
-class RESTController(BaseController):
- """
- Base class for providing a RESTful interface to a resource.
-
- To use this class, simply derive a class from it and implement the methods
- you want to support. The list of possible methods are:
-
- * list()
- * bulk_set(data)
- * create(data)
- * bulk_delete()
- * get(key)
- * set(data, key)
- * delete(key)
-
- Test with curl:
-
- curl -H "Content-Type: application/json" -X POST \
- -d '{"username":"xyz","password":"xyz"}' http://127.0.0.1:8080/foo
- curl http://127.0.0.1:8080/foo
- curl http://127.0.0.1:8080/foo/0
-
- """
-
- def _not_implemented(self, is_sub_path):
- methods = [method
- for ((method, _is_element), (meth, _))
- in self._method_mapping.items()
- if _is_element == is_sub_path is not None and hasattr(self, meth)]
- cherrypy.response.headers['Allow'] = ','.join(methods)
- raise cherrypy.HTTPError(405, 'Method not implemented.')
-
- _method_mapping = {
- ('GET', False): ('list', 200),
- ('PUT', False): ('bulk_set', 200),
- ('PATCH', False): ('bulk_set', 200),
- ('POST', False): ('create', 201),
- ('DELETE', False): ('bulk_delete', 204),
- ('GET', True): ('get', 200),
- ('PUT', True): ('set', 200),
- ('PATCH', True): ('set', 200),
- ('DELETE', True): ('delete', 204),
- }
-
- def _get_method(self, vpath):
- is_sub_path = bool(len(vpath))
- try:
- method_name, status_code = self._method_mapping[
- (cherrypy.request.method, is_sub_path)]
- except KeyError:
- self._not_implemented(is_sub_path)
- method = getattr(self, method_name, None)
- if not method:
- self._not_implemented(is_sub_path)
- return method, status_code
-
- @cherrypy.expose
- def default(self, *vpath, **params):
- method, status_code = self._get_method(vpath)
-
- if cherrypy.request.method not in ['GET', 'DELETE']:
- method = RESTController._takes_json(method)
-
- if cherrypy.request.method != 'DELETE':
- method = RESTController._returns_json(method)
-
- cherrypy.response.status = status_code
-
- return method(*vpath, **params)
-
- @staticmethod
- def args_from_json(func):
- func._args_from_json_ = True
- return func
-
- # pylint: disable=W1505
- @staticmethod
- def _takes_json(func):
- def inner(*args, **kwargs):
- content_length = int(cherrypy.request.headers['Content-Length'])
- body = cherrypy.request.body.read(content_length)
- if not body:
- raise cherrypy.HTTPError(400, 'Empty body. Content-Length={}'
- .format(content_length))
- try:
- data = json.loads(body.decode('utf-8'))
- except Exception as e:
- raise cherrypy.HTTPError(400, 'Failed to decode JSON: {}'
- .format(str(e)))
- if hasattr(func, '_args_from_json_'):
- if sys.version_info > (3, 0):
- f_args = list(inspect.signature(func).parameters.keys())
- else:
- f_args = inspect.getargspec(func).args[1:]
- n_args = []
- for arg in args:
- n_args.append(arg)
- for arg in f_args:
- if arg in data:
- n_args.append(data[arg])
- data.pop(arg)
- kwargs.update(data)
- return func(*n_args, **kwargs)
-
- return func(data, *args, **kwargs)
- return inner
-
- @staticmethod
- def _returns_json(func):
- def inner(*args, **kwargs):
- cherrypy.response.headers['Content-Type'] = 'application/json'
- ret = func(*args, **kwargs)
- return json.dumps(ret).encode('utf8')
- return inner
-
- @staticmethod
- def split_vpath(vpath):
- if not vpath:
- return None, None
- if len(vpath) == 1:
- return vpath[0], None
- return vpath[0], vpath[1]
-
-
-class Session(object):
- """
- This class contains all relevant settings related to cherrypy.session.
- """
- NAME = 'session_id'
-
- # The keys used to store the information in the cherrypy.session.
- USERNAME = '_username'
- TS = '_ts'
- EXPIRE_AT_BROWSER_CLOSE = '_expire_at_browser_close'
-
- # The default values.
- DEFAULT_EXPIRE = 1200.0
-
-
-class SessionExpireAtBrowserCloseTool(cherrypy.Tool):
- """
- A CherryPi Tool which takes care that the cookie does not expire
- at browser close if the 'Keep me logged in' checkbox was selected
- on the login page.
- """
- def __init__(self):
- cherrypy.Tool.__init__(self, 'before_finalize', self._callback)
-
- def _callback(self):
- # Shall the cookie expire at browser close?
- expire_at_browser_close = cherrypy.session.get(
- Session.EXPIRE_AT_BROWSER_CLOSE, True)
- logger.debug("expire at browser close: %s", expire_at_browser_close)
- if expire_at_browser_close:
- # Get the cookie and its name.
- cookie = cherrypy.response.cookie
- name = cherrypy.request.config.get(
- 'tools.sessions.name', Session.NAME)
- # Make the cookie a session cookie by purging the
- # fields 'expires' and 'max-age'.
- logger.debug("expire at browser close: removing 'expires' and 'max-age'")
- if name in cookie:
- del cookie[name]['expires']
- del cookie[name]['max-age']
-
-
-class NotificationQueue(threading.Thread):
- _ALL_TYPES_ = '__ALL__'
- _listeners = collections.defaultdict(set)
- _lock = threading.Lock()
- _cond = threading.Condition()
- _queue = collections.deque()
- _running = False
- _instance = None
-
- def __init__(self):
- super(NotificationQueue, self).__init__()
-
- @classmethod
- def start_queue(cls):
- with cls._lock:
- if cls._instance:
- # the queue thread is already running
- return
- cls._running = True
- cls._instance = NotificationQueue()
- logger.debug("starting notification queue")
- cls._instance.start()
-
- @classmethod
- def stop(cls):
- with cls._lock:
- if not cls._instance:
- # the queue thread was not started
- return
- instance = cls._instance
- cls._instance = None
- cls._running = False
- with cls._cond:
- cls._cond.notify()
- logger.debug("waiting for notification queue to finish")
- instance.join()
- logger.debug("notification queue stopped")
-
- @classmethod
- def register(cls, func, types=None):
- """Registers function to listen for notifications
-
- If the second parameter `types` is omitted, the function in `func`
- parameter will be called for any type of notifications.
-
- Args:
- func (function): python function ex: def foo(val)
- types (str|list): the single type to listen, or a list of types
- """
- with cls._lock:
- if not types:
- cls._listeners[cls._ALL_TYPES_].add(func)
- return
- if isinstance(types, str):
- cls._listeners[types].add(func)
- elif isinstance(types, list):
- for typ in types:
- cls._listeners[typ].add(func)
- else:
- raise Exception("types param is neither a string nor a list")
-
- @classmethod
- def new_notification(cls, notify_type, notify_value):
- cls._queue.append((notify_type, notify_value))
- with cls._cond:
- cls._cond.notify()
-
- @classmethod
- def notify_listeners(cls, events):
- for ev in events:
- notify_type, notify_value = ev
- with cls._lock:
- listeners = list(cls._listeners[notify_type])
- listeners.extend(cls._listeners[cls._ALL_TYPES_])
- for listener in listeners:
- listener(notify_value)
-
- def run(self):
- logger.debug("notification queue started")
- while self._running:
- private_buffer = []
- logger.debug("NQ: processing queue: %s", len(self._queue))
- try:
- while True:
- private_buffer.append(self._queue.popleft())
- except IndexError:
- pass
- self.notify_listeners(private_buffer)
- with self._cond:
- self._cond.wait(1.0)
- # flush remaining events
- logger.debug("NQ: flush remaining events: %s", len(self._queue))
- self.notify_listeners(self._queue)
- self._queue.clear()
- logger.debug("notification queue finished")
+++ /dev/null
-[tox]
-envlist = cov-init,py27,py3,cov-report,lint
-skipsdist = true
-
-[testenv]
-deps=-r{toxinidir}/requirements.txt
-setenv=
- UNITTEST=true
- WEBTEST_INTERACTIVE=false
- COVERAGE_FILE= .coverage.{envname}
- PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.3:{toxinidir}/../../../../build/lib/cython_modules/lib.2
- LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
- PATH = {toxinidir}/../../../../build/bin:$PATH
-commands=
- {envbindir}/py.test --cov=. --cov-report= --junitxml=junit.{envname}.xml --doctest-modules controllers/rbd.py tests/
-
-[testenv:cov-init]
-setenv =
- COVERAGE_FILE = .coverage
-deps = coverage
-commands =
- coverage erase
-
-[testenv:cov-report]
-setenv =
- COVERAGE_FILE = .coverage
-deps = coverage
-commands =
- coverage combine
- coverage report
- coverage xml
-
-[testenv:lint]
-setenv =
- PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.3:{toxinidir}/../../../../build/lib/cython_modules/lib.2
- LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
-deps=-r{toxinidir}/requirements.txt
-commands=
- pylint --rcfile=.pylintrc --jobs=5 . module.py tools.py controllers tests services
- pycodestyle --max-line-length=100 --exclude=python2.7,.tox,venv,frontend --ignore=E402,E121,E123,E126,E226,E24,E704,W503 .
add_test(NAME run-tox-ceph-disk COMMAND bash ${CMAKE_SOURCE_DIR}/src/ceph-disk/run-tox.sh)
add_test(NAME run-tox-ceph-detect-init COMMAND bash ${CMAKE_SOURCE_DIR}/src/ceph-detect-init/run-tox.sh)
if(WITH_MGR)
- add_test(NAME run-tox-mgr-dashboard_v2 COMMAND bash ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/run-tox.sh)
+ add_test(NAME run-tox-mgr-dashboard COMMAND bash ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/run-tox.sh)
endif(WITH_MGR)
set(CEPH_DISK_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/ceph-disk-virtualenv)
set(CEPH_DETECT_INIT_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/ceph-detect-init-virtualenv)
if(WITH_MGR)
- set(MGR_DASHBOARD_V2_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/mgr-dashboard_v2-virtualenv)
+ set(MGR_DASHBOARD_VIRTUALENV ${CEPH_BUILD_VIRTUALENV}/mgr-dashboard-virtualenv)
endif(WITH_MGR)
set_property(TEST
run-tox-ceph-disk
run-tox-ceph-detect-init
- run-tox-mgr-dashboard_v2
+ run-tox-mgr-dashboard
PROPERTY ENVIRONMENT
CEPH_BUILD_DIR=${CMAKE_BINARY_DIR}
CEPH_ROOT=${CMAKE_SOURCE_DIR}
CEPH_BUILD_VIRTUALENV=${CEPH_BUILD_VIRTUALENV}
CEPH_DISK_VIRTUALENV=${CEPH_DISK_VIRTUALENV}
CEPH_DETECT_INIT_VIRTUALENV=${CEPH_DETECT_INIT_VIRTUALENV}
- MGR_DASHBOARD_V2_VIRTUALENV=${MGR_DASHBOARD_V2_VIRTUALENV}
+ MGR_DASHBOARD_VIRTUALENV=${MGR_DASHBOARD_VIRTUALENV}
LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/lib
PATH=$ENV{PATH}:${CMAKE_RUNTIME_OUTPUT_DIRECTORY}:${CMAKE_SOURCE_DIR}/src
PYTHONPATH=${CMAKE_SOURCE_DIR}/src/pybind
#scripts
-if(WITH_MGR_DASHBOARD_V2_FRONTEND)
-
+if(WITH_MGR_DASHBOARD_FRONTEND)
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
- add_ceph_test(mgr-dashboard_v2-frontend-unittests ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard_v2/run-frontend-unittests.sh)
+ add_ceph_test(mgr-dashboard-frontend-unittests ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/run-frontend-unittests.sh)
endif(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64|arm|ARM")
- add_ceph_test(mgr-dashboard_v2-smoke.sh ${CMAKE_CURRENT_SOURCE_DIR}/mgr-dashboard_v2-smoke.sh)
-endif(WITH_MGR_DASHBOARD_V2_FRONTEND)
+ add_ceph_test(mgr-dashboard-smoke.sh ${CMAKE_CURRENT_SOURCE_DIR}/mgr-dashboard-smoke.sh)
+endif(WITH_MGR_DASHBOARD_FRONTEND)
--- /dev/null
+#!/usr/bin/env bash
+#
+# Copyright (C) 2014,2015,2017 Red Hat <contact@redhat.com>
+# Copyright (C) 2018 SUSE LLC
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Library Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Library Public License for more details.
+#
+source $(dirname $0)/../detect-build-env-vars.sh
+source $CEPH_ROOT/qa/standalone/ceph-helpers.sh
+
+function run() {
+ local dir=$1
+ shift
+
+ export CEPH_MON=127.0.0.1:7160 # git grep '\<7160\>' : there must be only one
+ export CEPH_ARGS
+ CEPH_ARGS+="--fsid=$(uuidgen) --auth-supported=none "
+ CEPH_ARGS+="--mon-initial-members=a --mon-host=$MON "
+ CEPH_ARGS+="--mgr-initial-modules=dashboard "
+ CEPH_ARGS+="--mon-host=$CEPH_MON"
+
+ setup $dir || return 1
+ TEST_dashboard $dir || return 1
+ teardown $dir || return 1
+}
+
+function TEST_dashboard() {
+ local dir=$1
+ shift
+
+ run_mon $dir a || return 1
+ timeout 30 ceph mon stat || return 1
+ ceph config-key set mgr/dashboard/x/server_port 7161
+ MGR_ARGS+="--mgr_module_path=${CEPH_ROOT}/src/pybind/mgr "
+ run_mgr $dir x ${MGR_ARGS} || return 1
+
+ tries=0
+ while [[ $tries < 30 ]] ; do
+ if [ $(ceph status -f json | jq .mgrmap.available) = "true" ]
+ then
+ break
+ fi
+ tries=$((tries+1))
+ sleep 1
+ done
+ ceph_adm tell mgr dashboard set-login-credentials admin admin
+
+ tries=0
+ while [[ $tries < 30 ]] ; do
+ if curl -c $dir/cookiefile -X POST -d '{"username":"admin","password":"admin"}' http://127.0.0.1:7161/api/auth
+ then
+ if curl -b $dir/cookiefile -s http://127.0.0.1:7161/api/summary | \
+ jq '.health.overall_status' | grep HEALTH_
+ then
+ break
+ fi
+ fi
+ tries=$((tries+1))
+ sleep 0.5
+ done
+}
+
+main mgr-dashboard-smoke "$@"
+
+# Local Variables:
+# compile-command: "cd ../.. ; make -j4 TESTS=test/mgr/mgr-dashboard-smoke.sh check"
+# End:
+++ /dev/null
-#!/usr/bin/env bash
-#
-# Copyright (C) 2014,2015,2017 Red Hat <contact@redhat.com>
-# Copyright (C) 2018 SUSE LLC
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU Library Public License as published by
-# the Free Software Foundation; either version 2, or (at your option)
-# any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Library Public License for more details.
-#
-source $(dirname $0)/../detect-build-env-vars.sh
-source $CEPH_ROOT/qa/standalone/ceph-helpers.sh
-
-function run() {
- local dir=$1
- shift
-
- export CEPH_MON=127.0.0.1:7160 # git grep '\<7160\>' : there must be only one
- export CEPH_ARGS
- CEPH_ARGS+="--fsid=$(uuidgen) --auth-supported=none "
- CEPH_ARGS+="--mon-initial-members=a --mon-host=$MON "
- CEPH_ARGS+="--mgr-initial-modules=dashbaord_v2 "
- CEPH_ARGS+="--mon-host=$CEPH_MON"
-
- setup $dir || return 1
- TEST_dashboardv2 $dir || return 1
- teardown $dir || return 1
-}
-
-function TEST_dashboardv2() {
- local dir=$1
- shift
-
- run_mon $dir a || return 1
- timeout 30 ceph mon stat || return 1
- ceph config-key set mgr/dashboard_v2/x/server_port 7161
- MGR_ARGS+="--mgr_module_path=${CEPH_ROOT}/src/pybind/mgr "
- run_mgr $dir x ${MGR_ARGS} || return 1
-
- tries=0
- while [[ $tries < 30 ]] ; do
- if [ $(ceph status -f json | jq .mgrmap.available) = "true" ]
- then
- break
- fi
- tries=$((tries+1))
- sleep 1
- done
- ceph_adm tell mgr dashboard set-login-credentials admin admin
-
- tries=0
- while [[ $tries < 30 ]] ; do
- if curl -c $dir/cookiefile -X POST -d '{"username":"admin","password":"admin"}' http://127.0.0.1:7161/api/auth
- then
- if curl -b $dir/cookiefile -s http://127.0.0.1:7161/api/summary | \
- jq '.health.overall_status' | grep HEALTH_
- then
- break
- fi
- fi
- tries=$((tries+1))
- sleep 0.5
- done
-}
-
-main mgr-dashboard_v2-smoke "$@"
-
-# Local Variables:
-# compile-command: "cd ../.. ; make -j4 TESTS=test/mgr/mgr-dashboard_v2-smoke.sh check"
-# End:
MON_ADDR=""
DASH_URLS=""
-DASH_V2_URLS=""
RESTFUL_URLS=""
conf_fn="$CEPH_CONF_PATH/ceph.conf"
$COSDSHORT
$extra_conf
[mon]
- mgr initial modules = restful status balancer
+ mgr initial modules = dashboard restful status balancer
$DAEMONOPTS
$CMONDEBUG
$extra_conf
host = $HOSTNAME
EOF
- ceph_adm config-key set mgr/restful/$name/server_port $MGR_PORT
+ ceph_adm config-key set mgr/dashboard/$name/server_port $MGR_PORT
if [ $mgr -eq 1 ]; then
- RESTFUL_URLS="https://$IP:$MGR_PORT"
+ DASH_URLS="http://$IP:$MGR_PORT"
else
- RESTFUL_URLS+=", https://$IP:$MGR_PORT"
+ DASH_URLS+=", http://$IP:$MGR_PORT"
fi
MGR_PORT=$(($MGR_PORT + 1000))
- # dashboard_v2
- ceph_adm config-key set mgr/dashboard_v2/$name/server_port $MGR_PORT
+ ceph_adm config-key set mgr/restful/$name/server_port $MGR_PORT
if [ $mgr -eq 1 ]; then
- DASH_V2_URLS="http://$IP:$MGR_PORT"
+ RESTFUL_URLS="https://$IP:$MGR_PORT"
else
- DASH_V2_URLS+=", http://$IP:$MGR_PORT"
+ RESTFUL_URLS+=", https://$IP:$MGR_PORT"
fi
MGR_PORT=$(($MGR_PORT + 1000))
# use tell mgr here because the first mgr might not have activated yet
# to register the python module commands.
+
+ # setting login credentials for dashboard
+ ceph_adm tell mgr dashboard set-login-credentials admin admin
+
if ceph_adm tell mgr restful create-self-signed-cert; then
SF=`mktemp`
ceph_adm restful create-key admin -o $SF
else
echo MGR Restful is not working, perhaps the package is not installed?
fi
-
- # dashboard_v2
- sleep 5 # when running with more than 1 mgrs, if we enable dashboard_v2
- # immediately it will fail, so we just wait for a bit
- ceph_adm mgr module enable dashboard_v2
- # setting login credentials for dashboard_v2
- ceph_adm tell mgr dashboard set-login-credentials admin admin
}
start_mds() {
echo "started. stop.sh to stop. see out/* (e.g. 'tail -f out/????') for debug output."
echo ""
-echo "dashboard_v2 urls: $DASH_V2_URLS"
+echo "dashboard urls: $DASH_URLS"
echo " w/ user/pass: admin / admin"
echo "restful urls: $RESTFUL_URLS"
echo " w/ user/pass: admin / $RESTFUL_SECRET"