From: Ricardo Dias Date: Thu, 15 Mar 2018 08:50:22 +0000 (+0000) Subject: mgr/dashboard: renamed dashboard_v2 to dashboard X-Git-Tag: v13.1.0~545^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F20912%2Fhead;p=ceph.git mgr/dashboard: renamed dashboard_v2 to dashboard Signed-off-by: Ricardo Dias --- diff --git a/CMakeLists.txt b/CMakeLists.txt index b521b9cec6ee..a0eb77b74149 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -613,7 +613,7 @@ endif() 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) diff --git a/ceph.spec.in b/ceph.spec.in index 5e890c4c6187..cc4d0ee5da50 100644 --- a/ceph.spec.in +++ b/ceph.spec.in @@ -899,7 +899,7 @@ cmake .. \ -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 diff --git a/debian/rules b/debian/rules index 17a0d804b306..f1e150035fa2 100755 --- a/debian/rules +++ b/debian/rules @@ -5,7 +5,7 @@ export DESTDIR=$(CURDIR)/debian/tmp 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. diff --git a/make-dist b/make-dist index e492fae93e0f..607caf3f9c7b 100755 --- a/make-dist +++ b/make-dist @@ -63,11 +63,11 @@ download_boost() { 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... diff --git a/qa/suites/rados/mgr/tasks/dashboard.yaml b/qa/suites/rados/mgr/tasks/dashboard.yaml new file mode 100644 index 000000000000..8570eaff1a40 --- /dev/null +++ b/qa/suites/rados/mgr/tasks/dashboard.yaml @@ -0,0 +1,34 @@ + +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 diff --git a/qa/suites/rados/mgr/tasks/dashboard_v2.yaml b/qa/suites/rados/mgr/tasks/dashboard_v2.yaml deleted file mode 100644 index cd5e347a8bb4..000000000000 --- a/qa/suites/rados/mgr/tasks/dashboard_v2.yaml +++ /dev/null @@ -1,34 +0,0 @@ - -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 diff --git a/qa/tasks/mgr/dashboard/__init__.py b/qa/tasks/mgr/dashboard/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/qa/tasks/mgr/dashboard/helper.py b/qa/tasks/mgr/dashboard/helper.py new file mode 100644 index 000000000000..3e0c836e8b05 --- /dev/null +++ b/qa/tasks/mgr/dashboard/helper.py @@ -0,0 +1,145 @@ +# -*- 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']] diff --git a/qa/tasks/mgr/dashboard/test_auth.py b/qa/tasks/mgr/dashboard/test_auth.py new file mode 100644 index 000000000000..b176dd041b1a --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_auth.py @@ -0,0 +1,75 @@ +# -*- 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) diff --git a/qa/tasks/mgr/dashboard/test_cephfs.py b/qa/tasks/mgr/dashboard/test_cephfs.py new file mode 100644 index 000000000000..5669e41e07ec --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_cephfs.py @@ -0,0 +1,39 @@ +# -*- 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) diff --git a/qa/tasks/mgr/dashboard/test_cluster_configuration.py b/qa/tasks/mgr/dashboard/test_cluster_configuration.py new file mode 100644 index 000000000000..6cfe605e9474 --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_cluster_configuration.py @@ -0,0 +1,39 @@ +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) + diff --git a/qa/tasks/mgr/dashboard/test_dashboard.py b/qa/tasks/mgr/dashboard/test_dashboard.py new file mode 100644 index 000000000000..fa7001797ad2 --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_dashboard.py @@ -0,0 +1,38 @@ +# -*- 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']) diff --git a/qa/tasks/mgr/dashboard/test_host.py b/qa/tasks/mgr/dashboard/test_host.py new file mode 100644 index 000000000000..efa28d913292 --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_host.py @@ -0,0 +1,25 @@ +# -*- 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']) diff --git a/qa/tasks/mgr/dashboard/test_monitor.py b/qa/tasks/mgr/dashboard/test_monitor.py new file mode 100644 index 000000000000..0b199cdb1934 --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_monitor.py @@ -0,0 +1,18 @@ +# -*- 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']) diff --git a/qa/tasks/mgr/dashboard/test_osd.py b/qa/tasks/mgr/dashboard/test_osd.py new file mode 100644 index 000000000000..587a448ec54e --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_osd.py @@ -0,0 +1,36 @@ +# -*- 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']) diff --git a/qa/tasks/mgr/dashboard/test_perf_counters.py b/qa/tasks/mgr/dashboard/test_perf_counters.py new file mode 100644 index 000000000000..4da9236f67dc --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_perf_counters.py @@ -0,0 +1,56 @@ +# -*- 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) diff --git a/qa/tasks/mgr/dashboard/test_pool.py b/qa/tasks/mgr/dashboard/test_pool.py new file mode 100644 index 000000000000..6852ddbb631a --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_pool.py @@ -0,0 +1,62 @@ +# -*- 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) diff --git a/qa/tasks/mgr/dashboard/test_rbd.py b/qa/tasks/mgr/dashboard/test_rbd.py new file mode 100644 index 000000000000..0835fc27fff1 --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_rbd.py @@ -0,0 +1,113 @@ +# -*- 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"}) diff --git a/qa/tasks/mgr/dashboard/test_rgw.py b/qa/tasks/mgr/dashboard/test_rgw.py new file mode 100644 index 000000000000..f6cbf843c624 --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_rgw.py @@ -0,0 +1,30 @@ +# -*- 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']) diff --git a/qa/tasks/mgr/dashboard/test_summary.py b/qa/tasks/mgr/dashboard/test_summary.py new file mode 100644 index 000000000000..df7cbaf282e9 --- /dev/null +++ b/qa/tasks/mgr/dashboard/test_summary.py @@ -0,0 +1,25 @@ +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}) diff --git a/qa/tasks/mgr/dashboard_v2/__init__.py b/qa/tasks/mgr/dashboard_v2/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/qa/tasks/mgr/dashboard_v2/helper.py b/qa/tasks/mgr/dashboard_v2/helper.py deleted file mode 100644 index f43970f110c4..000000000000 --- a/qa/tasks/mgr/dashboard_v2/helper.py +++ /dev/null @@ -1,145 +0,0 @@ -# -*- 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']] diff --git a/qa/tasks/mgr/dashboard_v2/test_auth.py b/qa/tasks/mgr/dashboard_v2/test_auth.py deleted file mode 100644 index b176dd041b1a..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_auth.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- 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) diff --git a/qa/tasks/mgr/dashboard_v2/test_cephfs.py b/qa/tasks/mgr/dashboard_v2/test_cephfs.py deleted file mode 100644 index 5669e41e07ec..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_cephfs.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- 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) diff --git a/qa/tasks/mgr/dashboard_v2/test_cluster_configuration.py b/qa/tasks/mgr/dashboard_v2/test_cluster_configuration.py deleted file mode 100644 index 6cfe605e9474..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_cluster_configuration.py +++ /dev/null @@ -1,39 +0,0 @@ -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) - diff --git a/qa/tasks/mgr/dashboard_v2/test_dashboard.py b/qa/tasks/mgr/dashboard_v2/test_dashboard.py deleted file mode 100644 index fa7001797ad2..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_dashboard.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- 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']) diff --git a/qa/tasks/mgr/dashboard_v2/test_host.py b/qa/tasks/mgr/dashboard_v2/test_host.py deleted file mode 100644 index efa28d913292..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_host.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- 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']) diff --git a/qa/tasks/mgr/dashboard_v2/test_monitor.py b/qa/tasks/mgr/dashboard_v2/test_monitor.py deleted file mode 100644 index 0b199cdb1934..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_monitor.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- 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']) diff --git a/qa/tasks/mgr/dashboard_v2/test_osd.py b/qa/tasks/mgr/dashboard_v2/test_osd.py deleted file mode 100644 index 587a448ec54e..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_osd.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- 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']) diff --git a/qa/tasks/mgr/dashboard_v2/test_perf_counters.py b/qa/tasks/mgr/dashboard_v2/test_perf_counters.py deleted file mode 100644 index 4da9236f67dc..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_perf_counters.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- 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) diff --git a/qa/tasks/mgr/dashboard_v2/test_pool.py b/qa/tasks/mgr/dashboard_v2/test_pool.py deleted file mode 100644 index 6852ddbb631a..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_pool.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- 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) diff --git a/qa/tasks/mgr/dashboard_v2/test_rbd.py b/qa/tasks/mgr/dashboard_v2/test_rbd.py deleted file mode 100644 index 0835fc27fff1..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_rbd.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- 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"}) diff --git a/qa/tasks/mgr/dashboard_v2/test_rgw.py b/qa/tasks/mgr/dashboard_v2/test_rgw.py deleted file mode 100644 index f6cbf843c624..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_rgw.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- 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']) diff --git a/qa/tasks/mgr/dashboard_v2/test_summary.py b/qa/tasks/mgr/dashboard_v2/test_summary.py deleted file mode 100644 index df7cbaf282e9..000000000000 --- a/qa/tasks/mgr/dashboard_v2/test_summary.py +++ /dev/null @@ -1,25 +0,0 @@ -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}) diff --git a/qa/tasks/mgr/test_dashboard.py b/qa/tasks/mgr/test_dashboard.py new file mode 100644 index 000000000000..79aebee4eabf --- /dev/null +++ b/qa/tasks/mgr/test_dashboard.py @@ -0,0 +1,65 @@ + + +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, []) diff --git a/qa/tasks/mgr/test_dashboard_v2.py b/qa/tasks/mgr/test_dashboard_v2.py deleted file mode 100644 index 750ef911b54c..000000000000 --- a/qa/tasks/mgr/test_dashboard_v2.py +++ /dev/null @@ -1,65 +0,0 @@ - - -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, []) diff --git a/src/pybind/mgr/CMakeLists.txt b/src/pybind/mgr/CMakeLists.txt index acd0da46c18a..360a923847e4 100644 --- a/src/pybind/mgr/CMakeLists.txt +++ b/src/pybind/mgr/CMakeLists.txt @@ -1 +1 @@ -add_subdirectory(dashboard_v2) +add_subdirectory(dashboard) diff --git a/src/pybind/mgr/dashboard/.coveragerc b/src/pybind/mgr/dashboard/.coveragerc new file mode 100644 index 000000000000..29a63192c3f8 --- /dev/null +++ b/src/pybind/mgr/dashboard/.coveragerc @@ -0,0 +1,7 @@ +[run] +omit = tests/* + */python*/* + ceph_module_mock.py + __init__.py + */mgr_module.py + diff --git a/src/pybind/mgr/dashboard/.editorconfig b/src/pybind/mgr/dashboard/.editorconfig new file mode 100644 index 000000000000..a831e3da1860 --- /dev/null +++ b/src/pybind/mgr/dashboard/.editorconfig @@ -0,0 +1,29 @@ +# 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 diff --git a/src/pybind/mgr/dashboard/.gitignore b/src/pybind/mgr/dashboard/.gitignore new file mode 100644 index 000000000000..b6369487a3ff --- /dev/null +++ b/src/pybind/mgr/dashboard/.gitignore @@ -0,0 +1,17 @@ +.coverage* +htmlcov +.tox +coverage.xml +junit*xml +__pycache__ +.cache +ceph.conf +wheelhouse* + +# IDE +.vscode +.idea +*.egg + +# virtualenv +venv diff --git a/src/pybind/mgr/dashboard/.pylintrc b/src/pybind/mgr/dashboard/.pylintrc new file mode 100644 index 000000000000..ab5d1f8a7777 --- /dev/null +++ b/src/pybind/mgr/dashboard/.pylintrc @@ -0,0 +1,548 @@ +[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*(# )??$ + +# 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 diff --git a/src/pybind/mgr/dashboard/CMakeLists.txt b/src/pybind/mgr/dashboard/CMakeLists.txt new file mode 100644 index 000000000000..512034c9793d --- /dev/null +++ b/src/pybind/mgr/dashboard/CMakeLists.txt @@ -0,0 +1,69 @@ +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") diff --git a/src/pybind/mgr/dashboard/HACKING.rst b/src/pybind/mgr/dashboard/HACKING.rst new file mode 100644 index 000000000000..f8d5e2bcd6fe --- /dev/null +++ b/src/pybind/mgr/dashboard/HACKING.rst @@ -0,0 +1,510 @@ +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 `_ and requires the +`Node Package Manager `_ ``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 `_ + 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 +`_. + +Running End-to-End Tests +~~~~~~~~~~~~~~~~~~~~~~~~ + +Run ``npm run e2e`` to execute the end-to-end tests via +`Protractor `__. + +Further Help +~~~~~~~~~~~~ + +To get more help on the Angular CLI use ``ng help`` or go check out the +`Angular CLI +README `__. + +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 +`_ 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 +`_, 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 = '' + for l in self.log_buffer: + ret += "{}
".format(l) + ret += "" + 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 + +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. + diff --git a/src/pybind/mgr/dashboard/README.rst b/src/pybind/mgr/dashboard/README.rst new file mode 100644 index 000000000000..22368188138b --- /dev/null +++ b/src/pybind/mgr/dashboard/README.rst @@ -0,0 +1,77 @@ +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 `_ +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 `_ (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 `_. + +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 `_ 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 + +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. diff --git a/src/pybind/mgr/dashboard/__init__.py b/src/pybind/mgr/dashboard/__init__.py new file mode 100644 index 000000000000..f09ef245276b --- /dev/null +++ b/src/pybind/mgr/dashboard/__init__.py @@ -0,0 +1,53 @@ +# -*- 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() diff --git a/src/pybind/mgr/dashboard/controllers/__init__.py b/src/pybind/mgr/dashboard/controllers/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/controllers/auth.py b/src/pybind/mgr/dashboard/controllers/auth.py new file mode 100644 index 000000000000..28a2f28f7d36 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/auth.py @@ -0,0 +1,91 @@ +# -*- 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 | + | | 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 ` 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) diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py new file mode 100644 index 000000000000..c4786cebaf92 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -0,0 +1,318 @@ +# -*- 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) diff --git a/src/pybind/mgr/dashboard/controllers/cluster_configuration.py b/src/pybind/mgr/dashboard/controllers/cluster_configuration.py new file mode 100644 index 000000000000..d02027b1c656 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/cluster_configuration.py @@ -0,0 +1,22 @@ +# -*- 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) diff --git a/src/pybind/mgr/dashboard/controllers/dashboard.py b/src/pybind/mgr/dashboard/controllers/dashboard.py new file mode 100644 index 000000000000..3457c2f14db5 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/dashboard.py @@ -0,0 +1,127 @@ +# -*- 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 diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py new file mode 100644 index 000000000000..8bef07185ae9 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/host.py @@ -0,0 +1,12 @@ +# -*- 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() diff --git a/src/pybind/mgr/dashboard/controllers/monitor.py b/src/pybind/mgr/dashboard/controllers/monitor.py new file mode 100644 index 000000000000..ac3bfe478b63 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/monitor.py @@ -0,0 +1,42 @@ +# -*- 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 + } diff --git a/src/pybind/mgr/dashboard/controllers/osd.py b/src/pybind/mgr/dashboard/controllers/osd.py new file mode 100644 index 000000000000..24fca6d9828d --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/osd.py @@ -0,0 +1,89 @@ +# -*- 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, + } diff --git a/src/pybind/mgr/dashboard/controllers/perf_counters.py b/src/pybind/mgr/dashboard/controllers/perf_counters.py new file mode 100644 index 000000000000..59692d3309a1 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/perf_counters.py @@ -0,0 +1,66 @@ +# -*- 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 diff --git a/src/pybind/mgr/dashboard/controllers/pool.py b/src/pybind/mgr/dashboard/controllers/pool.py new file mode 100644 index 000000000000..2eac9f50f881 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/pool.py @@ -0,0 +1,49 @@ +# -*- 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] diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py new file mode 100644 index 000000000000..b73697b0a160 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/rbd.py @@ -0,0 +1,129 @@ +# -*- 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} diff --git a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py new file mode 100644 index 000000000000..62164ffa4799 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py @@ -0,0 +1,305 @@ +# -*- 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 + } diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py new file mode 100644 index 000000000000..4f8e169e917e --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -0,0 +1,70 @@ +# -*- 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 diff --git a/src/pybind/mgr/dashboard/controllers/summary.py b/src/pybind/mgr/dashboard/controllers/summary.py new file mode 100644 index 000000000000..93631bbaec1d --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/summary.py @@ -0,0 +1,69 @@ +# -*- 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() + } diff --git a/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py b/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py new file mode 100644 index 000000000000..f4849b7daa54 --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py @@ -0,0 +1,76 @@ +# -*- 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']), + } diff --git a/src/pybind/mgr/dashboard/frontend/.angular-cli.json b/src/pybind/mgr/dashboard/frontend/.angular-cli.json new file mode 100644 index 000000000000..6ffd7b7d6e8f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/.angular-cli.json @@ -0,0 +1,66 @@ +{ + "$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": {} + } +} diff --git a/src/pybind/mgr/dashboard/frontend/.editorconfig b/src/pybind/mgr/dashboard/frontend/.editorconfig new file mode 100644 index 000000000000..6e87a003da89 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/.editorconfig @@ -0,0 +1,13 @@ +# 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 diff --git a/src/pybind/mgr/dashboard/frontend/.gitignore b/src/pybind/mgr/dashboard/frontend/.gitignore new file mode 100644 index 000000000000..2e55dc6354dd --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/.gitignore @@ -0,0 +1,50 @@ +# 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 diff --git a/src/pybind/mgr/dashboard/frontend/e2e/app.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/app.e2e-spec.ts new file mode 100644 index 000000000000..3e9837024233 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/e2e/app.e2e-spec.ts @@ -0,0 +1,14 @@ +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!'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/e2e/app.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/app.po.ts new file mode 100644 index 000000000000..d9761bb4a4e4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/e2e/app.po.ts @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get('/'); + } + + getParagraphText() { + return element(by.css('oa-root h1')).getText(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/e2e/tsconfig.e2e.json b/src/pybind/mgr/dashboard/frontend/e2e/tsconfig.e2e.json new file mode 100644 index 000000000000..1d9e5edf0965 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/e2e/tsconfig.e2e.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/e2e", + "baseUrl": "./", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} diff --git a/src/pybind/mgr/dashboard/frontend/karma.conf.js b/src/pybind/mgr/dashboard/frontend/karma.conf.js new file mode 100644 index 000000000000..f86ab20aa5fd --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/karma.conf.js @@ -0,0 +1,40 @@ +// 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 + }); +}; diff --git a/src/pybind/mgr/dashboard/frontend/package.json b/src/pybind/mgr/dashboard/frontend/package.json new file mode 100644 index 000000000000..e173870909fb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/package.json @@ -0,0 +1,65 @@ +{ + "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" + } +} diff --git a/src/pybind/mgr/dashboard/frontend/protractor.conf.js b/src/pybind/mgr/dashboard/frontend/protractor.conf.js new file mode 100644 index 000000000000..7ee3b5ee863a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/protractor.conf.js @@ -0,0 +1,28 @@ +// 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 } })); + } +}; diff --git a/src/pybind/mgr/dashboard/frontend/proxy.conf.json.sample b/src/pybind/mgr/dashboard/frontend/proxy.conf.json.sample new file mode 100644 index 000000000000..e654419c9cfd --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/proxy.conf.json.sample @@ -0,0 +1,7 @@ +{ + "/api/": { + "target": "http://localhost:8080", + "secure": false, + "logLevel": "debug" + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts new file mode 100644 index 000000000000..8883796d367d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -0,0 +1,54 @@ +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 { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.html b/src/pybind/mgr/dashboard/frontend/src/app/app.component.html new file mode 100644 index 000000000000..638edaa604f1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.html @@ -0,0 +1,5 @@ + +
+ +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/app.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts new file mode 100644 index 000000000000..3cca10d09170 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts @@ -0,0 +1,28 @@ +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(); + }) + ); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/app.component.ts new file mode 100644 index 000000000000..c9e0e7e8773b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.ts @@ -0,0 +1,27 @@ +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(); + } + +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts new file mode 100644 index 000000000000..525e94729fad --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts @@ -0,0 +1,57 @@ +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 { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts new file mode 100644 index 000000000000..6e094fa04d6a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts @@ -0,0 +1,35 @@ +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 { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html new file mode 100644 index 000000000000..68f9326690cd --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html @@ -0,0 +1,20 @@ + + +Daemons + + + +Images + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts new file mode 100644 index 000000000000..78c19b072475 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts @@ -0,0 +1,37 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts new file mode 100644 index 000000000000..9d700f44ed20 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts @@ -0,0 +1,102 @@ +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; + }); + } + +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts new file mode 100644 index 000000000000..f22bcf2a599b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts @@ -0,0 +1,8 @@ +import { MirrorHealthColorPipe } from './mirror-health-color.pipe'; + +describe('MirrorHealthColorPipe', () => { + it('create an instance', () => { + const pipe = new MirrorHealthColorPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.ts new file mode 100644 index 000000000000..43d880ffb1aa --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirror-health-color.pipe.ts @@ -0,0 +1,17 @@ +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'; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.html new file mode 100644 index 000000000000..a76047d431f6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.html @@ -0,0 +1,94 @@ + + + + +
+
+
+ Daemons + + + +
+
+ +
+
+ Pools + + + +
+
+
+ +
+
+
+ Images + + + + + + + + + + + + + + +
+
+
+ + + {{ value }} + + + + {{ value }} + + + + Syncing + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts new file mode 100644 index 000000000000..f20d0484c018 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts @@ -0,0 +1,50 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.ts new file mode 100644 index 000000000000..63e960ef52e4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.ts @@ -0,0 +1,137 @@ +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; + @ViewChild('stateTmpl') stateTmpl: TemplateRef; + @ViewChild('syncTmpl') syncTmpl: TemplateRef; + @ViewChild('progressTmpl') progressTmpl: TemplateRef; + + 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; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html new file mode 100644 index 000000000000..1bdd5a22fbe7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html @@ -0,0 +1,18 @@ + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts new file mode 100644 index 000000000000..aea790cf1da7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts @@ -0,0 +1,40 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts new file mode 100644 index 000000000000..98ac59c66640 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts @@ -0,0 +1,92 @@ +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; + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts new file mode 100644 index 000000000000..0f74b8234a6e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts @@ -0,0 +1,25 @@ +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 { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html new file mode 100644 index 000000000000..b98d70838a00 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html @@ -0,0 +1,12 @@ +
+ + +
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss new file mode 100644 index 000000000000..62a023b9aaf4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss @@ -0,0 +1,6 @@ +@import '../../../../styles/chart-tooltip.scss'; + +.chart-container { + height: 500px; + width: 100%; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts new file mode 100644 index 000000000000..6d552041f1ca --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts @@ -0,0 +1,29 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts new file mode 100644 index 000000000000..cca1ae2feb92 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts @@ -0,0 +1,164 @@ +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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts new file mode 100644 index 000000000000..c47051c18e6e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts @@ -0,0 +1,25 @@ +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 {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts new file mode 100644 index 000000000000..a9e59a016dd5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts @@ -0,0 +1,20 @@ +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(); + }) + ); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.ts new file mode 100644 index 000000000000..a5c4994da451 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.service.ts @@ -0,0 +1,21 @@ +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}`); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html new file mode 100644 index 000000000000..ef62292d8ad3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html @@ -0,0 +1,69 @@ + + + + +
+
+
+ Ranks + + + +
+ + + +
+ +
+
+ Pools + + + + +
+
+
+ +
+
+ +
+
+ + + + + + + + + {{ row.state === 'standby-replay' ? 'Evts' : 'Reqs' }}: {{ value | dimless }} /s + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss new file mode 100644 index 000000000000..d82829af85c4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss @@ -0,0 +1,3 @@ +.progress { + margin-bottom: 0px; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts new file mode 100644 index 000000000000..3df655defa6d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts @@ -0,0 +1,57 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts new file mode 100644 index 000000000000..d8fe382fec6f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts @@ -0,0 +1,126 @@ +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; + @ViewChild('activityTmpl') activityTmpl: TemplateRef; + + 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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.html new file mode 100644 index 000000000000..7832a38744f0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.html @@ -0,0 +1,22 @@ + + +
+ + + + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts new file mode 100644 index 000000000000..d3506a906861 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts @@ -0,0 +1,51 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.ts new file mode 100644 index 000000000000..fc2cbdec61b2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/clients/clients.component.ts @@ -0,0 +1,56 @@ +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; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts new file mode 100644 index 000000000000..d661f51baed4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -0,0 +1,48 @@ +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 {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html new file mode 100644 index 000000000000..efe071a02278 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html @@ -0,0 +1,67 @@ + + +
+
+ +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescriptionTypeLevelDefaultTagsServicesSee_alsoMaxMin
{{ row.name }} +

+ {{ row.desc }}

+

{{ row.long_desc }}

+
{{ row.type }}{{ row.level }} + {{ row.default }} {{ row.daemon_default }} + +

{{ item }}

+
+

{{ item }}

+
+

{{ item }}

+
{{ row.max }}{{ row.min }}
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.scss new file mode 100644 index 000000000000..e968d6d90861 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.scss @@ -0,0 +1,5 @@ +@import '../../../shared/datatable/table/table.component.scss'; + +td.wrap { + word-break: break-all; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts new file mode 100644 index 000000000000..0d98766efd46 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts @@ -0,0 +1,41 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts new file mode 100644 index 000000000000..7c6ed68b448c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts @@ -0,0 +1,79 @@ +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; + + 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]; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html new file mode 100644 index 000000000000..f2935c3a0a15 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html @@ -0,0 +1,19 @@ + + + + + {{ service.type }}.{{ service.id }}{{ !isLast ? ", " : "" }} + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts new file mode 100644 index 000000000000..90eb5e6498ba --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts @@ -0,0 +1,40 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts new file mode 100644 index 000000000000..28a193fd8ce8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts @@ -0,0 +1,64 @@ +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 = []; + hosts: Array = []; + isLoadingHosts = false; + + @ViewChild('servicesTpl') public servicesTpl: TemplateRef; + + 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; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.spec.ts new file mode 100644 index 000000000000..1d5f7de97b77 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.spec.ts @@ -0,0 +1,21 @@ +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(); + })); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.ts new file mode 100644 index 000000000000..32057f3b6ea1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor.service.ts @@ -0,0 +1,11 @@ +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'); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html new file mode 100644 index 000000000000..d59de84c1451 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html @@ -0,0 +1,72 @@ + + +
+
+
+ Status + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Cluster ID{{ mon_status.monmap.fsid }}
monmap modified{{ mon_status.monmap.modified }}
monmap epoch{{ mon_status.monmap.epoch }}
quorum con{{ mon_status.features.quorum_con }}
quorum mon{{ mon_status.features.quorum_mon }}
required con{{ mon_status.features.required_con }}
required mon{{ mon_status.features.required_mon }}
+
+
+ +
+
+ In Quorum + + + + Not In Quorum + + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts new file mode 100644 index 000000000000..906581e76b98 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts @@ -0,0 +1,23 @@ +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; + + beforeEach( + async(() => { + TestBed.configureTestingModule({ + imports: [AppModule] + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.ts new file mode 100644 index 000000000000..0a23129667ef --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.ts @@ -0,0 +1,66 @@ +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; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html new file mode 100644 index 000000000000..c511d54e78c1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + +

+ Histogram not available -> {{ osd.histogram_failed }} +

+
+
+

Writes

+ + +
+
+

Reads

+ + +
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts new file mode 100644 index 000000000000..c24618286de1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts @@ -0,0 +1,48 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts new file mode 100644 index 000000000000..7f2af3724976 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts @@ -0,0 +1,44 @@ +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; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html new file mode 100644 index 000000000000..2683102f0188 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html @@ -0,0 +1,25 @@ + + + + + + + + + + {{ state }}, + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts new file mode 100644 index 000000000000..506b536a6364 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts @@ -0,0 +1,47 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts new file mode 100644 index 000000000000..29f0f22ede28 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts @@ -0,0 +1,75 @@ +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; + + 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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html new file mode 100644 index 000000000000..080f121f3bf5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html @@ -0,0 +1,9 @@ + + + + +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts new file mode 100644 index 000000000000..7ff7d646a286 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OsdPerformanceHistogramComponent } from './osd-performance-histogram.component'; + +describe('OsdPerformanceHistogramComponent', () => { + let component: OsdPerformanceHistogramComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ OsdPerformanceHistogramComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(OsdPerformanceHistogramComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts new file mode 100644 index 000000000000..c3f06450659f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts @@ -0,0 +1,61 @@ +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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts new file mode 100644 index 000000000000..115d6a4dbd96 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts @@ -0,0 +1,19 @@ +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(); + })); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.ts new file mode 100644 index 000000000000..cf9adf1b5fd7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd.service.ts @@ -0,0 +1,17 @@ +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}`); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts new file mode 100644 index 000000000000..cf4c025060fc --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts @@ -0,0 +1,37 @@ +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 {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts new file mode 100644 index 000000000000..bf061e9f6452 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts @@ -0,0 +1,23 @@ +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(); + }) + ); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.ts new file mode 100644 index 000000000000..cb51cb4d71b0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.service.ts @@ -0,0 +1,11 @@ +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'); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html new file mode 100644 index 000000000000..89a37fd6c0eb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html @@ -0,0 +1,12 @@ +
+ + + + + + + + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss new file mode 100644 index 000000000000..04eee2d6f256 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss @@ -0,0 +1,3 @@ +div { + padding-top: 20px; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts new file mode 100644 index 000000000000..80500c0b9ae3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DashboardComponent } from './dashboard.component'; + +describe('DashboardComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DashboardComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts new file mode 100644 index 000000000000..fc676c74c6d3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts @@ -0,0 +1,16 @@ +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() { + } + +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html new file mode 100644 index 000000000000..7135f96f67bb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html @@ -0,0 +1,15 @@ +
+ +
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss new file mode 100644 index 000000000000..b3abf8681a29 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss @@ -0,0 +1 @@ +@import '../../../../styles/chart-tooltip.scss'; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts new file mode 100644 index 000000000000..dca539f041c2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts @@ -0,0 +1,30 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts new file mode 100644 index 000000000000..196d871066ac --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts @@ -0,0 +1,117 @@ +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]); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html new file mode 100644 index 000000000000..348324e4dc13 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html @@ -0,0 +1,209 @@ +
+
+ +
+
+
+ Health + Overall status: + {{ contentData.health.status }} +
    +
  • + {{ check.type }}: {{ check.summary.message }} +
  • +
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+ + Monitors + + {{ contentData.mon_status | monSummary }} +
+
+
+
+
+
+
+
+ +
+
+ + OSDs + + {{ contentData.osd_map | osdSummary }} +
+
+
+
+
+
+
+
+
+
+ +
+
+ Metadata servers + {{ contentData.fs_map | mdsSummary }} +
+
+
+
+
+
+
+
+ +
+
+ Manager daemons + {{ contentData.mgr_map | mgrSummary }} +
+
+
+
+
+
+
+ +
+ +
+
+
+ Usage + + + + + + + + + + + + +
+ {{ contentData.df.stats.total_objects | dimless }} + +
+ +
+
+
+ +
+
Objects + Raw capacity +
+ ({{ contentData.df.stats.total_used_bytes | dimlessBinary }} used) +
Usage by pool
+
+
+
+ +
+
+
+ Pools + + + + + + + + + + + + + + + + + + + + + +
NamePG statusUsageReadWrite
{{ pool.pool_name }} + {{ pool.pg_status | pgStatus }} + + {{ pool.stats.bytes_used.latest | dimlessBinary }} / {{ pool.stats.max_avail.latest | dimlessBinary }} + + {{ pool.stats.rd_bytes.rate | dimless }} + + {{ pool.stats.rd.rate | dimless }} ops + + {{ pool.stats.wr_bytes.rate | dimless }} + + {{ pool.stats.wr.rate | dimless }} ops +
+
+
+
+
+ +
+
+ +
+
+ Logs + + + + + {{ line.stamp }} {{ line.priority }}  + + {{ line.message }} +
+
+
+
+ + + {{ line.stamp }} {{ line.priority }}  + + + {{ line.message }} + +
+
+
+
+
+
+
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.scss new file mode 100644 index 000000000000..919b41d10fc6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.scss @@ -0,0 +1,62 @@ +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; + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts new file mode 100644 index 000000000000..983b1452e89d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts @@ -0,0 +1,35 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts new file mode 100644 index 000000000000..3cdddc970e3c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts @@ -0,0 +1,99 @@ +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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts new file mode 100644 index 000000000000..43af68d08af2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts @@ -0,0 +1,8 @@ +import { LogColorPipe } from './log-color.pipe'; + +describe('LogColorPipe', () => { + it('create an instance', () => { + const pipe = new LogColorPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.ts new file mode 100644 index 000000000000..eb60ddba4c33 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/log-color.pipe.ts @@ -0,0 +1,21 @@ +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 ''; + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts new file mode 100644 index 000000000000..37883a82a987 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts @@ -0,0 +1,8 @@ +import { MdsSummaryPipe } from './mds-summary.pipe'; + +describe('MdsSummaryPipe', () => { + it('create an instance', () => { + const pipe = new MdsSummaryPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts new file mode 100644 index 000000000000..9e6eeca6e8ac --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts @@ -0,0 +1,38 @@ +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'; + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts new file mode 100644 index 000000000000..fdab76c4808d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts @@ -0,0 +1,8 @@ +import { MgrSummaryPipe } from './mgr-summary.pipe'; + +describe('MgrSummaryPipe', () => { + it('create an instance', () => { + const pipe = new MgrSummaryPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts new file mode 100644 index 000000000000..cf793e66e475 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts @@ -0,0 +1,22 @@ +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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts new file mode 100644 index 000000000000..49526cf3fcd7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts @@ -0,0 +1,8 @@ +import { MonSummaryPipe } from './mon-summary.pipe'; + +describe('MonSummaryPipe', () => { + it('create an instance', () => { + const pipe = new MonSummaryPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts new file mode 100644 index 000000000000..6877e2247c7e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts @@ -0,0 +1,18 @@ +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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts new file mode 100644 index 000000000000..466eec1ac30e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts @@ -0,0 +1,8 @@ +import { OsdSummaryPipe } from './osd-summary.pipe'; + +describe('OsdSummaryPipe', () => { + it('create an instance', () => { + const pipe = new OsdSummaryPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts new file mode 100644 index 000000000000..b02d97644bb8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts @@ -0,0 +1,26 @@ +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)'; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts new file mode 100644 index 000000000000..67c5f10c5e35 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts @@ -0,0 +1,8 @@ +import { PgStatusStylePipe } from './pg-status-style.pipe'; + +describe('PgStatusStylePipe', () => { + it('create an instance', () => { + const pipe = new PgStatusStylePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts new file mode 100644 index 000000000000..4e9afab97cfa --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts @@ -0,0 +1,40 @@ +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' }; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts new file mode 100644 index 000000000000..d7d5592b653f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts @@ -0,0 +1,8 @@ +import { PgStatusPipe } from './pg-status.pipe'; + +describe('PgStatusPipe', () => { + it('create an instance', () => { + const pipe = new PgStatusPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.ts new file mode 100644 index 000000000000..5c6c7b393c30 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/pg-status.pipe.ts @@ -0,0 +1,16 @@ +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(', '); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter.module.ts new file mode 100644 index 000000000000..f2c20299f4c4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter.module.ts @@ -0,0 +1,31 @@ +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 { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html new file mode 100644 index 000000000000..ebb9ba93d62f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html @@ -0,0 +1,7 @@ +
+ Performance Counters +

{{ serviceType }}.{{ serviceId }}

+ + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts new file mode 100644 index 000000000000..a4cc71702317 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts @@ -0,0 +1,49 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts new file mode 100644 index 000000000000..25fa82e4e9d1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts @@ -0,0 +1,26 @@ +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(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts new file mode 100644 index 000000000000..6f0af94e4b60 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts @@ -0,0 +1,27 @@ +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(); + }) + ); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts new file mode 100644 index 000000000000..b6ac5d5fe3e6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts @@ -0,0 +1,28 @@ +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 => { + return resp['counters']; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html new file mode 100644 index 000000000000..6564dc1ab134 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html @@ -0,0 +1,8 @@ + + + {{ row.value | dimless }} {{ row.unit }} + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts new file mode 100644 index 000000000000..4baefe8911da --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts @@ -0,0 +1,38 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts new file mode 100644 index 000000000000..6ac05c9849a6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts @@ -0,0 +1,59 @@ +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 = []; + counters: Array = []; + + @ViewChild('valueTpl') public valueTpl: TemplateRef; + + /** + * 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; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html new file mode 100644 index 000000000000..81c5919db697 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts new file mode 100644 index 000000000000..afce56a2bacf --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts @@ -0,0 +1,47 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts new file mode 100644 index 000000000000..8ac62fac84c8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts @@ -0,0 +1,36 @@ +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']; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html new file mode 100644 index 000000000000..64b703fd98e4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html @@ -0,0 +1,18 @@ + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts new file mode 100644 index 000000000000..c0d331e3599f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts @@ -0,0 +1,41 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts new file mode 100644 index 000000000000..ce1c2456fdf0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts @@ -0,0 +1,51 @@ +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 = []; + daemons: Array = []; + 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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts new file mode 100644 index 000000000000..a888940c27da --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -0,0 +1,34 @@ +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 { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts new file mode 100644 index 000000000000..691cc787a9b8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts @@ -0,0 +1,21 @@ +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(); + }) + ); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts new file mode 100644 index 000000000000..907537ef2d9e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts @@ -0,0 +1,26 @@ +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; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts new file mode 100644 index 000000000000..e96b1b30b8f8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts @@ -0,0 +1,18 @@ +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 { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html new file mode 100644 index 000000000000..e0b33c893d68 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.html @@ -0,0 +1,73 @@ + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss new file mode 100644 index 000000000000..1f77356d2967 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss @@ -0,0 +1,31 @@ +@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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.spec.ts new file mode 100644 index 000000000000..b8307b594e81 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.spec.ts @@ -0,0 +1,40 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts new file mode 100644 index 000000000000..f8f46254976c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts @@ -0,0 +1,38 @@ +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(['']); + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.html new file mode 100644 index 000000000000..993fd95ebb1a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.html @@ -0,0 +1,6 @@ + + + Logout + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.spec.ts new file mode 100644 index 000000000000..318ca820af2d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.spec.ts @@ -0,0 +1,35 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.ts new file mode 100644 index 000000000000..4bf11e368712 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/logout/logout.component.ts @@ -0,0 +1,24 @@ +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']); + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts new file mode 100644 index 000000000000..bd1768158e7e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts @@ -0,0 +1,17 @@ +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 { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts new file mode 100644 index 000000000000..823d4feea184 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts @@ -0,0 +1,24 @@ +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 {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html new file mode 100644 index 000000000000..cd7e8040c81b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -0,0 +1,228 @@ + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts new file mode 100644 index 000000000000..7548b2bd7c1f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts @@ -0,0 +1,37 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts new file mode 100644 index 000000000000..ee61c41134fa --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts @@ -0,0 +1,31 @@ +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 = []; + + 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' }; + } + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html new file mode 100644 index 000000000000..0f3847b93934 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html @@ -0,0 +1,14 @@ +
+
+

Sorry, we could not find what you were looking for

+ + + + "Mimic Octopus" by prilfish is licensed under + CC BY 2.0 + +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss new file mode 100644 index 000000000000..e94d9f2fb82c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss @@ -0,0 +1,15 @@ +h1 { + font-size: -webkit-xxx-large; +} + +h2 { + font-size: xx-large; +} + +*{ + font-family: monospace; +} + +img{ + width: 50vw; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts new file mode 100644 index 000000000000..35189ed0b766 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NotFoundComponent } from './not-found.component'; + +describe('NotFoundComponent', () => { + let component: NotFoundComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ NotFoundComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NotFoundComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.ts new file mode 100644 index 000000000000..d12bc32100de --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'cd-not-found', + templateUrl: './not-found.component.html', + styleUrls: ['./not-found.component.scss'] +}) +export class NotFoundComponent { + constructor() {} +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts new file mode 100644 index 000000000000..fe65bea599bb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts @@ -0,0 +1,26 @@ +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 { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.html new file mode 100644 index 000000000000..4b7a1b872239 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.html @@ -0,0 +1,13 @@ +
+ + +
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.scss new file mode 100644 index 000000000000..ec7d98291e8c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.scss @@ -0,0 +1,5 @@ +@import '../../../../styles/chart-tooltip.scss'; + +.chart-container { + position: static !important; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts new file mode 100644 index 000000000000..4a879c3fcbd1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts @@ -0,0 +1,27 @@ +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; + + beforeEach( + async(() => { + TestBed.configureTestingModule({ + imports: [AppModule] + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(SparklineComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.ts new file mode 100644 index 000000000000..fa20ce301864 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/sparkline/sparkline.component.ts @@ -0,0 +1,108 @@ +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 = [ + { + 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 = [ + { + data: [] + } + ]; + + public labels: Array = []; + + 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)]; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.html new file mode 100644 index 000000000000..1d71da24c1ab --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.html @@ -0,0 +1,17 @@ + + Retrieving data, please wait. + + + + Displaying previously cached data. + + + + Could not load data. Please check the cluster health. + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts new file mode 100644 index 000000000000..da68def0b62d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts @@ -0,0 +1,28 @@ +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; + + 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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.ts new file mode 100644 index 000000000000..63bc97947c2f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/view-cache/view-cache.component.ts @@ -0,0 +1,17 @@ +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() {} +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts new file mode 100644 index 000000000000..b09a31ebaa7c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts @@ -0,0 +1,34 @@ +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 { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html new file mode 100644 index 000000000000..d0ab74c07863 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html @@ -0,0 +1,9 @@ + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts new file mode 100644 index 000000000000..16e05bd1818c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts @@ -0,0 +1,99 @@ +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; + + 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'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts new file mode 100644 index 000000000000..101580f35ef1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts @@ -0,0 +1,100 @@ +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 = []; + + @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(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html new file mode 100644 index 000000000000..ba6adf5ae7e7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html @@ -0,0 +1,120 @@ +
+
+ +
+ +
+ + + +
+ + + + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + +
+ +
+ + +
+ + + + + + + {{ value }} + + + + + + + + {{ value }} + + + + {{ value }} /s + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss new file mode 100644 index 000000000000..db9c4eb97772 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss @@ -0,0 +1,245 @@ +@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%; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts new file mode 100644 index 000000000000..60ec7d013279 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts @@ -0,0 +1,145 @@ +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; + 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); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts new file mode 100644 index 000000000000..9f04e91c71ca --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts @@ -0,0 +1,282 @@ +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; + @ViewChild('sparklineTpl') sparklineTpl: TemplateRef; + @ViewChild('routerLinkTpl') routerLinkTpl: TemplateRef; + @ViewChild('perSecondTpl') perSecondTpl: TemplateRef; + + // 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 + } = {}; + 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) + }; + }; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.spec.ts new file mode 100644 index 000000000000..1fc8f9c7cbb9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.spec.ts @@ -0,0 +1,8 @@ +import { PasswordButtonDirective } from './password-button.directive'; + +describe('PasswordButtonDirective', () => { + it('should create an instance', () => { + const directive = new PasswordButtonDirective(null, null); + expect(directive).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts new file mode 100644 index 000000000000..b375ba256b0a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/password-button.directive.ts @@ -0,0 +1,40 @@ +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(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts new file mode 100644 index 000000000000..7c1c2162f58d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts @@ -0,0 +1,6 @@ +export enum CellTemplate { + bold = 'bold', + sparkline = 'sparkline', + perSecond = 'perSecond', + routerLink = 'routerLink' +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/view-cache-status.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/view-cache-status.enum.ts new file mode 100644 index 000000000000..169059c4405a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/view-cache-status.enum.ts @@ -0,0 +1,6 @@ +export enum ViewCacheStatus { + ValueOk = 0, + ValueStale = 1, + ValueNone = 2, + ValueException = 3 +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts new file mode 100644 index 000000000000..bf45c4818748 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column.ts @@ -0,0 +1,7 @@ +import { TableColumn } from '@swimlane/ngx-datatable'; +import { CellTemplate } from '../enum/cell-template.enum'; + +export interface CdTableColumn extends TableColumn { + cellTransformation?: CellTemplate; + isHidden?: boolean; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-selection.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-selection.ts new file mode 100644 index 000000000000..9732abc5a032 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-selection.ts @@ -0,0 +1,28 @@ +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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/chart-tooltip.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/chart-tooltip.ts new file mode 100644 index 000000000000..56962f3da69f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/chart-tooltip.ts @@ -0,0 +1,117 @@ +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 = ''; + + titleLines.forEach(title => { + innerHtml += '' + this.getTitle(title) + ''; + }); + innerHtml += ''; + + 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 = ''; + innerHtml += '' + span + this.getBody(body) + ''; + }); + innerHtml += ''; + + 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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts new file mode 100644 index 000000000000..b33c366c0376 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/credentials.ts @@ -0,0 +1,5 @@ +export class Credentials { + username: string; + password: string; + stay_signed_in = false; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts new file mode 100644 index 000000000000..bfe10c2f4697 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts @@ -0,0 +1,8 @@ +import { CephShortVersionPipe } from './ceph-short-version.pipe'; + +describe('CephShortVersionPipe', () => { + it('create an instance', () => { + const pipe = new CephShortVersionPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts new file mode 100644 index 000000000000..9599112c811c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts @@ -0,0 +1,18 @@ +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; + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts new file mode 100644 index 000000000000..2424ebc16bb8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts @@ -0,0 +1,10 @@ +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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.ts new file mode 100644 index 000000000000..92f000824aa7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary.pipe.ts @@ -0,0 +1,20 @@ +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' + ]); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.spec.ts new file mode 100644 index 000000000000..4bbfdd8564c9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.spec.ts @@ -0,0 +1,10 @@ +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(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.ts new file mode 100644 index 000000000000..5e02846e3520 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless.pipe.ts @@ -0,0 +1,20 @@ +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' + ]); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.spec.ts new file mode 100644 index 000000000000..1427de361bd6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.spec.ts @@ -0,0 +1,8 @@ +import { FilterPipe } from './filter.pipe'; + +describe('FilterPipe', () => { + it('create an instance', () => { + const pipe = new FilterPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.ts new file mode 100644 index 000000000000..21115a7b6d01 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/filter.pipe.ts @@ -0,0 +1,25 @@ +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; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.spec.ts new file mode 100644 index 000000000000..e0e44e0eb85d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.spec.ts @@ -0,0 +1,8 @@ +import { HealthColorPipe } from './health-color.pipe'; + +describe('HealthColorPipe', () => { + it('create an instance', () => { + const pipe = new HealthColorPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.ts new file mode 100644 index 000000000000..9d82475a14f6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.ts @@ -0,0 +1,18 @@ +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; + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.spec.ts new file mode 100644 index 000000000000..768f12a24e52 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.spec.ts @@ -0,0 +1,8 @@ +import { ListPipe } from './list.pipe'; + +describe('ListPipe', () => { + it('create an instance', () => { + const pipe = new ListPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.ts new file mode 100644 index 000000000000..1e379190e04e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/list.pipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'list' +}) +export class ListPipe implements PipeTransform { + transform(value: any, args?: any): any { + return value.join(', '); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts new file mode 100644 index 000000000000..51dc736c8a69 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts @@ -0,0 +1,40 @@ +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 {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts new file mode 100644 index 000000000000..1295b0dc25b6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts @@ -0,0 +1,8 @@ +import { RelativeDatePipe } from './relative-date.pipe'; + +describe('RelativeDatePipe', () => { + it('create an instance', () => { + const pipe = new RelativeDatePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts new file mode 100644 index 000000000000..6bfa3958c628 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts @@ -0,0 +1,17 @@ +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(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.ts new file mode 100644 index 000000000000..a3ec80387032 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.ts @@ -0,0 +1,19 @@ +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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-interceptor.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-interceptor.service.ts new file mode 100644 index 000000000000..f09250d6c172 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-interceptor.service.ts @@ -0,0 +1,42 @@ +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, next: HttpHandler): Observable> { + return next.handle(request).do((event: HttpEvent) => { + 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']); + } + } + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts new file mode 100644 index 000000000000..cd6dbbe7a0b2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts @@ -0,0 +1,21 @@ +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; + } + +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth.service.ts new file mode 100644 index 000000000000..88a7136289e3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth.service.ts @@ -0,0 +1,25 @@ +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(); + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.spec.ts new file mode 100644 index 000000000000..dcb5a9e10b9f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.spec.ts @@ -0,0 +1,21 @@ +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(); + }) + ); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.ts new file mode 100644 index 000000000000..41ac7bb5196a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/configuration.service.ts @@ -0,0 +1,11 @@ +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/'); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts new file mode 100644 index 000000000000..f3a99b595488 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.spec.ts @@ -0,0 +1,15 @@ +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(); + })); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts new file mode 100644 index 000000000000..3986408798fd --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts @@ -0,0 +1,51 @@ +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]); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/host.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/host.service.ts new file mode 100644 index 000000000000..3d28cd78926a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/host.service.ts @@ -0,0 +1,15 @@ +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; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/pool.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/pool.service.ts new file mode 100644 index 000000000000..8ac6de9d5b4e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/pool.service.ts @@ -0,0 +1,15 @@ +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; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts new file mode 100644 index 000000000000..0f598318a31d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts @@ -0,0 +1,18 @@ +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(); + })); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.ts new file mode 100644 index 000000000000..b840b3053a05 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/rbd-mirroring.service.ts @@ -0,0 +1,11 @@ +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'); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/services.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/services.module.ts new file mode 100644 index 000000000000..04d4a3ccbde3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/services.module.ts @@ -0,0 +1,21 @@ +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 { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts new file mode 100644 index 000000000000..23af9836a147 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.spec.ts @@ -0,0 +1,21 @@ +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(); + }) + ); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.ts new file mode 100644 index 000000000000..9556930ebd42 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/summary.service.ts @@ -0,0 +1,31 @@ +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); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/tcmu-iscsi.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/tcmu-iscsi.service.ts new file mode 100644 index 000000000000..2f36bb81813b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/tcmu-iscsi.service.ts @@ -0,0 +1,15 @@ +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; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts new file mode 100644 index 000000000000..7651338d9c0b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts @@ -0,0 +1,43 @@ +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 {} diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/.gitkeep b/src/pybind/mgr/dashboard/frontend/src/assets/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/1280px-Mimic_Octopus2.jpg b/src/pybind/mgr/dashboard/frontend/src/assets/1280px-Mimic_Octopus2.jpg new file mode 100644 index 000000000000..f8cf2a8de5f3 Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/1280px-Mimic_Octopus2.jpg differ diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png new file mode 100644 index 000000000000..26d602be3bc3 Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png differ diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png new file mode 100644 index 000000000000..0f07b83ed9a3 Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png differ diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/loading.gif b/src/pybind/mgr/dashboard/frontend/src/assets/loading.gif new file mode 100755 index 000000000000..8fb88dea377e Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/loading.gif differ diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/logo-mini.png b/src/pybind/mgr/dashboard/frontend/src/assets/logo-mini.png new file mode 100644 index 000000000000..b3446a894dc7 Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/logo-mini.png differ diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/notification-icons.png b/src/pybind/mgr/dashboard/frontend/src/assets/notification-icons.png new file mode 100644 index 000000000000..d609a7c8cb49 Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/assets/notification-icons.png differ diff --git a/src/pybind/mgr/dashboard/frontend/src/defaults.scss b/src/pybind/mgr/dashboard/frontend/src/defaults.scss new file mode 100644 index 000000000000..8e25b71e2806 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/defaults.scss @@ -0,0 +1,11 @@ +$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; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/environments/environment.prod.ts b/src/pybind/mgr/dashboard/frontend/src/environments/environment.prod.ts new file mode 100644 index 000000000000..3612073bc31c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/src/pybind/mgr/dashboard/frontend/src/environments/environment.ts b/src/pybind/mgr/dashboard/frontend/src/environments/environment.ts new file mode 100644 index 000000000000..b7f639aecac5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/environments/environment.ts @@ -0,0 +1,8 @@ +// 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 +}; diff --git a/src/pybind/mgr/dashboard/frontend/src/favicon.ico b/src/pybind/mgr/dashboard/frontend/src/favicon.ico new file mode 100644 index 000000000000..90e538ba7049 Binary files /dev/null and b/src/pybind/mgr/dashboard/frontend/src/favicon.ico differ diff --git a/src/pybind/mgr/dashboard/frontend/src/index.html b/src/pybind/mgr/dashboard/frontend/src/index.html new file mode 100644 index 000000000000..05a8f7091167 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/index.html @@ -0,0 +1,28 @@ + + + + + Ceph + + + + + + + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/main.ts b/src/pybind/mgr/dashboard/frontend/src/main.ts new file mode 100644 index 000000000000..91ec6da5f078 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/main.ts @@ -0,0 +1,12 @@ +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)); diff --git a/src/pybind/mgr/dashboard/frontend/src/openattic-theme.scss b/src/pybind/mgr/dashboard/frontend/src/openattic-theme.scss new file mode 100755 index 000000000000..aa819a2f49e0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/openattic-theme.scss @@ -0,0 +1,1184 @@ +/* + 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; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/polyfills.ts b/src/pybind/mgr/dashboard/frontend/src/polyfills.ts new file mode 100644 index 000000000000..caac2e0875de --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/polyfills.ts @@ -0,0 +1,71 @@ +/** + * 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'; diff --git a/src/pybind/mgr/dashboard/frontend/src/styles.scss b/src/pybind/mgr/dashboard/frontend/src/styles.scss new file mode 100644 index 000000000000..c10c1ee98b3c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/styles.scss @@ -0,0 +1,2 @@ +/* You can add global styles to this file, and also import other style files */ +@import './openattic-theme.scss'; diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/chart-tooltip.scss b/src/pybind/mgr/dashboard/frontend/src/styles/chart-tooltip.scss new file mode 100644 index 000000000000..835bb362db43 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/styles/chart-tooltip.scss @@ -0,0 +1,62 @@ +.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; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/test.ts b/src/pybind/mgr/dashboard/frontend/src/test.ts new file mode 100644 index 000000000000..19beece05c8c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/test.ts @@ -0,0 +1,33 @@ +/* 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(); diff --git a/src/pybind/mgr/dashboard/frontend/src/tsconfig.app.json b/src/pybind/mgr/dashboard/frontend/src/tsconfig.app.json new file mode 100644 index 000000000000..39ba8dbacbbe --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/tsconfig.app.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "baseUrl": "./", + "module": "es2015", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ] +} diff --git a/src/pybind/mgr/dashboard/frontend/src/tsconfig.spec.json b/src/pybind/mgr/dashboard/frontend/src/tsconfig.spec.json new file mode 100644 index 000000000000..63d89ff283f6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "baseUrl": "./", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/src/pybind/mgr/dashboard/frontend/src/typings.d.ts b/src/pybind/mgr/dashboard/frontend/src/typings.d.ts new file mode 100644 index 000000000000..ef5c7bd62057 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/typings.d.ts @@ -0,0 +1,5 @@ +/* SystemJS module definition */ +declare var module: NodeModule; +interface NodeModule { + id: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/tsconfig.json b/src/pybind/mgr/dashboard/frontend/tsconfig.json new file mode 100644 index 000000000000..a6c016bf38ad --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "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" + ] + } +} diff --git a/src/pybind/mgr/dashboard/frontend/tslint.json b/src/pybind/mgr/dashboard/frontend/tslint.json new file mode 100644 index 000000000000..d2486f1bbcaa --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/tslint.json @@ -0,0 +1,180 @@ +{ + "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"] + } +} diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py new file mode 100644 index 000000000000..6877bd86b64d --- /dev/null +++ b/src/pybind/mgr/dashboard/module.py @@ -0,0 +1,292 @@ +# -*- 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 "' + .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:
+
    + {lis} +
+ """ + endpoints = ['
  • {}
  • '.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 "' + 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 = """ + + + + Ceph + + + + No active ceph-mgr instance is currently running + the dashboard. A failover may be in progress. + Retrying in {delay} seconds... + + + """ + 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") diff --git a/src/pybind/mgr/dashboard/requirements.txt b/src/pybind/mgr/dashboard/requirements.txt new file mode 100644 index 000000000000..f6191ea38572 --- /dev/null +++ b/src/pybind/mgr/dashboard/requirements.txt @@ -0,0 +1,32 @@ +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 diff --git a/src/pybind/mgr/dashboard/run-backend-api-tests.sh b/src/pybind/mgr/dashboard/run-backend-api-tests.sh new file mode 100755 index 000000000000..8f86401c67a6 --- /dev/null +++ b/src/pybind/mgr/dashboard/run-backend-api-tests.sh @@ -0,0 +1,106 @@ +#!/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 <= 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 diff --git a/src/pybind/mgr/dashboard/settings.py b/src/pybind/mgr/dashboard/settings.py new file mode 100644 index 000000000000..4f68fbb46ffd --- /dev/null +++ b/src/pybind/mgr/dashboard/settings.py @@ -0,0 +1,102 @@ +# -*- 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']), '' diff --git a/src/pybind/mgr/dashboard/tests/__init__.py b/src/pybind/mgr/dashboard/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/tests/helper.py b/src/pybind/mgr/dashboard/tests/helper.py new file mode 100644 index 000000000000..effe21d76327 --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/helper.py @@ -0,0 +1,54 @@ +# -*- 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) diff --git a/src/pybind/mgr/dashboard/tests/test_notification.py b/src/pybind/mgr/dashboard/tests/test_notification.py new file mode 100644 index 000000000000..bca27f9e6f91 --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/test_notification.py @@ -0,0 +1,94 @@ +# -*- 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) diff --git a/src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py b/src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py new file mode 100644 index 000000000000..c283c947d84f --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py @@ -0,0 +1,88 @@ +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}) diff --git a/src/pybind/mgr/dashboard/tests/test_settings.py b/src/pybind/mgr/dashboard/tests/test_settings.py new file mode 100644 index 000000000000..92fcf7ff349e --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/test_settings.py @@ -0,0 +1,89 @@ +# -*- 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'") diff --git a/src/pybind/mgr/dashboard/tests/test_tcmu_iscsi.py b/src/pybind/mgr/dashboard/tests/test_tcmu_iscsi.py new file mode 100644 index 000000000000..88077cbb5bc3 --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/test_tcmu_iscsi.py @@ -0,0 +1,71 @@ +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} + }] + }) diff --git a/src/pybind/mgr/dashboard/tests/test_tools.py b/src/pybind/mgr/dashboard/tests/test_tools.py new file mode 100644 index 000000000000..ca4d9040c3c0 --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/test_tools.py @@ -0,0 +1,103 @@ +# -*- 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) diff --git a/src/pybind/mgr/dashboard/tools.py b/src/pybind/mgr/dashboard/tools.py new file mode 100644 index 000000000000..8775a708730a --- /dev/null +++ b/src/pybind/mgr/dashboard/tools.py @@ -0,0 +1,531 @@ +# -*- 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") diff --git a/src/pybind/mgr/dashboard/tox.ini b/src/pybind/mgr/dashboard/tox.ini new file mode 100644 index 000000000000..743a8a6929e5 --- /dev/null +++ b/src/pybind/mgr/dashboard/tox.ini @@ -0,0 +1,40 @@ +[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 . diff --git a/src/pybind/mgr/dashboard_v2/.coveragerc b/src/pybind/mgr/dashboard_v2/.coveragerc deleted file mode 100644 index 29a63192c3f8..000000000000 --- a/src/pybind/mgr/dashboard_v2/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -omit = tests/* - */python*/* - ceph_module_mock.py - __init__.py - */mgr_module.py - diff --git a/src/pybind/mgr/dashboard_v2/.editorconfig b/src/pybind/mgr/dashboard_v2/.editorconfig deleted file mode 100644 index a831e3da1860..000000000000 --- a/src/pybind/mgr/dashboard_v2/.editorconfig +++ /dev/null @@ -1,29 +0,0 @@ -# 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 diff --git a/src/pybind/mgr/dashboard_v2/.gitignore b/src/pybind/mgr/dashboard_v2/.gitignore deleted file mode 100644 index b6369487a3ff..000000000000 --- a/src/pybind/mgr/dashboard_v2/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -.coverage* -htmlcov -.tox -coverage.xml -junit*xml -__pycache__ -.cache -ceph.conf -wheelhouse* - -# IDE -.vscode -.idea -*.egg - -# virtualenv -venv diff --git a/src/pybind/mgr/dashboard_v2/.pylintrc b/src/pybind/mgr/dashboard_v2/.pylintrc deleted file mode 100644 index ab5d1f8a7777..000000000000 --- a/src/pybind/mgr/dashboard_v2/.pylintrc +++ /dev/null @@ -1,548 +0,0 @@ -[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*(# )??$ - -# 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 diff --git a/src/pybind/mgr/dashboard_v2/CMakeLists.txt b/src/pybind/mgr/dashboard_v2/CMakeLists.txt deleted file mode 100644 index 15e2f27ca290..000000000000 --- a/src/pybind/mgr/dashboard_v2/CMakeLists.txt +++ /dev/null @@ -1,68 +0,0 @@ -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") diff --git a/src/pybind/mgr/dashboard_v2/HACKING.rst b/src/pybind/mgr/dashboard_v2/HACKING.rst deleted file mode 100644 index 610e117ba12b..000000000000 --- a/src/pybind/mgr/dashboard_v2/HACKING.rst +++ /dev/null @@ -1,510 +0,0 @@ -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 `_ and requires the -`Node Package Manager `_ ``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 `_ - 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 -`_. - -Running End-to-End Tests -~~~~~~~~~~~~~~~~~~~~~~~~ - -Run ``npm run e2e`` to execute the end-to-end tests via -`Protractor `__. - -Further Help -~~~~~~~~~~~~ - -To get more help on the Angular CLI use ``ng help`` or go check out the -`Angular CLI -README `__. - -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 -`_ 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 -`_, 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 = '' - for l in self.log_buffer: - ret += "{}
    ".format(l) - ret += "" - 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 - -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. - diff --git a/src/pybind/mgr/dashboard_v2/README.rst b/src/pybind/mgr/dashboard_v2/README.rst deleted file mode 100644 index 6ee5984892ae..000000000000 --- a/src/pybind/mgr/dashboard_v2/README.rst +++ /dev/null @@ -1,77 +0,0 @@ -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 `_ -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 `_ (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 `_. - -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 `_ 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 - -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. diff --git a/src/pybind/mgr/dashboard_v2/__init__.py b/src/pybind/mgr/dashboard_v2/__init__.py deleted file mode 100644 index f09ef245276b..000000000000 --- a/src/pybind/mgr/dashboard_v2/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- 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() diff --git a/src/pybind/mgr/dashboard_v2/controllers/__init__.py b/src/pybind/mgr/dashboard_v2/controllers/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/controllers/auth.py b/src/pybind/mgr/dashboard_v2/controllers/auth.py deleted file mode 100644 index 28a2f28f7d36..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/auth.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- 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 | - | | 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 ` 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) diff --git a/src/pybind/mgr/dashboard_v2/controllers/cephfs.py b/src/pybind/mgr/dashboard_v2/controllers/cephfs.py deleted file mode 100644 index c4786cebaf92..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/cephfs.py +++ /dev/null @@ -1,318 +0,0 @@ -# -*- 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) diff --git a/src/pybind/mgr/dashboard_v2/controllers/cluster_configuration.py b/src/pybind/mgr/dashboard_v2/controllers/cluster_configuration.py deleted file mode 100644 index d02027b1c656..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/cluster_configuration.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- 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) diff --git a/src/pybind/mgr/dashboard_v2/controllers/dashboard.py b/src/pybind/mgr/dashboard_v2/controllers/dashboard.py deleted file mode 100644 index 3457c2f14db5..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/dashboard.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- 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 diff --git a/src/pybind/mgr/dashboard_v2/controllers/host.py b/src/pybind/mgr/dashboard_v2/controllers/host.py deleted file mode 100644 index 8bef07185ae9..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/host.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- 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() diff --git a/src/pybind/mgr/dashboard_v2/controllers/monitor.py b/src/pybind/mgr/dashboard_v2/controllers/monitor.py deleted file mode 100644 index ac3bfe478b63..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/monitor.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- 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 - } diff --git a/src/pybind/mgr/dashboard_v2/controllers/osd.py b/src/pybind/mgr/dashboard_v2/controllers/osd.py deleted file mode 100644 index 24fca6d9828d..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/osd.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- 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, - } diff --git a/src/pybind/mgr/dashboard_v2/controllers/perf_counters.py b/src/pybind/mgr/dashboard_v2/controllers/perf_counters.py deleted file mode 100644 index 59692d3309a1..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/perf_counters.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- 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 diff --git a/src/pybind/mgr/dashboard_v2/controllers/pool.py b/src/pybind/mgr/dashboard_v2/controllers/pool.py deleted file mode 100644 index 2eac9f50f881..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/pool.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- 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] diff --git a/src/pybind/mgr/dashboard_v2/controllers/rbd.py b/src/pybind/mgr/dashboard_v2/controllers/rbd.py deleted file mode 100644 index b73697b0a160..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/rbd.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- 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} diff --git a/src/pybind/mgr/dashboard_v2/controllers/rbd_mirroring.py b/src/pybind/mgr/dashboard_v2/controllers/rbd_mirroring.py deleted file mode 100644 index 62164ffa4799..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/rbd_mirroring.py +++ /dev/null @@ -1,305 +0,0 @@ -# -*- 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 - } diff --git a/src/pybind/mgr/dashboard_v2/controllers/rgw.py b/src/pybind/mgr/dashboard_v2/controllers/rgw.py deleted file mode 100644 index 4f8e169e917e..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/rgw.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- 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 diff --git a/src/pybind/mgr/dashboard_v2/controllers/summary.py b/src/pybind/mgr/dashboard_v2/controllers/summary.py deleted file mode 100644 index 93631bbaec1d..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/summary.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- 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() - } diff --git a/src/pybind/mgr/dashboard_v2/controllers/tcmu_iscsi.py b/src/pybind/mgr/dashboard_v2/controllers/tcmu_iscsi.py deleted file mode 100644 index f4849b7daa54..000000000000 --- a/src/pybind/mgr/dashboard_v2/controllers/tcmu_iscsi.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- 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']), - } diff --git a/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json b/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json deleted file mode 100644 index 6ffd7b7d6e8f..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "$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": {} - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/.editorconfig b/src/pybind/mgr/dashboard_v2/frontend/.editorconfig deleted file mode 100644 index 6e87a003da89..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# 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 diff --git a/src/pybind/mgr/dashboard_v2/frontend/.gitignore b/src/pybind/mgr/dashboard_v2/frontend/.gitignore deleted file mode 100644 index 2e55dc6354dd..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/.gitignore +++ /dev/null @@ -1,50 +0,0 @@ -# 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 diff --git a/src/pybind/mgr/dashboard_v2/frontend/e2e/app.e2e-spec.ts b/src/pybind/mgr/dashboard_v2/frontend/e2e/app.e2e-spec.ts deleted file mode 100644 index 3e9837024233..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/e2e/app.e2e-spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -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!'); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/e2e/app.po.ts b/src/pybind/mgr/dashboard_v2/frontend/e2e/app.po.ts deleted file mode 100644 index d9761bb4a4e4..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/e2e/app.po.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { browser, by, element } from 'protractor'; - -export class AppPage { - navigateTo() { - return browser.get('/'); - } - - getParagraphText() { - return element(by.css('oa-root h1')).getText(); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/e2e/tsconfig.e2e.json b/src/pybind/mgr/dashboard_v2/frontend/e2e/tsconfig.e2e.json deleted file mode 100644 index 1d9e5edf0965..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/e2e/tsconfig.e2e.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/e2e", - "baseUrl": "./", - "module": "commonjs", - "target": "es5", - "types": [ - "jasmine", - "jasminewd2", - "node" - ] - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/karma.conf.js b/src/pybind/mgr/dashboard_v2/frontend/karma.conf.js deleted file mode 100644 index 95969e05ce41..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/karma.conf.js +++ /dev/null @@ -1,40 +0,0 @@ -// 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 - }); -}; diff --git a/src/pybind/mgr/dashboard_v2/frontend/package.json b/src/pybind/mgr/dashboard_v2/frontend/package.json deleted file mode 100644 index e173870909fb..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/package.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "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" - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/protractor.conf.js b/src/pybind/mgr/dashboard_v2/frontend/protractor.conf.js deleted file mode 100644 index 7ee3b5ee863a..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/protractor.conf.js +++ /dev/null @@ -1,28 +0,0 @@ -// 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 } })); - } -}; diff --git a/src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample b/src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample deleted file mode 100644 index e654419c9cfd..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample +++ /dev/null @@ -1,7 +0,0 @@ -{ - "/api/": { - "target": "http://localhost:8080", - "secure": false, - "logLevel": "debug" - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts deleted file mode 100644 index 8883796d367d..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts +++ /dev/null @@ -1,54 +0,0 @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.html deleted file mode 100644 index 638edaa604f1..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.html +++ /dev/null @@ -1,5 +0,0 @@ - -
    - -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts deleted file mode 100644 index 3cca10d09170..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -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(); - }) - ); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.ts deleted file mode 100644 index c9e0e7e8773b..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.ts +++ /dev/null @@ -1,27 +0,0 @@ -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(); - } - -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts deleted file mode 100644 index 525e94729fad..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts +++ /dev/null @@ -1,57 +0,0 @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/block.module.ts deleted file mode 100644 index 6e094fa04d6a..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/block.module.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.html deleted file mode 100644 index 68f9326690cd..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.html +++ /dev/null @@ -1,20 +0,0 @@ - - -Daemons - - - -Images - - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts deleted file mode 100644 index 78c19b072475..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.ts deleted file mode 100644 index 9d700f44ed20..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/iscsi/iscsi.component.ts +++ /dev/null @@ -1,102 +0,0 @@ -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; - }); - } - -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts deleted file mode 100644 index f22bcf2a599b..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MirrorHealthColorPipe } from './mirror-health-color.pipe'; - -describe('MirrorHealthColorPipe', () => { - it('create an instance', () => { - const pipe = new MirrorHealthColorPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.ts deleted file mode 100644 index 43d880ffb1aa..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirror-health-color.pipe.ts +++ /dev/null @@ -1,17 +0,0 @@ -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'; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.html deleted file mode 100644 index a76047d431f6..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.html +++ /dev/null @@ -1,94 +0,0 @@ - - - - -
    -
    -
    - Daemons - - - -
    -
    - -
    -
    - Pools - - - -
    -
    -
    - -
    -
    -
    - Images - - - - - - - - - - - - - - -
    -
    -
    - - - {{ value }} - - - - {{ value }} - - - - Syncing - - - - - - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts deleted file mode 100644 index f20d0484c018..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.ts deleted file mode 100644 index 63e960ef52e4..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/mirroring/mirroring.component.ts +++ /dev/null @@ -1,137 +0,0 @@ -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; - @ViewChild('stateTmpl') stateTmpl: TemplateRef; - @ViewChild('syncTmpl') syncTmpl: TemplateRef; - @ViewChild('progressTmpl') progressTmpl: TemplateRef; - - 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; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html deleted file mode 100644 index 1bdd5a22fbe7..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts deleted file mode 100644 index aea790cf1da7..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts deleted file mode 100644 index 98ac59c66640..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts +++ /dev/null @@ -1,92 +0,0 @@ -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; - } - ); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts deleted file mode 100644 index 0f74b8234a6e..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html deleted file mode 100644 index b98d70838a00..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.html +++ /dev/null @@ -1,12 +0,0 @@ -
    - - -
    -
    -
    -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss deleted file mode 100644 index 62a023b9aaf4..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import '../../../../styles/chart-tooltip.scss'; - -.chart-container { - height: 500px; - width: 100%; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts deleted file mode 100644 index 6d552041f1ca..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts deleted file mode 100644 index cca1ae2feb92..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs-chart/cephfs-chart.component.ts +++ /dev/null @@ -1,164 +0,0 @@ -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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.module.ts deleted file mode 100644 index c47051c18e6e..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 {} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts deleted file mode 100644 index a9e59a016dd5..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -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(); - }) - ); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.ts deleted file mode 100644 index a5c4994da451..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -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}`); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html deleted file mode 100644 index ef62292d8ad3..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - -
    -
    -
    - Ranks - - - -
    - - - -
    - -
    -
    - Pools - - - - -
    -
    -
    - -
    -
    - -
    -
    - - - - - - - - - {{ row.state === 'standby-replay' ? 'Evts' : 'Reqs' }}: {{ value | dimless }} /s - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss deleted file mode 100644 index d82829af85c4..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.progress { - margin-bottom: 0px; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts deleted file mode 100644 index 3df655defa6d..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts deleted file mode 100644 index d8fe382fec6f..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/cephfs/cephfs.component.ts +++ /dev/null @@ -1,126 +0,0 @@ -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; - @ViewChild('activityTmpl') activityTmpl: TemplateRef; - - 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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.html deleted file mode 100644 index 7832a38744f0..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.html +++ /dev/null @@ -1,22 +0,0 @@ - - -
    - - - - -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts deleted file mode 100644 index d3506a906861..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.ts deleted file mode 100644 index fc2cbdec61b2..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cephfs/clients/clients.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -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; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts deleted file mode 100644 index d661f51baed4..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts +++ /dev/null @@ -1,48 +0,0 @@ -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 {} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.html deleted file mode 100644 index efe071a02278..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.html +++ /dev/null @@ -1,67 +0,0 @@ - - -
    -
    - -
    - - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameDescriptionTypeLevelDefaultTagsServicesSee_alsoMaxMin
    {{ row.name }} -

    - {{ row.desc }}

    -

    {{ row.long_desc }}

    -
    {{ row.type }}{{ row.level }} - {{ row.default }} {{ row.daemon_default }} - -

    {{ item }}

    -
    -

    {{ item }}

    -
    -

    {{ item }}

    -
    {{ row.max }}{{ row.min }}
    -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.scss deleted file mode 100644 index e968d6d90861..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import '../../../shared/datatable/table/table.component.scss'; - -td.wrap { - word-break: break-all; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts deleted file mode 100644 index 0d98766efd46..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.ts deleted file mode 100644 index 7c6ed68b448c..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/configuration/configuration.component.ts +++ /dev/null @@ -1,79 +0,0 @@ -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; - - 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]; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.html deleted file mode 100644 index f2935c3a0a15..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - {{ service.type }}.{{ service.id }}{{ !isLast ? ", " : "" }} - - - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts deleted file mode 100644 index 90eb5e6498ba..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.ts deleted file mode 100644 index 28a193fd8ce8..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/hosts/hosts.component.ts +++ /dev/null @@ -1,64 +0,0 @@ -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 = []; - hosts: Array = []; - isLoadingHosts = false; - - @ViewChild('servicesTpl') public servicesTpl: TemplateRef; - - 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; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.spec.ts deleted file mode 100644 index 1d5f7de97b77..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -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(); - })); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.ts deleted file mode 100644 index 32057f3b6ea1..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -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'); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.html deleted file mode 100644 index d59de84c1451..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.html +++ /dev/null @@ -1,72 +0,0 @@ - - -
    -
    -
    - Status - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Cluster ID{{ mon_status.monmap.fsid }}
    monmap modified{{ mon_status.monmap.modified }}
    monmap epoch{{ mon_status.monmap.epoch }}
    quorum con{{ mon_status.features.quorum_con }}
    quorum mon{{ mon_status.features.quorum_mon }}
    required con{{ mon_status.features.required_con }}
    required mon{{ mon_status.features.required_mon }}
    -
    -
    - -
    -
    - In Quorum - - - - Not In Quorum - - -
    -
    -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts deleted file mode 100644 index 906581e76b98..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -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; - - beforeEach( - async(() => { - TestBed.configureTestingModule({ - imports: [AppModule] - }).compileComponents(); - }) - ); - - beforeEach(() => { - fixture = TestBed.createComponent(MonitorComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.ts deleted file mode 100644 index 0a23129667ef..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.ts +++ /dev/null @@ -1,66 +0,0 @@ -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; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html deleted file mode 100644 index c511d54e78c1..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - -

    - Histogram not available -> {{ osd.histogram_failed }} -

    -
    -
    -

    Writes

    - - -
    -
    -

    Reads

    - - -
    -
    -
    -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts deleted file mode 100644 index c24618286de1..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts deleted file mode 100644 index 7f2af3724976..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -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; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html deleted file mode 100644 index 2683102f0188..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - {{ state }}, - - - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts deleted file mode 100644 index 506b536a6364..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts deleted file mode 100644 index 29f0f22ede28..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts +++ /dev/null @@ -1,75 +0,0 @@ -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; - - 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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html deleted file mode 100644 index 080f121f3bf5..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - -
    -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts deleted file mode 100644 index 7ff7d646a286..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { OsdPerformanceHistogramComponent } from './osd-performance-histogram.component'; - -describe('OsdPerformanceHistogramComponent', () => { - let component: OsdPerformanceHistogramComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ OsdPerformanceHistogramComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(OsdPerformanceHistogramComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts deleted file mode 100644 index c3f06450659f..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts deleted file mode 100644 index 115d6a4dbd96..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -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(); - })); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.ts deleted file mode 100644 index cf9adf1b5fd7..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.ts +++ /dev/null @@ -1,17 +0,0 @@ -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}`); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts deleted file mode 100644 index cf4c025060fc..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.module.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 {} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts deleted file mode 100644 index bf061e9f6452..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -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(); - }) - ); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.ts deleted file mode 100644 index cb51cb4d71b0..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -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'); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html deleted file mode 100644 index 89a37fd6c0eb..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.html +++ /dev/null @@ -1,12 +0,0 @@ -
    - - - - - - - - -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss deleted file mode 100644 index 04eee2d6f256..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -div { - padding-top: 20px; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts deleted file mode 100644 index 80500c0b9ae3..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DashboardComponent } from './dashboard.component'; - -describe('DashboardComponent', () => { - let component: DashboardComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ DashboardComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DashboardComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - // it('should create', () => { - // expect(component).toBeTruthy(); - // }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts deleted file mode 100644 index fc676c74c6d3..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/dashboard/dashboard.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -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() { - } - -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html deleted file mode 100644 index 7135f96f67bb..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.html +++ /dev/null @@ -1,15 +0,0 @@ -
    - -
    -
    -
    -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss deleted file mode 100644 index b3abf8681a29..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.scss +++ /dev/null @@ -1 +0,0 @@ -@import '../../../../styles/chart-tooltip.scss'; diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts deleted file mode 100644 index dca539f041c2..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts deleted file mode 100644 index 196d871066ac..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts +++ /dev/null @@ -1,117 +0,0 @@ -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]); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html deleted file mode 100644 index 348324e4dc13..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.html +++ /dev/null @@ -1,209 +0,0 @@ -
    -
    - -
    -
    -
    - Health - Overall status: - {{ contentData.health.status }} -
      -
    • - {{ check.type }}: {{ check.summary.message }} -
    • -
    -
    -
    -
    - -
    - -
    -
    -
    -
    -
    - -
    -
    - - Monitors - - {{ contentData.mon_status | monSummary }} -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - - OSDs - - {{ contentData.osd_map | osdSummary }} -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - Metadata servers - {{ contentData.fs_map | mdsSummary }} -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - Manager daemons - {{ contentData.mgr_map | mgrSummary }} -
    -
    -
    -
    -
    -
    -
    - -
    - -
    -
    -
    - Usage - - - - - - - - - - - - -
    - {{ contentData.df.stats.total_objects | dimless }} - -
    - -
    -
    -
    - -
    -
    Objects - Raw capacity -
    - ({{ contentData.df.stats.total_used_bytes | dimlessBinary }} used) -
    Usage by pool
    -
    -
    -
    - -
    -
    -
    - Pools - - - - - - - - - - - - - - - - - - - - - -
    NamePG statusUsageReadWrite
    {{ pool.pool_name }} - {{ pool.pg_status | pgStatus }} - - {{ pool.stats.bytes_used.latest | dimlessBinary }} / {{ pool.stats.max_avail.latest | dimlessBinary }} - - {{ pool.stats.rd_bytes.rate | dimless }} - - {{ pool.stats.rd.rate | dimless }} ops - - {{ pool.stats.wr_bytes.rate | dimless }} - - {{ pool.stats.wr.rate | dimless }} ops -
    -
    -
    -
    -
    - -
    -
    - -
    -
    - Logs - - - - - {{ line.stamp }} {{ line.priority }}  - - {{ line.message }} -
    -
    -
    -
    - - - {{ line.stamp }} {{ line.priority }}  - - - {{ line.message }} - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.scss deleted file mode 100644 index 919b41d10fc6..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.scss +++ /dev/null @@ -1,62 +0,0 @@ -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; - } - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts deleted file mode 100644 index 983b1452e89d..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts deleted file mode 100644 index 3cdddc970e3c..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/health/health.component.ts +++ /dev/null @@ -1,99 +0,0 @@ -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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts deleted file mode 100644 index 43af68d08af2..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { LogColorPipe } from './log-color.pipe'; - -describe('LogColorPipe', () => { - it('create an instance', () => { - const pipe = new LogColorPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.ts deleted file mode 100644 index eb60ddba4c33..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/log-color.pipe.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 ''; - } - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts deleted file mode 100644 index 37883a82a987..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MdsSummaryPipe } from './mds-summary.pipe'; - -describe('MdsSummaryPipe', () => { - it('create an instance', () => { - const pipe = new MdsSummaryPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts deleted file mode 100644 index 9e6eeca6e8ac..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mds-summary.pipe.ts +++ /dev/null @@ -1,38 +0,0 @@ -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'; - } - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts deleted file mode 100644 index fdab76c4808d..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MgrSummaryPipe } from './mgr-summary.pipe'; - -describe('MgrSummaryPipe', () => { - it('create an instance', () => { - const pipe = new MgrSummaryPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts deleted file mode 100644 index cf793e66e475..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mgr-summary.pipe.ts +++ /dev/null @@ -1,22 +0,0 @@ -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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts deleted file mode 100644 index 49526cf3fcd7..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MonSummaryPipe } from './mon-summary.pipe'; - -describe('MonSummaryPipe', () => { - it('create an instance', () => { - const pipe = new MonSummaryPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts deleted file mode 100644 index 6877e2247c7e..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/mon-summary.pipe.ts +++ /dev/null @@ -1,18 +0,0 @@ -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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts deleted file mode 100644 index 466eec1ac30e..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { OsdSummaryPipe } from './osd-summary.pipe'; - -describe('OsdSummaryPipe', () => { - it('create an instance', () => { - const pipe = new OsdSummaryPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts deleted file mode 100644 index b02d97644bb8..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/osd-summary.pipe.ts +++ /dev/null @@ -1,26 +0,0 @@ -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)'; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts deleted file mode 100644 index 67c5f10c5e35..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PgStatusStylePipe } from './pg-status-style.pipe'; - -describe('PgStatusStylePipe', () => { - it('create an instance', () => { - const pipe = new PgStatusStylePipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts deleted file mode 100644 index 4e9afab97cfa..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status-style.pipe.ts +++ /dev/null @@ -1,40 +0,0 @@ -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' }; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts deleted file mode 100644 index d7d5592b653f..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PgStatusPipe } from './pg-status.pipe'; - -describe('PgStatusPipe', () => { - it('create an instance', () => { - const pipe = new PgStatusPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.ts deleted file mode 100644 index 5c6c7b393c30..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/dashboard/pg-status.pipe.ts +++ /dev/null @@ -1,16 +0,0 @@ -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(', '); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter.module.ts deleted file mode 100644 index f2c20299f4c4..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter.module.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html deleted file mode 100644 index ebb9ba93d62f..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html +++ /dev/null @@ -1,7 +0,0 @@ -
    - Performance Counters -

    {{ serviceType }}.{{ serviceId }}

    - - -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts deleted file mode 100644 index a4cc71702317..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts deleted file mode 100644 index 25fa82e4e9d1..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -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(); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts deleted file mode 100644 index 6f0af94e4b60..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -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(); - }) - ); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts deleted file mode 100644 index b6ac5d5fe3e6..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/services/table-performance-counter.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 => { - return resp['counters']; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html deleted file mode 100644 index 6564dc1ab134..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.html +++ /dev/null @@ -1,8 +0,0 @@ - - - {{ row.value | dimless }} {{ row.unit }} - - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts deleted file mode 100644 index 4baefe8911da..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts deleted file mode 100644 index 6ac05c9849a6..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/performance-counter/table-performance-counter/table-performance-counter.component.ts +++ /dev/null @@ -1,59 +0,0 @@ -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 = []; - counters: Array = []; - - @ViewChild('valueTpl') public valueTpl: TemplateRef; - - /** - * 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; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html deleted file mode 100644 index 81c5919db697..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts deleted file mode 100644 index afce56a2bacf..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts deleted file mode 100644 index 8ac62fac84c8..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-details/rgw-daemon-details.component.ts +++ /dev/null @@ -1,36 +0,0 @@ -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']; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html deleted file mode 100644 index 64b703fd98e4..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts deleted file mode 100644 index c0d331e3599f..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts deleted file mode 100644 index ce1c2456fdf0..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts +++ /dev/null @@ -1,51 +0,0 @@ -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 = []; - daemons: Array = []; - 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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw.module.ts deleted file mode 100644 index a888940c27da..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/rgw.module.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts deleted file mode 100644 index 691cc787a9b8..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -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(); - }) - ); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts deleted file mode 100644 index 907537ef2d9e..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/rgw/services/rgw-daemon.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -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; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts deleted file mode 100644 index e96b1b30b8f8..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html deleted file mode 100644 index e0b33c893d68..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html +++ /dev/null @@ -1,73 +0,0 @@ - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss deleted file mode 100644 index 1f77356d2967..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss +++ /dev/null @@ -1,31 +0,0 @@ -@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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts deleted file mode 100644 index b8307b594e81..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts deleted file mode 100644 index f8f46254976c..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -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(['']); - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html deleted file mode 100644 index 993fd95ebb1a..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html +++ /dev/null @@ -1,6 +0,0 @@ - - - Logout - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts deleted file mode 100644 index 318ca820af2d..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts deleted file mode 100644 index 4bf11e368712..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -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']); - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core.module.ts deleted file mode 100644 index bd1768158e7e..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/core.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation.module.ts deleted file mode 100644 index 823d4feea184..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -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 {} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html deleted file mode 100644 index cd7e8040c81b..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html +++ /dev/null @@ -1,228 +0,0 @@ - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts deleted file mode 100644 index 7548b2bd7c1f..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.ts deleted file mode 100644 index ee61c41134fa..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 = []; - - 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' }; - } - } - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.html deleted file mode 100644 index 0f3847b93934..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.html +++ /dev/null @@ -1,14 +0,0 @@ -
    -
    -

    Sorry, we could not find what you were looking for

    - - - - "Mimic Octopus" by prilfish is licensed under - CC BY 2.0 - -
    -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.scss deleted file mode 100644 index e94d9f2fb82c..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.scss +++ /dev/null @@ -1,15 +0,0 @@ -h1 { - font-size: -webkit-xxx-large; -} - -h2 { - font-size: xx-large; -} - -*{ - font-family: monospace; -} - -img{ - width: 50vw; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.spec.ts deleted file mode 100644 index 35189ed0b766..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { NotFoundComponent } from './not-found.component'; - -describe('NotFoundComponent', () => { - let component: NotFoundComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ NotFoundComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(NotFoundComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.ts deleted file mode 100644 index d12bc32100de..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/not-found/not-found.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'cd-not-found', - templateUrl: './not-found.component.html', - styleUrls: ['./not-found.component.scss'] -}) -export class NotFoundComponent { - constructor() {} -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/components.module.ts deleted file mode 100644 index fe65bea599bb..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/components.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.html deleted file mode 100644 index 4b7a1b872239..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.html +++ /dev/null @@ -1,13 +0,0 @@ -
    - - -
    -
    -
    -
    diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.scss deleted file mode 100644 index ec7d98291e8c..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import '../../../../styles/chart-tooltip.scss'; - -.chart-container { - position: static !important; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts deleted file mode 100644 index 4a879c3fcbd1..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -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; - - beforeEach( - async(() => { - TestBed.configureTestingModule({ - imports: [AppModule] - }).compileComponents(); - }) - ); - - beforeEach(() => { - fixture = TestBed.createComponent(SparklineComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.ts deleted file mode 100644 index fa20ce301864..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/sparkline/sparkline.component.ts +++ /dev/null @@ -1,108 +0,0 @@ -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 = [ - { - 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 = [ - { - data: [] - } - ]; - - public labels: Array = []; - - 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)]; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.html deleted file mode 100644 index 1d71da24c1ab..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.html +++ /dev/null @@ -1,17 +0,0 @@ - - Retrieving data, please wait. - - - - Displaying previously cached data. - - - - Could not load data. Please check the cluster health. - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts deleted file mode 100644 index da68def0b62d..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -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; - - 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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.ts deleted file mode 100644 index 63bc97947c2f..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/components/view-cache/view-cache.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -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() {} -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/datatable.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/datatable.module.ts deleted file mode 100644 index b09a31ebaa7c..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/datatable.module.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html deleted file mode 100644 index d0ab74c07863..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.html +++ /dev/null @@ -1,9 +0,0 @@ - - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts deleted file mode 100644 index 16e05bd1818c..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -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; - - 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'); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts deleted file mode 100644 index 101580f35ef1..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table-key-value/table-key-value.component.ts +++ /dev/null @@ -1,100 +0,0 @@ -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 = []; - - @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(); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.html deleted file mode 100644 index ba6adf5ae7e7..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.html +++ /dev/null @@ -1,120 +0,0 @@ -
    -
    - -
    - -
    - - - -
    - - - - - - - -
    - - - -
    - -
    - - - -
    - -
    - - - -
    - - - -
    - -
    - - -
    - - - - - - - {{ value }} - - - - - - - - {{ value }} - - - - {{ value }} /s - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.scss deleted file mode 100644 index db9c4eb97772..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.scss +++ /dev/null @@ -1,245 +0,0 @@ -@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%; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.spec.ts deleted file mode 100644 index 60ec7d013279..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -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; - 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); - }); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.ts deleted file mode 100644 index 9f04e91c71ca..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.ts +++ /dev/null @@ -1,282 +0,0 @@ -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; - @ViewChild('sparklineTpl') sparklineTpl: TemplateRef; - @ViewChild('routerLinkTpl') routerLinkTpl: TemplateRef; - @ViewChild('perSecondTpl') perSecondTpl: TemplateRef; - - // 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 - } = {}; - 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) - }; - }; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/directives/password-button.directive.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/directives/password-button.directive.spec.ts deleted file mode 100644 index 1fc8f9c7cbb9..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/directives/password-button.directive.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PasswordButtonDirective } from './password-button.directive'; - -describe('PasswordButtonDirective', () => { - it('should create an instance', () => { - const directive = new PasswordButtonDirective(null, null); - expect(directive).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/directives/password-button.directive.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/directives/password-button.directive.ts deleted file mode 100644 index b375ba256b0a..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/directives/password-button.directive.ts +++ /dev/null @@ -1,40 +0,0 @@ -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(); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/cell-template.enum.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/cell-template.enum.ts deleted file mode 100644 index 7c1c2162f58d..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/cell-template.enum.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum CellTemplate { - bold = 'bold', - sparkline = 'sparkline', - perSecond = 'perSecond', - routerLink = 'routerLink' -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/view-cache-status.enum.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/view-cache-status.enum.ts deleted file mode 100644 index 169059c4405a..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/view-cache-status.enum.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum ViewCacheStatus { - ValueOk = 0, - ValueStale = 1, - ValueNone = 2, - ValueException = 3 -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-column.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-column.ts deleted file mode 100644 index bf45c4818748..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-column.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TableColumn } from '@swimlane/ngx-datatable'; -import { CellTemplate } from '../enum/cell-template.enum'; - -export interface CdTableColumn extends TableColumn { - cellTransformation?: CellTemplate; - isHidden?: boolean; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-selection.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-selection.ts deleted file mode 100644 index 9732abc5a032..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/cd-table-selection.ts +++ /dev/null @@ -1,28 +0,0 @@ -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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/chart-tooltip.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/chart-tooltip.ts deleted file mode 100644 index 56962f3da69f..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/chart-tooltip.ts +++ /dev/null @@ -1,117 +0,0 @@ -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 = ''; - - titleLines.forEach(title => { - innerHtml += '' + this.getTitle(title) + ''; - }); - innerHtml += ''; - - 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 = ''; - innerHtml += '' + span + this.getBody(body) + ''; - }); - innerHtml += ''; - - 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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.ts deleted file mode 100644 index b33c366c0376..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class Credentials { - username: string; - password: string; - stay_signed_in = false; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts deleted file mode 100644 index bfe10c2f4697..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CephShortVersionPipe } from './ceph-short-version.pipe'; - -describe('CephShortVersionPipe', () => { - it('create an instance', () => { - const pipe = new CephShortVersionPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts deleted file mode 100644 index 9599112c811c..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/ceph-short-version.pipe.ts +++ /dev/null @@ -1,18 +0,0 @@ -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; - } - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts deleted file mode 100644 index 2424ebc16bb8..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.ts deleted file mode 100644 index 92f000824aa7..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless-binary.pipe.ts +++ /dev/null @@ -1,20 +0,0 @@ -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' - ]); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.spec.ts deleted file mode 100644 index 4bbfdd8564c9..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -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(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.ts deleted file mode 100644 index 5e02846e3520..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/dimless.pipe.ts +++ /dev/null @@ -1,20 +0,0 @@ -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' - ]); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.spec.ts deleted file mode 100644 index 1427de361bd6..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { FilterPipe } from './filter.pipe'; - -describe('FilterPipe', () => { - it('create an instance', () => { - const pipe = new FilterPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.ts deleted file mode 100644 index 21115a7b6d01..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/filter.pipe.ts +++ /dev/null @@ -1,25 +0,0 @@ -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; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.spec.ts deleted file mode 100644 index e0e44e0eb85d..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { HealthColorPipe } from './health-color.pipe'; - -describe('HealthColorPipe', () => { - it('create an instance', () => { - const pipe = new HealthColorPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.ts deleted file mode 100644 index 9d82475a14f6..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/health-color.pipe.ts +++ /dev/null @@ -1,18 +0,0 @@ -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; - } - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.spec.ts deleted file mode 100644 index 768f12a24e52..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ListPipe } from './list.pipe'; - -describe('ListPipe', () => { - it('create an instance', () => { - const pipe = new ListPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.ts deleted file mode 100644 index 1e379190e04e..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/list.pipe.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ - name: 'list' -}) -export class ListPipe implements PipeTransform { - transform(value: any, args?: any): any { - return value.join(', '); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/pipes.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/pipes.module.ts deleted file mode 100644 index 51dc736c8a69..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/pipes.module.ts +++ /dev/null @@ -1,40 +0,0 @@ -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 {} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts deleted file mode 100644 index 1295b0dc25b6..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RelativeDatePipe } from './relative-date.pipe'; - -describe('RelativeDatePipe', () => { - it('create an instance', () => { - const pipe = new RelativeDatePipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.ts deleted file mode 100644 index 6bfa3958c628..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/pipes/relative-date.pipe.ts +++ /dev/null @@ -1,17 +0,0 @@ -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(); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts deleted file mode 100644 index a3ec80387032..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -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; - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts deleted file mode 100644 index f09250d6c172..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts +++ /dev/null @@ -1,42 +0,0 @@ -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, next: HttpHandler): Observable> { - return next.handle(request).do((event: HttpEvent) => { - 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']); - } - } - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts deleted file mode 100644 index cd6dbbe7a0b2..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -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; - } - -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts deleted file mode 100644 index 88a7136289e3..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -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(); - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.spec.ts deleted file mode 100644 index dcb5a9e10b9f..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -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(); - }) - ); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.ts deleted file mode 100644 index 41ac7bb5196a..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/configuration.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -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/'); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.spec.ts deleted file mode 100644 index f3a99b595488..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -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(); - })); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.ts deleted file mode 100644 index 3986408798fd..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/formatter.service.ts +++ /dev/null @@ -1,51 +0,0 @@ -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]); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/host.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/host.service.ts deleted file mode 100644 index 3d28cd78926a..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/host.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -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; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/pool.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/pool.service.ts deleted file mode 100644 index 8ac6de9d5b4e..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/pool.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -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; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts deleted file mode 100644 index 0f598318a31d..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -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(); - })); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.ts deleted file mode 100644 index b840b3053a05..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/rbd-mirroring.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -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'); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/services.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/services.module.ts deleted file mode 100644 index 04d4a3ccbde3..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/services.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.spec.ts deleted file mode 100644 index 23af9836a147..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -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(); - }) - ); -}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.ts deleted file mode 100644 index 9556930ebd42..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/summary.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -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); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/tcmu-iscsi.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/tcmu-iscsi.service.ts deleted file mode 100644 index 2f36bb81813b..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/tcmu-iscsi.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -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; - }); - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts deleted file mode 100644 index 7651338d9c0b..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts +++ /dev/null @@ -1,43 +0,0 @@ -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 {} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/.gitkeep b/src/pybind/mgr/dashboard_v2/frontend/src/assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/1280px-Mimic_Octopus2.jpg b/src/pybind/mgr/dashboard_v2/frontend/src/assets/1280px-Mimic_Octopus2.jpg deleted file mode 100644 index f8cf2a8de5f3..000000000000 Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/1280px-Mimic_Octopus2.jpg and /dev/null differ diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png b/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png deleted file mode 100644 index 26d602be3bc3..000000000000 Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png and /dev/null differ diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png b/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png deleted file mode 100644 index 0f07b83ed9a3..000000000000 Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Standard_RGB_White_120411_fa.png and /dev/null differ diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/loading.gif b/src/pybind/mgr/dashboard_v2/frontend/src/assets/loading.gif deleted file mode 100755 index 8fb88dea377e..000000000000 Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/loading.gif and /dev/null differ diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/logo-mini.png b/src/pybind/mgr/dashboard_v2/frontend/src/assets/logo-mini.png deleted file mode 100644 index b3446a894dc7..000000000000 Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/logo-mini.png and /dev/null differ diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/notification-icons.png b/src/pybind/mgr/dashboard_v2/frontend/src/assets/notification-icons.png deleted file mode 100644 index d609a7c8cb49..000000000000 Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/assets/notification-icons.png and /dev/null differ diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/defaults.scss b/src/pybind/mgr/dashboard_v2/frontend/src/defaults.scss deleted file mode 100644 index 8e25b71e2806..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/defaults.scss +++ /dev/null @@ -1,11 +0,0 @@ -$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; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.prod.ts b/src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.prod.ts deleted file mode 100644 index 3612073bc31c..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.prod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: true -}; diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.ts b/src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.ts deleted file mode 100644 index b7f639aecac5..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/environments/environment.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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 -}; diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/favicon.ico b/src/pybind/mgr/dashboard_v2/frontend/src/favicon.ico deleted file mode 100644 index 90e538ba7049..000000000000 Binary files a/src/pybind/mgr/dashboard_v2/frontend/src/favicon.ico and /dev/null differ diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/index.html b/src/pybind/mgr/dashboard_v2/frontend/src/index.html deleted file mode 100644 index 05a8f7091167..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - Ceph - - - - - - - - - - - - diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/main.ts b/src/pybind/mgr/dashboard_v2/frontend/src/main.ts deleted file mode 100644 index 91ec6da5f078..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -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)); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/openattic-theme.scss b/src/pybind/mgr/dashboard_v2/frontend/src/openattic-theme.scss deleted file mode 100755 index aa819a2f49e0..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/openattic-theme.scss +++ /dev/null @@ -1,1184 +0,0 @@ -/* - 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; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/polyfills.ts b/src/pybind/mgr/dashboard_v2/frontend/src/polyfills.ts deleted file mode 100644 index caac2e0875de..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/polyfills.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * 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'; diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/styles.scss b/src/pybind/mgr/dashboard_v2/frontend/src/styles.scss deleted file mode 100644 index c10c1ee98b3c..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/styles.scss +++ /dev/null @@ -1,2 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ -@import './openattic-theme.scss'; diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/styles/chart-tooltip.scss b/src/pybind/mgr/dashboard_v2/frontend/src/styles/chart-tooltip.scss deleted file mode 100644 index 835bb362db43..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/styles/chart-tooltip.scss +++ /dev/null @@ -1,62 +0,0 @@ -.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; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/test.ts b/src/pybind/mgr/dashboard_v2/frontend/src/test.ts deleted file mode 100644 index 19beece05c8c..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* 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(); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.app.json b/src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.app.json deleted file mode 100644 index 39ba8dbacbbe..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.app.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/app", - "baseUrl": "./", - "module": "es2015", - "types": [] - }, - "exclude": [ - "test.ts", - "**/*.spec.ts" - ] -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.spec.json b/src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.spec.json deleted file mode 100644 index 63d89ff283f6..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/tsconfig.spec.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/spec", - "baseUrl": "./", - "module": "commonjs", - "target": "es5", - "types": [ - "jasmine", - "node" - ] - }, - "files": [ - "test.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/typings.d.ts b/src/pybind/mgr/dashboard_v2/frontend/src/typings.d.ts deleted file mode 100644 index ef5c7bd62057..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/src/typings.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* SystemJS module definition */ -declare var module: NodeModule; -interface NodeModule { - id: string; -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/tsconfig.json b/src/pybind/mgr/dashboard_v2/frontend/tsconfig.json deleted file mode 100644 index a6c016bf38ad..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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" - ] - } -} diff --git a/src/pybind/mgr/dashboard_v2/frontend/tslint.json b/src/pybind/mgr/dashboard_v2/frontend/tslint.json deleted file mode 100644 index d2486f1bbcaa..000000000000 --- a/src/pybind/mgr/dashboard_v2/frontend/tslint.json +++ /dev/null @@ -1,180 +0,0 @@ -{ - "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"] - } -} diff --git a/src/pybind/mgr/dashboard_v2/module.py b/src/pybind/mgr/dashboard_v2/module.py deleted file mode 100644 index 6877bd86b64d..000000000000 --- a/src/pybind/mgr/dashboard_v2/module.py +++ /dev/null @@ -1,292 +0,0 @@ -# -*- 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 "' - .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:
    -
      - {lis} -
    - """ - endpoints = ['
  • {}
  • '.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 "' - 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 = """ - - - - Ceph - - - - No active ceph-mgr instance is currently running - the dashboard. A failover may be in progress. - Retrying in {delay} seconds... - - - """ - 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") diff --git a/src/pybind/mgr/dashboard_v2/requirements.txt b/src/pybind/mgr/dashboard_v2/requirements.txt deleted file mode 100644 index f6191ea38572..000000000000 --- a/src/pybind/mgr/dashboard_v2/requirements.txt +++ /dev/null @@ -1,32 +0,0 @@ -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 diff --git a/src/pybind/mgr/dashboard_v2/run-backend-api-tests.sh b/src/pybind/mgr/dashboard_v2/run-backend-api-tests.sh deleted file mode 100755 index 64ed636055e8..000000000000 --- a/src/pybind/mgr/dashboard_v2/run-backend-api-tests.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/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 <= 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 diff --git a/src/pybind/mgr/dashboard_v2/settings.py b/src/pybind/mgr/dashboard_v2/settings.py deleted file mode 100644 index 4f68fbb46ffd..000000000000 --- a/src/pybind/mgr/dashboard_v2/settings.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- 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']), '' diff --git a/src/pybind/mgr/dashboard_v2/tests/__init__.py b/src/pybind/mgr/dashboard_v2/tests/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pybind/mgr/dashboard_v2/tests/helper.py b/src/pybind/mgr/dashboard_v2/tests/helper.py deleted file mode 100644 index effe21d76327..000000000000 --- a/src/pybind/mgr/dashboard_v2/tests/helper.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- 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) diff --git a/src/pybind/mgr/dashboard_v2/tests/test_notification.py b/src/pybind/mgr/dashboard_v2/tests/test_notification.py deleted file mode 100644 index bca27f9e6f91..000000000000 --- a/src/pybind/mgr/dashboard_v2/tests/test_notification.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- 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) diff --git a/src/pybind/mgr/dashboard_v2/tests/test_rbd_mirroring.py b/src/pybind/mgr/dashboard_v2/tests/test_rbd_mirroring.py deleted file mode 100644 index 7087ba63b4d4..000000000000 --- a/src/pybind/mgr/dashboard_v2/tests/test_rbd_mirroring.py +++ /dev/null @@ -1,88 +0,0 @@ -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}) diff --git a/src/pybind/mgr/dashboard_v2/tests/test_settings.py b/src/pybind/mgr/dashboard_v2/tests/test_settings.py deleted file mode 100644 index 92fcf7ff349e..000000000000 --- a/src/pybind/mgr/dashboard_v2/tests/test_settings.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- 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'") diff --git a/src/pybind/mgr/dashboard_v2/tests/test_tcmu_iscsi.py b/src/pybind/mgr/dashboard_v2/tests/test_tcmu_iscsi.py deleted file mode 100644 index 88077cbb5bc3..000000000000 --- a/src/pybind/mgr/dashboard_v2/tests/test_tcmu_iscsi.py +++ /dev/null @@ -1,71 +0,0 @@ -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} - }] - }) diff --git a/src/pybind/mgr/dashboard_v2/tests/test_tools.py b/src/pybind/mgr/dashboard_v2/tests/test_tools.py deleted file mode 100644 index ca4d9040c3c0..000000000000 --- a/src/pybind/mgr/dashboard_v2/tests/test_tools.py +++ /dev/null @@ -1,103 +0,0 @@ -# -*- 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) diff --git a/src/pybind/mgr/dashboard_v2/tools.py b/src/pybind/mgr/dashboard_v2/tools.py deleted file mode 100644 index f8013706ba3e..000000000000 --- a/src/pybind/mgr/dashboard_v2/tools.py +++ /dev/null @@ -1,531 +0,0 @@ -# -*- 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") diff --git a/src/pybind/mgr/dashboard_v2/tox.ini b/src/pybind/mgr/dashboard_v2/tox.ini deleted file mode 100644 index 743a8a6929e5..000000000000 --- a/src/pybind/mgr/dashboard_v2/tox.ini +++ /dev/null @@ -1,40 +0,0 @@ -[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 . diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 660158e62d37..eb3d82b98777 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -579,19 +579,19 @@ add_ceph_test(unittest_bufferlist.sh ${CMAKE_SOURCE_DIR}/src/unittest_bufferlist 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} @@ -600,7 +600,7 @@ set_property(TEST 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 diff --git a/src/test/mgr/CMakeLists.txt b/src/test/mgr/CMakeLists.txt index 08b50288963a..c212b5419245 100644 --- a/src/test/mgr/CMakeLists.txt +++ b/src/test/mgr/CMakeLists.txt @@ -1,10 +1,9 @@ #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) diff --git a/src/test/mgr/mgr-dashboard-smoke.sh b/src/test/mgr/mgr-dashboard-smoke.sh new file mode 100755 index 000000000000..582909a6ed54 --- /dev/null +++ b/src/test/mgr/mgr-dashboard-smoke.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2014,2015,2017 Red Hat +# 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: diff --git a/src/test/mgr/mgr-dashboard_v2-smoke.sh b/src/test/mgr/mgr-dashboard_v2-smoke.sh deleted file mode 100755 index b0eb325e8bcc..000000000000 --- a/src/test/mgr/mgr-dashboard_v2-smoke.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright (C) 2014,2015,2017 Red Hat -# 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: diff --git a/src/vstart.sh b/src/vstart.sh index c5fee8d84522..e74f3d61e262 100755 --- a/src/vstart.sh +++ b/src/vstart.sh @@ -136,7 +136,6 @@ VSTART_SEC="client.vstart.sh" MON_ADDR="" DASH_URLS="" -DASH_V2_URLS="" RESTFUL_URLS="" conf_fn="$CEPH_CONF_PATH/ceph.conf" @@ -503,7 +502,7 @@ $DAEMONOPTS $COSDSHORT $extra_conf [mon] - mgr initial modules = restful status balancer + mgr initial modules = dashboard restful status balancer $DAEMONOPTS $CMONDEBUG $extra_conf @@ -649,20 +648,19 @@ start_mgr() { 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)) @@ -672,6 +670,10 @@ EOF # 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 @@ -680,13 +682,6 @@ EOF 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() { @@ -1063,7 +1058,7 @@ fi 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"