]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add a manage clusters page to the multi-cluster nav to
authorAashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com>
Thu, 25 Jan 2024 07:14:01 +0000 (12:44 +0530)
committerNizamudeen A <nia@redhat.com>
Thu, 22 Feb 2024 05:12:01 +0000 (10:42 +0530)
list/connect/disconnect/edit clusters in multi-cluster setup

Fixes: https://tracker.ceph.com/issues/64530
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
23 files changed:
monitoring/ceph-mixin/dashboards.libsonnet
monitoring/ceph-mixin/dashboards/multi-cluster.libsonnet [new file with mode: 0644]
monitoring/ceph-mixin/dashboards/utils.libsonnet
monitoring/ceph-mixin/dashboards_out/multi-cluster-overview.json [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/multi_cluster.py
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/multi-cluster.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/multi-cluster.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts
src/pybind/mgr/dashboard/openapi.yaml

index 8cb0bcceaf79cee921ecd4f00bdda2017b8a2095..82e1888e036d9c1fe36285982bab49274d57c27c 100644 (file)
@@ -8,5 +8,6 @@
     (import 'dashboards/rgw.libsonnet') +
     (import 'dashboards/ceph-cluster.libsonnet') +
     (import 'dashboards/rgw-s3-analytics.libsonnet') +
+    (import 'dashboards/multi-cluster.libsonnet') +
     { _config:: $._config },
 }
diff --git a/monitoring/ceph-mixin/dashboards/multi-cluster.libsonnet b/monitoring/ceph-mixin/dashboards/multi-cluster.libsonnet
new file mode 100644 (file)
index 0000000..ec725f4
--- /dev/null
@@ -0,0 +1,964 @@
+local g = import 'grafonnet/grafana.libsonnet';
+
+(import 'utils.libsonnet') {
+  'multi-cluster-overview.json':
+    $.dashboardSchema(
+      'Ceph - Multi-cluster',
+      '',
+      'BnxelG7Sz',
+      'now-1h',
+      '30s',
+      22,
+      $._config.dashboardTags,
+      ''
+    )
+    .addAnnotation(
+      $.addAnnotationSchema(
+        1,
+        '-- Grafana --',
+        true,
+        true,
+        'rgba(0, 211, 255, 1)',
+        'Annotations & Alerts',
+        'dashboard'
+      )
+    )
+    .addTemplate(
+      g.template.datasource('DS_PROMETHEUS', 'prometheus', 'default', label='Data Source')
+    )
+
+    .addTemplate(
+      $.addTemplateSchema('Cluster',
+                          '$DS_PROMETHEUS',
+                          'label_values(ceph_health_status, cluster)',
+                          2,
+                          true,
+                          0,
+                          null,
+                          '',
+                          current='All')
+    )
+    .addPanels([
+      $.addRowSchema(false, true, 'Clusters') + { gridPos: { x: 0, y: 1, w: 24, h: 1 } },
+      $.addStatPanel(
+        title='Status',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 0, y: 2, w: 5, h: 7 },
+        graphMode='none',
+        colorMode='value',
+        orientation='auto',
+        justifyMode='center',
+        thresholdsMode='absolute',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'text', value: null },
+      ])
+      .addOverrides(
+        [
+          {
+            matcher: { id: 'byName', options: 'Warning' },
+            properties: [
+              {
+                id: 'thresholds',
+                value: { mode: 'absolute', steps: [{ color: 'text', value: null }, { color: 'semi-dark-yellow', value: 1 }] },
+              },
+            ],
+          },
+          {
+            matcher: { id: 'byName', options: 'Error' },
+            properties: [
+              {
+                id: 'thresholds',
+                value: { mode: 'absolute', steps: [{ color: 'text', value: null }, { color: 'semi-dark-red', value: 1 }] },
+              },
+            ],
+          },
+          {
+            matcher: { id: 'byName', options: 'Healthy' },
+            properties: [
+              {
+                id: 'thresholds',
+                value: { mode: 'absolute', steps: [{ color: 'text', value: null }, { color: 'semi-dark-green', value: 1 }] },
+              },
+            ],
+          },
+        ]
+      )
+      .addTargets([
+        $.addTargetSchema(
+          expr='count(ceph_health_status==0) or vector(0)',
+          datasource='${DS_PROMETHEUS}',
+          legendFormat='Healthy',
+        ),
+        $.addTargetSchema(
+          expr='count(ceph_health_status==1)',
+          datasource='${DS_PROMETHEUS}',
+          legendFormat='Warning'
+        ),
+        $.addTargetSchema(
+          expr='count(ceph_health_status==2)',
+          datasource='${DS_PROMETHEUS}',
+          legendFormat='Error'
+        ),
+      ]),
+
+      $.addTableExtended(
+        datasource='${DS_PROMETHEUS}',
+        title='Details',
+        gridPosition={ h: 7, w: 19, x: 5, y: 2 },
+        options={
+          footer: {
+            fields: '',
+            reducer: ['sum'],
+            countRows: false,
+            enablePagination: false,
+            show: false,
+          },
+          frameIndex: 1,
+          showHeader: true,
+        },
+        custom={ align: 'left', cellOptions: { type: 'color-text' }, filterable: false, inspect: false },
+        thresholds={
+          mode: 'absolute',
+          steps: [
+            { color: 'text' },
+          ],
+        },
+        overrides=[
+          {
+            matcher: { id: 'byName', options: 'Value #A' },
+            properties: [
+              { id: 'mappings', value: [{ options: { '0': { color: 'semi-dark-green', index: 2, text: 'Healthy' }, '1': { color: 'semi-dark-yellow', index: 0, text: 'Warning' }, '2': { color: 'semi-dark-red', index: 1, text: 'Error' } }, type: 'value' }] },
+            ],
+          },
+          {
+            matcher: { id: 'byName', options: 'IOPS' },
+            properties: [
+              { id: 'unit', value: 'ops' },
+            ],
+          },
+          {
+            matcher: { id: 'byName', options: 'Value #E' },
+            properties: [
+              { id: 'unit', value: 'bytes' },
+            ],
+          },
+          {
+            matcher: { id: 'byName', options: 'Capacity Used' },
+            properties: [
+              { id: 'unit', value: 'bytes' },
+            ],
+          },
+          {
+            matcher: { id: 'byName', options: 'Cluster' },
+            properties: [
+              { id: 'links', value: [{ title: '', url: '/d/GQ3MHvnIz/ceph-cluster-new?var-cluster=${__data.fields.Cluster}&${DS_PROMETHEUS:queryparam}' }] },
+            ],
+          },
+          {
+            matcher: { id: 'byName', options: 'Alerts' },
+            properties: [
+              { id: 'mappings', value: [{ options: { match: null, result: { index: 0, text: '0' } }, type: 'special' }] },
+            ],
+          },
+        ],
+        pluginVersion='9.4.7'
+      )
+      .addTransformations([
+        {
+          id: 'joinByField',
+          options: { byField: 'cluster', mode: 'outer' },
+        },
+        {
+          id: 'organize',
+          options: {
+            excludeByName: {
+              'Time 1': true,
+              'Time 2': true,
+              'Time 3': true,
+              'Time 4': true,
+              'Time 5': true,
+              'Time 6': true,
+              'Value #B': true,
+              '__name__ 1': true,
+              '__name__ 2': true,
+              '__name__ 3': true,
+              ceph_daemon: true,
+              device_class: true,
+              hostname: true,
+              'instance 1': true,
+              'instance 2': true,
+              'instance 3': true,
+              'job 1': true,
+              'job 2': true,
+              'job 3': true,
+              'replica 1': true,
+              'replica 2': true,
+              'replica 3': true,
+            },
+            indexByName: {
+              'Time 1': 8,
+              'Time 2': 13,
+              'Time 3': 21,
+              'Time 4': 7,
+              'Time 5': 22,
+              'Time 6': 23,
+              'Value #A': 1,
+              'Value #B': 20,
+              'Value #C': 3,
+              'Value #D': 4,
+              'Value #E': 5,
+              'Value #F': 6,
+              '__name__ 1': 9,
+              '__name__ 2': 14,
+              '__name__ 3': 24,
+              ceph_daemon: 15,
+              ceph_version: 2,
+              cluster: 0,
+              device_class: 25,
+              hostname: 16,
+              'instance 1': 10,
+              'instance 2': 17,
+              'instance 3': 26,
+              'job 1': 11,
+              'job 2': 18,
+              'job 3': 27,
+              'replica 1': 12,
+              'replica 2': 19,
+              'replica 3': 28,
+            },
+            renameByName: {
+              'Value #A': 'Status',
+              'Value #C': 'Alerts',
+              'Value #D': 'IOPS',
+              'Value #E': 'Throughput',
+              'Value #F': 'Capacity Used',
+              ceph_version: 'Version',
+              cluster: 'Cluster',
+            },
+          },
+        },
+      ]).addTargets([
+        $.addTargetSchema(
+          expr='ceph_health_status',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=true,
+          interval='',
+          legendFormat='__auto',
+          range=false,
+        ),
+        $.addTargetSchema(
+          expr='ceph_mgr_metadata',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=true,
+          interval='',
+          legendFormat='__auto',
+          range=false,
+        ),
+        $.addTargetSchema(
+          expr='count(ALERTS{alertstate="firing", cluster=~"$Cluster"})',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=true,
+          interval='',
+          legendFormat='__auto',
+          range=false,
+        ),
+        $.addTargetSchema(
+          expr='sum by (cluster) (irate(ceph_pool_wr[$__interval]))  \n+ sum by (cluster) (irate(ceph_pool_rd[$__interval])) ',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=true,
+          interval='',
+          legendFormat='__auto',
+          range=false,
+        ),
+        $.addTargetSchema(
+          expr='sum by (cluster) (irate(ceph_pool_rd_bytes[$__interval]))\n+ sum by (cluster) (irate(ceph_pool_wr_bytes[$__interval])) ',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=true,
+          interval='',
+          legendFormat='__auto',
+          range=false,
+        ),
+        $.addTargetSchema(
+          expr='ceph_cluster_by_class_total_used_bytes',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=true,
+          interval='',
+          legendFormat='__auto',
+          range=false,
+        ),
+      ]),
+
+
+      $.addRowSchema(false, true, 'Overview') + { gridPos: { x: 0, y: 9, w: 24, h: 1 } },
+      $.addStatPanel(
+        title='Cluster Count',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 0, y: 10, w: 3, h: 4 },
+        graphMode='none',
+        colorMode='value',
+        orientation='auto',
+        justifyMode='center',
+        thresholdsMode='absolute',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'text', value: null },
+        { color: 'red', value: 80 },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='count(ceph_health_status{cluster=~"$Cluster"}) or vector(0)',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=true,
+          interval='',
+          legendFormat='__auto',
+          range=false,
+        ),
+      ]),
+
+      $.addGaugePanel(
+        title='Capacity Used',
+        gridPosition={ h: 8, w: 4, x: 3, y: 10 },
+        unit='percentunit',
+        max=1,
+        min=0,
+        interval='1m',
+        pluginVersion='9.4.7'
+      )
+      .addThresholds([
+        { color: 'green', value: null },
+        { color: 'semi-dark-yellow', value: 0.75 },
+        { color: 'red', value: 0.85 },
+      ])
+      .addTarget($.addTargetSchema(
+        expr='sum(ceph_cluster_total_used_bytes{cluster=~"$Cluster"}) / sum(ceph_cluster_total_bytes{cluster=~"$Cluster"})',
+        instant=true,
+        legendFormat='Used',
+        datasource='${DS_PROMETHEUS}',
+      )),
+
+      $.addStatPanel(
+        title='Total Capacity',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 7, y: 10, w: 3, h: 4 },
+        graphMode='area',
+        colorMode='none',
+        orientation='auto',
+        justifyMode='auto',
+        thresholdsMode='absolute',
+        unit='bytes',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'green', value: null },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='sum(ceph_cluster_total_bytes{cluster=~"$Cluster"})',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=false,
+          interval='',
+          legendFormat='__auto',
+          range=true,
+        ),
+      ]),
+
+      $.addStatPanel(
+        title='OSDs',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 10, y: 10, w: 3, h: 4 },
+        graphMode='area',
+        colorMode='none',
+        orientation='auto',
+        justifyMode='auto',
+        thresholdsMode='absolute',
+        unit='none',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'green', value: null },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='count(ceph_osd_metadata{cluster=~"$Cluster"})',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=false,
+          interval='',
+          legendFormat='__auto',
+          range=true,
+        ),
+      ]),
+
+      $.addStatPanel(
+        title='Hosts',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 13, y: 10, w: 3, h: 4 },
+        graphMode='area',
+        colorMode='none',
+        orientation='auto',
+        justifyMode='auto',
+        thresholdsMode='absolute',
+        unit='none',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'green', value: null },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='count(sum by (hostname) (ceph_osd_metadata{cluster=~"$Cluster"}))',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=false,
+          interval='',
+          legendFormat='__auto',
+          range=true,
+        ),
+      ]),
+
+      $.addStatPanel(
+        title='Client IOPS',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 16, y: 10, w: 4, h: 4 },
+        graphMode='area',
+        colorMode='none',
+        orientation='auto',
+        justifyMode='center',
+        thresholdsMode='absolute',
+        unit='ops',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'green', value: null },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='sum(irate(ceph_pool_wr{cluster=~"$Cluster"}[$__interval]))',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          hide=false,
+          exemplar=false,
+          instant=false,
+          legendFormat='Write',
+          range=true,
+        ),
+        $.addTargetSchema(
+          expr='sum(irate(ceph_pool_rd{cluster=~"$Cluster"}[$__interval]))',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          hide=false,
+          exemplar=false,
+          legendFormat='Read',
+          range=true,
+        ),
+      ]),
+
+      $.addStatPanel(
+        title='OSD Latencies',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 20, y: 10, w: 4, h: 4 },
+        graphMode='area',
+        colorMode='none',
+        orientation='auto',
+        justifyMode='center',
+        thresholdsMode='absolute',
+        unit='ms',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'green', value: null },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='avg(ceph_osd_apply_latency_ms{cluster=~"$Cluster"})',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          hide=false,
+          exemplar=false,
+          instant=false,
+          legendFormat='Apply',
+          range=true,
+        ),
+        $.addTargetSchema(
+          expr='avg(ceph_osd_commit_latency_ms{cluster=~"$Cluster"})',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          hide=false,
+          exemplar=false,
+          legendFormat='Commit',
+          range=true,
+        ),
+      ]),
+
+      $.addStatPanel(
+        title='Alert Count',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 0, y: 14, w: 3, h: 4 },
+        graphMode='none',
+        colorMode='value',
+        orientation='auto',
+        justifyMode='center',
+        thresholdsMode='absolute',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'text', value: null },
+        { color: 'red', value: 80 },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='count(ALERTS{alertstate="firing", cluster=~"$Cluster"}) or vector(0)',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=true,
+          interval='',
+          legendFormat='__auto',
+          range=false,
+        ),
+      ]),
+
+      $.addStatPanel(
+        title='Total Used',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 7, y: 14, w: 3, h: 4 },
+        graphMode='area',
+        colorMode='none',
+        orientation='auto',
+        justifyMode='auto',
+        thresholdsMode='absolute',
+        unit='bytes',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'green', value: null },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='sum(ceph_cluster_total_used_bytes{cluster=~"$Cluster"})',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=false,
+          interval='',
+          legendFormat='__auto',
+          range=true,
+        ),
+      ]),
+
+      $.addStatPanel(
+        title='Capacity Prediction',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 10, y: 14, w: 3, h: 4 },
+        graphMode='none',
+        colorMode='none',
+        orientation='auto',
+        justifyMode='auto',
+        unit='s',
+        thresholdsMode='absolute',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'green', value: null },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='predict_linear(avg(increase(ceph_cluster_total_used_bytes{cluster=~"${Cluster}"}[1d]))[7d:1h],120)',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          hide=false,
+          exemplar=false,
+          legendFormat='__auto',
+          range=true,
+        ),
+      ]),
+
+      $.addStatPanel(
+        title='Pools',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 13, y: 14, w: 3, h: 4 },
+        graphMode='area',
+        colorMode='none',
+        orientation='auto',
+        justifyMode='auto',
+        thresholdsMode='absolute',
+        unit='none',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'green', value: null },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='count(ceph_pool_metadata{cluster=~"$Cluster"})',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          format='table',
+          hide=false,
+          exemplar=false,
+          instant=false,
+          interval='',
+          legendFormat='__auto',
+          range=true,
+        ),
+      ]),
+
+      $.addStatPanel(
+        title='Client Bandwidth',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 16, y: 14, w: 4, h: 4 },
+        graphMode='area',
+        colorMode='none',
+        orientation='auto',
+        justifyMode='center',
+        thresholdsMode='absolute',
+        unit='binBps',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'green', value: null },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='sum(irate(ceph_pool_rd_bytes{cluster=~"$Cluster"}[$__interval]))',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          hide=false,
+          exemplar=false,
+          instant=false,
+          legendFormat='Write',
+          range=true,
+        ),
+        $.addTargetSchema(
+          expr='sum(irate(ceph_pool_wr_bytes{cluster=~"$Cluster"}[$__interval]))',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          hide=false,
+          exemplar=false,
+          legendFormat='Read',
+          range=true,
+        ),
+      ]),
+
+      $.addStatPanel(
+        title='Recovery Rate',
+        datasource='${DS_PROMETHEUS}',
+        gridPosition={ x: 20, y: 14, w: 4, h: 4 },
+        graphMode='area',
+        colorMode='none',
+        orientation='auto',
+        justifyMode='center',
+        thresholdsMode='absolute',
+        unit='binBps',
+        pluginVersion='9.4.7',
+      ).addThresholds([
+        { color: 'green', value: null },
+      ])
+      .addTargets([
+        $.addTargetSchema(
+          expr='sum(irate(ceph_osd_recovery_ops{cluster=~"$Cluster"}[$__interval]))',
+          datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+          hide=false,
+          exemplar=false,
+          instant=false,
+          legendFormat='Write',
+          range=true,
+        ),
+      ]),
+
+
+      $.addRowSchema(false, true, 'Alerts', collapsed=true)
+      .addPanels([
+        $.addStatPanel(
+          title='Status',
+          datasource='${DS_PROMETHEUS}',
+          gridPosition={ x: 0, y: 19, w: 5, h: 7 },
+          graphMode='area',
+          colorMode='value',
+          orientation='auto',
+          justifyMode='center',
+          thresholdsMode='absolute',
+          pluginVersion='9.4.7',
+        ).addThresholds([
+          { color: 'text', value: null },
+        ])
+        .addOverrides(
+          [
+            {
+              matcher: { id: 'byName', options: 'Critical' },
+              properties: [
+                {
+                  id: 'thresholds',
+                  value: { mode: 'absolute', steps: [{ color: 'text', value: null }, { color: 'semi-dark-red', value: 1 }] },
+                },
+              ],
+            },
+            {
+              matcher: { id: 'byName', options: 'Warning' },
+              properties: [
+                {
+                  id: 'thresholds',
+                  value: { mode: 'absolute', steps: [{ color: 'text', value: null }, { color: 'semi-dark-yellow', value: 1 }] },
+                },
+              ],
+            },
+          ]
+        )
+        .addTargets([
+          $.addTargetSchema(
+            expr='count(ALERTS{alertstate="firing",severity="critical", cluster=~"$Cluster"}) OR vector(0)',
+            datasource='${DS_PROMETHEUS}',
+            legendFormat='Critical',
+            instant=true,
+            range=false
+          ),
+          $.addTargetSchema(
+            expr='count(ALERTS{alertstate="firing",severity="warning", cluster=~"$Cluster"}) OR vector(0)',
+            datasource='${DS_PROMETHEUS}',
+            legendFormat='Warning',
+            instant=true,
+            range=false
+          ),
+        ]),
+
+
+        $.addTableExtended(
+          datasource='${DS_PROMETHEUS}',
+          title='Alerts',
+          gridPosition={ h: 7, w: 19, x: 5, y: 19 },
+          options={
+            footer: {
+              fields: '',
+              reducer: ['sum'],
+              countRows: false,
+              enablePagination: false,
+              show: false,
+            },
+            frameIndex: 1,
+            showHeader: true,
+            sortBy: [{ desc: false, displayName: 'Severity' }],
+          },
+          custom={ align: 'auto', cellOptions: { type: 'auto' }, filterable: true, inspect: false },
+          thresholds={
+            mode: 'absolute',
+            steps: [
+              { color: 'green' },
+              { color: 'red', value: 80 },
+            ],
+          },
+          pluginVersion='9.4.7'
+        )
+        .addTransformations([
+          {
+            id: 'joinByField',
+            options: { byField: 'cluster', mode: 'outer' },
+          },
+          {
+            id: 'organize',
+            options: {
+              excludeByName: {
+                Time: true,
+                Value: true,
+                __name__: true,
+                instance: true,
+                job: true,
+                oid: true,
+                replica: true,
+                type: true,
+              },
+              indexByName: {
+                Time: 0,
+                Value: 9,
+                __name__: 1,
+                alertname: 2,
+                alertstate: 4,
+                cluster: 3,
+                instance: 6,
+                job: 7,
+                severity: 5,
+                type: 8,
+              },
+              renameByName: {
+                alertname: 'Name',
+                alertstate: 'State',
+                cluster: 'Cluster',
+                severity: 'Severity',
+              },
+            },
+          },
+        ]).addTargets([
+          $.addTargetSchema(
+            expr='ALERTS{alertstate="firing", cluster=~"$Cluster"}',
+            datasource={ type: 'prometheus', uid: '${DS_PROMETHEUS}' },
+            format='table',
+            hide=false,
+            exemplar=false,
+            instant=true,
+            interval='',
+            legendFormat='__auto',
+            range=false,
+          ),
+        ]),
+
+        $.addAlertListPanel(
+          title='Alerts(Grouped)',
+          datasource={
+            type: 'datasource',
+            uid: 'grafana',
+          },
+          gridPosition={ h: 8, w: 24, x: 0, y: 26 },
+          alertName='',
+          dashboardAlerts=false,
+          groupBy=[],
+          groupMode='default',
+          maxItems=20,
+          sortOrder=1,
+          stateFilter={
+            'error': true,
+            firing: true,
+            noData: false,
+            normal: false,
+            pending: true,
+          },
+        ),
+      ]) + { gridPos: { x: 0, y: 18, w: 24, h: 1 } },
+
+      $.addRowSchema(false, true, 'Cluster Stats', collapsed=true)
+      .addPanels([
+        $.timeSeriesPanel(
+          lineInterpolation='linear',
+          lineWidth=1,
+          drawStyle='line',
+          axisPlacement='auto',
+          title='Top 5 - Capacity Utilization(%)',
+          datasource='${DS_PROMETHEUS}',
+          gridPosition={ h: 7, w: 8, x: 0, y: 30 },
+          fillOpacity=0,
+          pointSize=5,
+          showPoints='auto',
+          unit='percentunit',
+          displayMode='table',
+          showLegend=true,
+          placement='bottom',
+          tooltip={ mode: 'multi', sort: 'desc' },
+          stackingMode='none',
+          spanNulls=false,
+          decimals=2,
+          thresholdsMode='percentage',
+          sortBy='Last',
+          sortDesc=true
+        )
+        .addCalcs(['last'])
+        .addThresholds([
+          { color: 'green' },
+        ])
+        .addTargets(
+          [
+            $.addTargetSchema(
+              expr='topk(5, ceph_cluster_total_used_bytes/ceph_cluster_total_bytes)',
+              datasource='${DS_PROMETHEUS}',
+              instant=false,
+              legendFormat='{{cluster}}',
+              step=300,
+              range=true,
+            ),
+          ]
+        ),
+
+
+        $.timeSeriesPanel(
+          lineInterpolation='linear',
+          lineWidth=1,
+          drawStyle='line',
+          axisPlacement='auto',
+          title='Top 5 - Cluster IOPS',
+          datasource='${DS_PROMETHEUS}',
+          gridPosition={ h: 7, w: 8, x: 8, y: 30 },
+          fillOpacity=0,
+          pointSize=5,
+          showPoints='auto',
+          unit='ops',
+          displayMode='table',
+          showLegend=true,
+          placement='bottom',
+          tooltip={ mode: 'multi', sort: 'desc' },
+          stackingMode='none',
+          spanNulls=false,
+          decimals=2,
+          thresholdsMode='percentage',
+          sortBy='Last',
+          sortDesc=true
+        )
+        .addCalcs(['last'])
+        .addThresholds([
+          { color: 'green' },
+        ])
+        .addTargets(
+          [
+            $.addTargetSchema(
+              expr='topk(10, sum by (cluster) (irate(ceph_osd_op_w[$__interval]))  \n+ sum by (cluster) (irate(ceph_osd_op_r[$__interval])) )',
+              datasource='${DS_PROMETHEUS}',
+              instant=false,
+              legendFormat='{{cluster}}',
+              step=300,
+              range=true,
+            ),
+          ]
+        ),
+
+
+        $.timeSeriesPanel(
+          lineInterpolation='linear',
+          lineWidth=1,
+          drawStyle='line',
+          axisPlacement='auto',
+          title='Top 10 - Capacity Utilization(%) by Pool',
+          datasource='${DS_PROMETHEUS}',
+          gridPosition={ h: 7, w: 8, x: 16, y: 30 },
+          fillOpacity=0,
+          pointSize=5,
+          showPoints='auto',
+          unit='percentunit',
+          displayMode='table',
+          showLegend=true,
+          placement='bottom',
+          tooltip={ mode: 'multi', sort: 'desc' },
+          stackingMode='none',
+          spanNulls=false,
+          decimals=2,
+          thresholdsMode='absolute',
+          sortBy='Last',
+          sortDesc=true
+        )
+        .addCalcs(['last'])
+        .addThresholds([
+          { color: 'green' },
+        ])
+        .addTargets(
+          [
+            $.addTargetSchema(
+              expr='topk(10, ceph_pool_bytes_used{cluster=~"$Cluster"}/ceph_pool_max_avail{cluster=~"$Cluster"} * on(pool_id, cluster) group_left(instance, name) ceph_pool_metadata{cluster=~"$Cluster"})',
+              datasource='${DS_PROMETHEUS}',
+              instant=false,
+              legendFormat='{{cluster}} - {{name}}',
+              step=300,
+              range=true,
+            ),
+          ]
+        ),
+      ]) + { gridPos: { x: 0, y: 29, w: 24, h: 1 } },
+    ]),
+}
index 397c00fe53add3ea5a4d6418fbc7d67d81d282c7..a1c2fe9421421b48c2ecb4d06319cbe21089e5b5 100644 (file)
@@ -117,7 +117,8 @@ local timeSeries = import 'timeseries_panel.libsonnet';
                     regex,
                     hide='',
                     multi=false,
-                    allValues=null)::
+                    allValues=null,
+                    current=null)::
     g.template.new(name=name,
                    datasource=datasource,
                    query=query,
@@ -128,7 +129,8 @@ local timeSeries = import 'timeseries_panel.libsonnet';
                    regex=regex,
                    hide=hide,
                    multi=multi,
-                   allValues=allValues),
+                   allValues=allValues,
+                   current=current),
 
   addAnnotationSchema(builtIn,
                       datasource,
diff --git a/monitoring/ceph-mixin/dashboards_out/multi-cluster-overview.json b/monitoring/ceph-mixin/dashboards_out/multi-cluster-overview.json
new file mode 100644 (file)
index 0000000..91b2934
--- /dev/null
@@ -0,0 +1,2123 @@
+{
+   "__inputs": [ ],
+   "__requires": [ ],
+   "annotations": {
+      "list": [
+         {
+            "builtIn": 1,
+            "datasource": "-- Grafana --",
+            "enable": true,
+            "hide": true,
+            "iconColor": "rgba(0, 211, 255, 1)",
+            "name": "Annotations & Alerts",
+            "showIn": 0,
+            "tags": [ ],
+            "type": "dashboard"
+         }
+      ]
+   },
+   "description": "",
+   "editable": false,
+   "gnetId": null,
+   "graphTooltip": 0,
+   "hideControls": false,
+   "id": null,
+   "links": [ ],
+   "panels": [
+      {
+         "collapse": false,
+         "collapsed": false,
+         "gridPos": {
+            "h": 1,
+            "w": 24,
+            "x": 0,
+            "y": 1
+         },
+         "id": 2,
+         "panels": [ ],
+         "repeat": null,
+         "repeatIteration": null,
+         "repeatRowId": null,
+         "showTitle": true,
+         "title": "Clusters",
+         "titleSize": "h6",
+         "type": "row"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "text",
+                        "value": null
+                     }
+                  ]
+               },
+               "unit": "none"
+            },
+            "overrides": [
+               {
+                  "matcher": {
+                     "id": "byName",
+                     "options": "Warning"
+                  },
+                  "properties": [
+                     {
+                        "id": "thresholds",
+                        "value": {
+                           "mode": "absolute",
+                           "steps": [
+                              {
+                                 "color": "text",
+                                 "value": null
+                              },
+                              {
+                                 "color": "semi-dark-yellow",
+                                 "value": 1
+                              }
+                           ]
+                        }
+                     }
+                  ]
+               },
+               {
+                  "matcher": {
+                     "id": "byName",
+                     "options": "Error"
+                  },
+                  "properties": [
+                     {
+                        "id": "thresholds",
+                        "value": {
+                           "mode": "absolute",
+                           "steps": [
+                              {
+                                 "color": "text",
+                                 "value": null
+                              },
+                              {
+                                 "color": "semi-dark-red",
+                                 "value": 1
+                              }
+                           ]
+                        }
+                     }
+                  ]
+               },
+               {
+                  "matcher": {
+                     "id": "byName",
+                     "options": "Healthy"
+                  },
+                  "properties": [
+                     {
+                        "id": "thresholds",
+                        "value": {
+                           "mode": "absolute",
+                           "steps": [
+                              {
+                                 "color": "text",
+                                 "value": null
+                              },
+                              {
+                                 "color": "semi-dark-green",
+                                 "value": 1
+                              }
+                           ]
+                        }
+                     }
+                  ]
+               }
+            ]
+         },
+         "gridPos": {
+            "h": 7,
+            "w": 5,
+            "x": 0,
+            "y": 2
+         },
+         "id": 3,
+         "links": [ ],
+         "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": "${DS_PROMETHEUS}",
+               "expr": "count(ceph_health_status==0) or vector(0)",
+               "format": "time_series",
+               "intervalFactor": 1,
+               "legendFormat": "Healthy",
+               "refId": "A"
+            },
+            {
+               "datasource": "${DS_PROMETHEUS}",
+               "expr": "count(ceph_health_status==1)",
+               "format": "time_series",
+               "intervalFactor": 1,
+               "legendFormat": "Warning",
+               "refId": "B"
+            },
+            {
+               "datasource": "${DS_PROMETHEUS}",
+               "expr": "count(ceph_health_status==2)",
+               "format": "time_series",
+               "intervalFactor": 1,
+               "legendFormat": "Error",
+               "refId": "C"
+            }
+         ],
+         "title": "Status",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "columns": [ ],
+         "datasource": "${DS_PROMETHEUS}",
+         "fieldConfig": {
+            "defaults": {
+               "custom": {
+                  "align": "left",
+                  "cellOptions": {
+                     "type": "color-text"
+                  },
+                  "filterable": false,
+                  "inspect": false
+               },
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "text"
+                     }
+                  ]
+               }
+            },
+            "overrides": [
+               {
+                  "matcher": {
+                     "id": "byName",
+                     "options": "Value #A"
+                  },
+                  "properties": [
+                     {
+                        "id": "mappings",
+                        "value": [
+                           {
+                              "options": {
+                                 "0": {
+                                    "color": "semi-dark-green",
+                                    "index": 2,
+                                    "text": "Healthy"
+                                 },
+                                 "1": {
+                                    "color": "semi-dark-yellow",
+                                    "index": 0,
+                                    "text": "Warning"
+                                 },
+                                 "2": {
+                                    "color": "semi-dark-red",
+                                    "index": 1,
+                                    "text": "Error"
+                                 }
+                              },
+                              "type": "value"
+                           }
+                        ]
+                     }
+                  ]
+               },
+               {
+                  "matcher": {
+                     "id": "byName",
+                     "options": "IOPS"
+                  },
+                  "properties": [
+                     {
+                        "id": "unit",
+                        "value": "ops"
+                     }
+                  ]
+               },
+               {
+                  "matcher": {
+                     "id": "byName",
+                     "options": "Value #E"
+                  },
+                  "properties": [
+                     {
+                        "id": "unit",
+                        "value": "bytes"
+                     }
+                  ]
+               },
+               {
+                  "matcher": {
+                     "id": "byName",
+                     "options": "Capacity Used"
+                  },
+                  "properties": [
+                     {
+                        "id": "unit",
+                        "value": "bytes"
+                     }
+                  ]
+               },
+               {
+                  "matcher": {
+                     "id": "byName",
+                     "options": "Cluster"
+                  },
+                  "properties": [
+                     {
+                        "id": "links",
+                        "value": [
+                           {
+                              "title": "",
+                              "url": "/d/GQ3MHvnIz/ceph-cluster-new?var-cluster=${__data.fields.Cluster}&${DS_PROMETHEUS:queryparam}"
+                           }
+                        ]
+                     }
+                  ]
+               },
+               {
+                  "matcher": {
+                     "id": "byName",
+                     "options": "Alerts"
+                  },
+                  "properties": [
+                     {
+                        "id": "mappings",
+                        "value": [
+                           {
+                              "options": {
+                                 "match": null,
+                                 "result": {
+                                    "index": 0,
+                                    "text": "0"
+                                 }
+                              },
+                              "type": "special"
+                           }
+                        ]
+                     }
+                  ]
+               }
+            ]
+         },
+         "gridPos": {
+            "h": 7,
+            "w": 19,
+            "x": 5,
+            "y": 2
+         },
+         "id": 4,
+         "links": [ ],
+         "options": {
+            "footer": {
+               "countRows": false,
+               "enablePagination": false,
+               "fields": "",
+               "reducer": [
+                  "sum"
+               ],
+               "show": false
+            },
+            "frameIndex": 1,
+            "showHeader": true
+         },
+         "pluginVersion": "9.4.7",
+         "styles": "",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "ceph_health_status",
+               "format": "table",
+               "hide": false,
+               "instant": true,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": false,
+               "refId": "A"
+            },
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "ceph_mgr_metadata",
+               "format": "table",
+               "hide": false,
+               "instant": true,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": false,
+               "refId": "B"
+            },
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "count(ALERTS{alertstate=\"firing\", cluster=~\"$Cluster\"})",
+               "format": "table",
+               "hide": false,
+               "instant": true,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": false,
+               "refId": "C"
+            },
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "sum by (cluster) (irate(ceph_pool_wr[$__interval]))  \n+ sum by (cluster) (irate(ceph_pool_rd[$__interval])) ",
+               "format": "table",
+               "hide": false,
+               "instant": true,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": false,
+               "refId": "D"
+            },
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "sum by (cluster) (irate(ceph_pool_rd_bytes[$__interval]))\n+ sum by (cluster) (irate(ceph_pool_wr_bytes[$__interval])) ",
+               "format": "table",
+               "hide": false,
+               "instant": true,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": false,
+               "refId": "E"
+            },
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "ceph_cluster_by_class_total_used_bytes",
+               "format": "table",
+               "hide": false,
+               "instant": true,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": false,
+               "refId": "F"
+            }
+         ],
+         "timeFrom": null,
+         "timeShift": null,
+         "title": "Details",
+         "transformations": [
+            {
+               "id": "joinByField",
+               "options": {
+                  "byField": "cluster",
+                  "mode": "outer"
+               }
+            },
+            {
+               "id": "organize",
+               "options": {
+                  "excludeByName": {
+                     "Time 1": true,
+                     "Time 2": true,
+                     "Time 3": true,
+                     "Time 4": true,
+                     "Time 5": true,
+                     "Time 6": true,
+                     "Value #B": true,
+                     "__name__ 1": true,
+                     "__name__ 2": true,
+                     "__name__ 3": true,
+                     "ceph_daemon": true,
+                     "device_class": true,
+                     "hostname": true,
+                     "instance 1": true,
+                     "instance 2": true,
+                     "instance 3": true,
+                     "job 1": true,
+                     "job 2": true,
+                     "job 3": true,
+                     "replica 1": true,
+                     "replica 2": true,
+                     "replica 3": true
+                  },
+                  "indexByName": {
+                     "Time 1": 8,
+                     "Time 2": 13,
+                     "Time 3": 21,
+                     "Time 4": 7,
+                     "Time 5": 22,
+                     "Time 6": 23,
+                     "Value #A": 1,
+                     "Value #B": 20,
+                     "Value #C": 3,
+                     "Value #D": 4,
+                     "Value #E": 5,
+                     "Value #F": 6,
+                     "__name__ 1": 9,
+                     "__name__ 2": 14,
+                     "__name__ 3": 24,
+                     "ceph_daemon": 15,
+                     "ceph_version": 2,
+                     "cluster": 0,
+                     "device_class": 25,
+                     "hostname": 16,
+                     "instance 1": 10,
+                     "instance 2": 17,
+                     "instance 3": 26,
+                     "job 1": 11,
+                     "job 2": 18,
+                     "job 3": 27,
+                     "replica 1": 12,
+                     "replica 2": 19,
+                     "replica 3": 28
+                  },
+                  "renameByName": {
+                     "Value #A": "Status",
+                     "Value #C": "Alerts",
+                     "Value #D": "IOPS",
+                     "Value #E": "Throughput",
+                     "Value #F": "Capacity Used",
+                     "ceph_version": "Version",
+                     "cluster": "Cluster"
+                  }
+               }
+            }
+         ],
+         "type": "table"
+      },
+      {
+         "collapse": false,
+         "collapsed": false,
+         "gridPos": {
+            "h": 1,
+            "w": 24,
+            "x": 0,
+            "y": 9
+         },
+         "id": 5,
+         "panels": [ ],
+         "repeat": null,
+         "repeatIteration": null,
+         "repeatRowId": null,
+         "showTitle": true,
+         "title": "Overview",
+         "titleSize": "h6",
+         "type": "row"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "text",
+                        "value": null
+                     },
+                     {
+                        "color": "red",
+                        "value": 80
+                     }
+                  ]
+               },
+               "unit": "none"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 3,
+            "x": 0,
+            "y": 10
+         },
+         "id": 6,
+         "links": [ ],
+         "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "count(ceph_health_status{cluster=~\"$Cluster\"}) or vector(0)",
+               "format": "table",
+               "hide": false,
+               "instant": true,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": false,
+               "refId": "A"
+            }
+         ],
+         "title": "Cluster Count",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "links": [ ],
+               "mappings": [ ],
+               "max": 1,
+               "min": 0,
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "green",
+                        "value": null
+                     },
+                     {
+                        "color": "semi-dark-yellow",
+                        "value": 0.75
+                     },
+                     {
+                        "color": "red",
+                        "value": 0.84999999999999998
+                     }
+                  ]
+               },
+               "unit": "percentunit"
+            }
+         },
+         "gridPos": {
+            "h": 8,
+            "w": 4,
+            "x": 3,
+            "y": 10
+         },
+         "id": 7,
+         "interval": "1m",
+         "links": [ ],
+         "maxDataPoints": 100,
+         "options": {
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "showThresholdLabels": false,
+            "showThresholdMarkers": true
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": "${DS_PROMETHEUS}",
+               "expr": "sum(ceph_cluster_total_used_bytes{cluster=~\"$Cluster\"}) / sum(ceph_cluster_total_bytes{cluster=~\"$Cluster\"})",
+               "format": "time_series",
+               "instant": true,
+               "intervalFactor": 1,
+               "legendFormat": "Used",
+               "refId": "A"
+            }
+         ],
+         "title": "Capacity Used",
+         "transparent": false,
+         "type": "gauge"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "green",
+                        "value": null
+                     }
+                  ]
+               },
+               "unit": "bytes"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 3,
+            "x": 7,
+            "y": 10
+         },
+         "id": 8,
+         "links": [ ],
+         "options": {
+            "colorMode": "none",
+            "graphMode": "area",
+            "justifyMode": "auto",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "sum(ceph_cluster_total_bytes{cluster=~\"$Cluster\"})",
+               "format": "table",
+               "hide": false,
+               "instant": false,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": true,
+               "refId": "A"
+            }
+         ],
+         "title": "Total Capacity",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "green",
+                        "value": null
+                     }
+                  ]
+               },
+               "unit": "none"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 3,
+            "x": 10,
+            "y": 10
+         },
+         "id": 9,
+         "links": [ ],
+         "options": {
+            "colorMode": "none",
+            "graphMode": "area",
+            "justifyMode": "auto",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "count(ceph_osd_metadata{cluster=~\"$Cluster\"})",
+               "format": "table",
+               "hide": false,
+               "instant": false,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": true,
+               "refId": "A"
+            }
+         ],
+         "title": "OSDs",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "green",
+                        "value": null
+                     }
+                  ]
+               },
+               "unit": "none"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 3,
+            "x": 13,
+            "y": 10
+         },
+         "id": 10,
+         "links": [ ],
+         "options": {
+            "colorMode": "none",
+            "graphMode": "area",
+            "justifyMode": "auto",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "count(sum by (hostname) (ceph_osd_metadata{cluster=~\"$Cluster\"}))",
+               "format": "table",
+               "hide": false,
+               "instant": false,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": true,
+               "refId": "A"
+            }
+         ],
+         "title": "Hosts",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "green",
+                        "value": null
+                     }
+                  ]
+               },
+               "unit": "ops"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 16,
+            "y": 10
+         },
+         "id": 11,
+         "links": [ ],
+         "options": {
+            "colorMode": "none",
+            "graphMode": "area",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "sum(irate(ceph_pool_wr{cluster=~\"$Cluster\"}[$__interval]))",
+               "format": "time_series",
+               "hide": false,
+               "instant": false,
+               "intervalFactor": 1,
+               "legendFormat": "Write",
+               "range": true,
+               "refId": "A"
+            },
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "sum(irate(ceph_pool_rd{cluster=~\"$Cluster\"}[$__interval]))",
+               "format": "time_series",
+               "hide": false,
+               "intervalFactor": 1,
+               "legendFormat": "Read",
+               "range": true,
+               "refId": "B"
+            }
+         ],
+         "title": "Client IOPS",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "green",
+                        "value": null
+                     }
+                  ]
+               },
+               "unit": "ms"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 20,
+            "y": 10
+         },
+         "id": 12,
+         "links": [ ],
+         "options": {
+            "colorMode": "none",
+            "graphMode": "area",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "avg(ceph_osd_apply_latency_ms{cluster=~\"$Cluster\"})",
+               "format": "time_series",
+               "hide": false,
+               "instant": false,
+               "intervalFactor": 1,
+               "legendFormat": "Apply",
+               "range": true,
+               "refId": "A"
+            },
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "avg(ceph_osd_commit_latency_ms{cluster=~\"$Cluster\"})",
+               "format": "time_series",
+               "hide": false,
+               "intervalFactor": 1,
+               "legendFormat": "Commit",
+               "range": true,
+               "refId": "B"
+            }
+         ],
+         "title": "OSD Latencies",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "text",
+                        "value": null
+                     },
+                     {
+                        "color": "red",
+                        "value": 80
+                     }
+                  ]
+               },
+               "unit": "none"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 3,
+            "x": 0,
+            "y": 14
+         },
+         "id": 13,
+         "links": [ ],
+         "options": {
+            "colorMode": "value",
+            "graphMode": "none",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "count(ALERTS{alertstate=\"firing\", cluster=~\"$Cluster\"}) or vector(0)",
+               "format": "table",
+               "hide": false,
+               "instant": true,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": false,
+               "refId": "A"
+            }
+         ],
+         "title": "Alert Count",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "green",
+                        "value": null
+                     }
+                  ]
+               },
+               "unit": "bytes"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 3,
+            "x": 7,
+            "y": 14
+         },
+         "id": 14,
+         "links": [ ],
+         "options": {
+            "colorMode": "none",
+            "graphMode": "area",
+            "justifyMode": "auto",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "sum(ceph_cluster_total_used_bytes{cluster=~\"$Cluster\"})",
+               "format": "table",
+               "hide": false,
+               "instant": false,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": true,
+               "refId": "A"
+            }
+         ],
+         "title": "Total Used",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "green",
+                        "value": null
+                     }
+                  ]
+               },
+               "unit": "s"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 3,
+            "x": 10,
+            "y": 14
+         },
+         "id": 15,
+         "links": [ ],
+         "options": {
+            "colorMode": "none",
+            "graphMode": "none",
+            "justifyMode": "auto",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "predict_linear(avg(increase(ceph_cluster_total_used_bytes{cluster=~\"${Cluster}\"}[1d]))[7d:1h],120)",
+               "format": "time_series",
+               "hide": false,
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": true,
+               "refId": "A"
+            }
+         ],
+         "title": "Capacity Prediction",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "green",
+                        "value": null
+                     }
+                  ]
+               },
+               "unit": "none"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 3,
+            "x": 13,
+            "y": 14
+         },
+         "id": 16,
+         "links": [ ],
+         "options": {
+            "colorMode": "none",
+            "graphMode": "area",
+            "justifyMode": "auto",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "count(ceph_pool_metadata{cluster=~\"$Cluster\"})",
+               "format": "table",
+               "hide": false,
+               "instant": false,
+               "interval": "",
+               "intervalFactor": 1,
+               "legendFormat": "__auto",
+               "range": true,
+               "refId": "A"
+            }
+         ],
+         "title": "Pools",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "green",
+                        "value": null
+                     }
+                  ]
+               },
+               "unit": "binBps"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 16,
+            "y": 14
+         },
+         "id": 17,
+         "links": [ ],
+         "options": {
+            "colorMode": "none",
+            "graphMode": "area",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "sum(irate(ceph_pool_rd_bytes{cluster=~\"$Cluster\"}[$__interval]))",
+               "format": "time_series",
+               "hide": false,
+               "instant": false,
+               "intervalFactor": 1,
+               "legendFormat": "Write",
+               "range": true,
+               "refId": "A"
+            },
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "sum(irate(ceph_pool_wr_bytes{cluster=~\"$Cluster\"}[$__interval]))",
+               "format": "time_series",
+               "hide": false,
+               "intervalFactor": 1,
+               "legendFormat": "Read",
+               "range": true,
+               "refId": "B"
+            }
+         ],
+         "title": "Client Bandwidth",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "colors": null,
+         "datasource": "${DS_PROMETHEUS}",
+         "description": "",
+         "fieldConfig": {
+            "defaults": {
+               "decimals": 0,
+               "links": [ ],
+               "mappings": [ ],
+               "thresholds": {
+                  "mode": "absolute",
+                  "steps": [
+                     {
+                        "color": "green",
+                        "value": null
+                     }
+                  ]
+               },
+               "unit": "binBps"
+            }
+         },
+         "gridPos": {
+            "h": 4,
+            "w": 4,
+            "x": 20,
+            "y": 14
+         },
+         "id": 18,
+         "links": [ ],
+         "options": {
+            "colorMode": "none",
+            "graphMode": "area",
+            "justifyMode": "center",
+            "orientation": "auto",
+            "reduceOptions": {
+               "calcs": [
+                  "lastNotNull"
+               ],
+               "fields": "",
+               "values": false
+            },
+            "textMode": "auto"
+         },
+         "pluginVersion": "9.4.7",
+         "targets": [
+            {
+               "datasource": {
+                  "type": "prometheus",
+                  "uid": "${DS_PROMETHEUS}"
+               },
+               "exemplar": false,
+               "expr": "sum(irate(ceph_osd_recovery_ops{cluster=~\"$Cluster\"}[$__interval]))",
+               "format": "time_series",
+               "hide": false,
+               "instant": false,
+               "intervalFactor": 1,
+               "legendFormat": "Write",
+               "range": true,
+               "refId": "A"
+            }
+         ],
+         "title": "Recovery Rate",
+         "transparent": false,
+         "type": "stat"
+      },
+      {
+         "collapse": false,
+         "collapsed": true,
+         "gridPos": {
+            "h": 1,
+            "w": 24,
+            "x": 0,
+            "y": 18
+         },
+         "id": 19,
+         "panels": [
+            {
+               "colors": null,
+               "datasource": "${DS_PROMETHEUS}",
+               "description": "",
+               "fieldConfig": {
+                  "defaults": {
+                     "decimals": 0,
+                     "links": [ ],
+                     "mappings": [ ],
+                     "thresholds": {
+                        "mode": "absolute",
+                        "steps": [
+                           {
+                              "color": "text",
+                              "value": null
+                           }
+                        ]
+                     },
+                     "unit": "none"
+                  },
+                  "overrides": [
+                     {
+                        "matcher": {
+                           "id": "byName",
+                           "options": "Critical"
+                        },
+                        "properties": [
+                           {
+                              "id": "thresholds",
+                              "value": {
+                                 "mode": "absolute",
+                                 "steps": [
+                                    {
+                                       "color": "text",
+                                       "value": null
+                                    },
+                                    {
+                                       "color": "semi-dark-red",
+                                       "value": 1
+                                    }
+                                 ]
+                              }
+                           }
+                        ]
+                     },
+                     {
+                        "matcher": {
+                           "id": "byName",
+                           "options": "Warning"
+                        },
+                        "properties": [
+                           {
+                              "id": "thresholds",
+                              "value": {
+                                 "mode": "absolute",
+                                 "steps": [
+                                    {
+                                       "color": "text",
+                                       "value": null
+                                    },
+                                    {
+                                       "color": "semi-dark-yellow",
+                                       "value": 1
+                                    }
+                                 ]
+                              }
+                           }
+                        ]
+                     }
+                  ]
+               },
+               "gridPos": {
+                  "h": 7,
+                  "w": 5,
+                  "x": 0,
+                  "y": 19
+               },
+               "id": 20,
+               "links": [ ],
+               "options": {
+                  "colorMode": "value",
+                  "graphMode": "area",
+                  "justifyMode": "center",
+                  "orientation": "auto",
+                  "reduceOptions": {
+                     "calcs": [
+                        "lastNotNull"
+                     ],
+                     "fields": "",
+                     "values": false
+                  },
+                  "textMode": "auto"
+               },
+               "pluginVersion": "9.4.7",
+               "targets": [
+                  {
+                     "datasource": "${DS_PROMETHEUS}",
+                     "expr": "count(ALERTS{alertstate=\"firing\",severity=\"critical\", cluster=~\"$Cluster\"}) OR vector(0)",
+                     "format": "time_series",
+                     "instant": true,
+                     "intervalFactor": 1,
+                     "legendFormat": "Critical",
+                     "range": false,
+                     "refId": "A"
+                  },
+                  {
+                     "datasource": "${DS_PROMETHEUS}",
+                     "expr": "count(ALERTS{alertstate=\"firing\",severity=\"warning\", cluster=~\"$Cluster\"}) OR vector(0)",
+                     "format": "time_series",
+                     "instant": true,
+                     "intervalFactor": 1,
+                     "legendFormat": "Warning",
+                     "range": false,
+                     "refId": "B"
+                  }
+               ],
+               "title": "Status",
+               "transparent": false,
+               "type": "stat"
+            },
+            {
+               "columns": [ ],
+               "datasource": "${DS_PROMETHEUS}",
+               "fieldConfig": {
+                  "defaults": {
+                     "custom": {
+                        "align": "auto",
+                        "cellOptions": {
+                           "type": "auto"
+                        },
+                        "filterable": true,
+                        "inspect": false
+                     },
+                     "thresholds": {
+                        "mode": "absolute",
+                        "steps": [
+                           {
+                              "color": "green"
+                           },
+                           {
+                              "color": "red",
+                              "value": 80
+                           }
+                        ]
+                     }
+                  },
+                  "overrides": [ ]
+               },
+               "gridPos": {
+                  "h": 7,
+                  "w": 19,
+                  "x": 5,
+                  "y": 19
+               },
+               "id": 21,
+               "links": [ ],
+               "options": {
+                  "footer": {
+                     "countRows": false,
+                     "enablePagination": false,
+                     "fields": "",
+                     "reducer": [
+                        "sum"
+                     ],
+                     "show": false
+                  },
+                  "frameIndex": 1,
+                  "showHeader": true,
+                  "sortBy": [
+                     {
+                        "desc": false,
+                        "displayName": "Severity"
+                     }
+                  ]
+               },
+               "pluginVersion": "9.4.7",
+               "styles": "",
+               "targets": [
+                  {
+                     "datasource": {
+                        "type": "prometheus",
+                        "uid": "${DS_PROMETHEUS}"
+                     },
+                     "exemplar": false,
+                     "expr": "ALERTS{alertstate=\"firing\", cluster=~\"$Cluster\"}",
+                     "format": "table",
+                     "hide": false,
+                     "instant": true,
+                     "interval": "",
+                     "intervalFactor": 1,
+                     "legendFormat": "__auto",
+                     "range": false,
+                     "refId": "A"
+                  }
+               ],
+               "timeFrom": null,
+               "timeShift": null,
+               "title": "Alerts",
+               "transformations": [
+                  {
+                     "id": "joinByField",
+                     "options": {
+                        "byField": "cluster",
+                        "mode": "outer"
+                     }
+                  },
+                  {
+                     "id": "organize",
+                     "options": {
+                        "excludeByName": {
+                           "Time": true,
+                           "Value": true,
+                           "__name__": true,
+                           "instance": true,
+                           "job": true,
+                           "oid": true,
+                           "replica": true,
+                           "type": true
+                        },
+                        "indexByName": {
+                           "Time": 0,
+                           "Value": 9,
+                           "__name__": 1,
+                           "alertname": 2,
+                           "alertstate": 4,
+                           "cluster": 3,
+                           "instance": 6,
+                           "job": 7,
+                           "severity": 5,
+                           "type": 8
+                        },
+                        "renameByName": {
+                           "alertname": "Name",
+                           "alertstate": "State",
+                           "cluster": "Cluster",
+                           "severity": "Severity"
+                        }
+                     }
+                  }
+               ],
+               "type": "table"
+            },
+            {
+               "datasource": {
+                  "type": "datasource",
+                  "uid": "grafana"
+               },
+               "gridPos": {
+                  "h": 8,
+                  "w": 24,
+                  "x": 0,
+                  "y": 26
+               },
+               "id": 22,
+               "limit": 10,
+               "onlyAlertsOnDashboard": true,
+               "options": {
+                  "alertName": "",
+                  "dashboardAlerts": false,
+                  "groupBy": [ ],
+                  "groupMode": "default",
+                  "maxItems": 20,
+                  "sortOrder": 1,
+                  "stateFilter": {
+                     "error": true,
+                     "firing": true,
+                     "noData": false,
+                     "normal": false,
+                     "pending": true
+                  },
+                  "viewMode": "list"
+               },
+               "show": "current",
+               "sortOrder": 1,
+               "stateFilter": [ ],
+               "title": "Alerts(Grouped)",
+               "type": "alertlist"
+            }
+         ],
+         "repeat": null,
+         "repeatIteration": null,
+         "repeatRowId": null,
+         "showTitle": true,
+         "title": "Alerts",
+         "titleSize": "h6",
+         "type": "row"
+      },
+      {
+         "collapse": false,
+         "collapsed": true,
+         "gridPos": {
+            "h": 1,
+            "w": 24,
+            "x": 0,
+            "y": 29
+         },
+         "id": 23,
+         "panels": [
+            {
+               "datasource": "${DS_PROMETHEUS}",
+               "fieldConfig": {
+                  "defaults": {
+                     "color": {
+                        "mode": "palette-classic"
+                     },
+                     "custom": {
+                        "axisCenteredZero": false,
+                        "axisColorMode": "text",
+                        "axisLabel": "",
+                        "axisPlacement": "auto",
+                        "barAlignment": 0,
+                        "drawStyle": "line",
+                        "fillOpacity": 0,
+                        "gradientMode": "none",
+                        "hideFrom": {
+                           "legend": false,
+                           "tooltip": false,
+                           "viz": false
+                        },
+                        "lineInterpolation": "linear",
+                        "lineWidth": 1,
+                        "pointSize": 5,
+                        "scaleDistribution": {
+                           "type": "linear"
+                        },
+                        "showPoints": "auto",
+                        "spanNulls": false,
+                        "stacking": {
+                           "group": "A",
+                           "mode": "none"
+                        },
+                        "thresholdsStyle": {
+                           "mode": "off"
+                        }
+                     },
+                     "decimals": 2,
+                     "thresholds": {
+                        "mode": "percentage",
+                        "steps": [
+                           {
+                              "color": "green"
+                           }
+                        ]
+                     },
+                     "unit": "percentunit"
+                  },
+                  "overrides": [ ]
+               },
+               "gridPos": {
+                  "h": 7,
+                  "w": 8,
+                  "x": 0,
+                  "y": 30
+               },
+               "id": 24,
+               "options": {
+                  "legend": {
+                     "calcs": [
+                        "last"
+                     ],
+                     "displayMode": "table",
+                     "placement": "bottom",
+                     "showLegend": true,
+                     "sortBy": "Last",
+                     "sortDesc": true
+                  },
+                  "tooltip": {
+                     "mode": "multi",
+                     "sort": "desc"
+                  }
+               },
+               "pluginVersion": "9.1.3",
+               "targets": [
+                  {
+                     "datasource": "${DS_PROMETHEUS}",
+                     "expr": "topk(5, ceph_cluster_total_used_bytes/ceph_cluster_total_bytes)",
+                     "format": "time_series",
+                     "instant": false,
+                     "intervalFactor": 1,
+                     "legendFormat": "{{cluster}}",
+                     "range": true,
+                     "refId": "A",
+                     "step": 300
+                  }
+               ],
+               "title": "Top 5 - Capacity Utilization(%)",
+               "type": "timeseries"
+            },
+            {
+               "datasource": "${DS_PROMETHEUS}",
+               "fieldConfig": {
+                  "defaults": {
+                     "color": {
+                        "mode": "palette-classic"
+                     },
+                     "custom": {
+                        "axisCenteredZero": false,
+                        "axisColorMode": "text",
+                        "axisLabel": "",
+                        "axisPlacement": "auto",
+                        "barAlignment": 0,
+                        "drawStyle": "line",
+                        "fillOpacity": 0,
+                        "gradientMode": "none",
+                        "hideFrom": {
+                           "legend": false,
+                           "tooltip": false,
+                           "viz": false
+                        },
+                        "lineInterpolation": "linear",
+                        "lineWidth": 1,
+                        "pointSize": 5,
+                        "scaleDistribution": {
+                           "type": "linear"
+                        },
+                        "showPoints": "auto",
+                        "spanNulls": false,
+                        "stacking": {
+                           "group": "A",
+                           "mode": "none"
+                        },
+                        "thresholdsStyle": {
+                           "mode": "off"
+                        }
+                     },
+                     "decimals": 2,
+                     "thresholds": {
+                        "mode": "percentage",
+                        "steps": [
+                           {
+                              "color": "green"
+                           }
+                        ]
+                     },
+                     "unit": "ops"
+                  },
+                  "overrides": [ ]
+               },
+               "gridPos": {
+                  "h": 7,
+                  "w": 8,
+                  "x": 8,
+                  "y": 30
+               },
+               "id": 25,
+               "options": {
+                  "legend": {
+                     "calcs": [
+                        "last"
+                     ],
+                     "displayMode": "table",
+                     "placement": "bottom",
+                     "showLegend": true,
+                     "sortBy": "Last",
+                     "sortDesc": true
+                  },
+                  "tooltip": {
+                     "mode": "multi",
+                     "sort": "desc"
+                  }
+               },
+               "pluginVersion": "9.1.3",
+               "targets": [
+                  {
+                     "datasource": "${DS_PROMETHEUS}",
+                     "expr": "topk(10, sum by (cluster) (irate(ceph_osd_op_w[$__interval]))  \n+ sum by (cluster) (irate(ceph_osd_op_r[$__interval])) )",
+                     "format": "time_series",
+                     "instant": false,
+                     "intervalFactor": 1,
+                     "legendFormat": "{{cluster}}",
+                     "range": true,
+                     "refId": "A",
+                     "step": 300
+                  }
+               ],
+               "title": "Top 5 - Cluster IOPS",
+               "type": "timeseries"
+            },
+            {
+               "datasource": "${DS_PROMETHEUS}",
+               "fieldConfig": {
+                  "defaults": {
+                     "color": {
+                        "mode": "palette-classic"
+                     },
+                     "custom": {
+                        "axisCenteredZero": false,
+                        "axisColorMode": "text",
+                        "axisLabel": "",
+                        "axisPlacement": "auto",
+                        "barAlignment": 0,
+                        "drawStyle": "line",
+                        "fillOpacity": 0,
+                        "gradientMode": "none",
+                        "hideFrom": {
+                           "legend": false,
+                           "tooltip": false,
+                           "viz": false
+                        },
+                        "lineInterpolation": "linear",
+                        "lineWidth": 1,
+                        "pointSize": 5,
+                        "scaleDistribution": {
+                           "type": "linear"
+                        },
+                        "showPoints": "auto",
+                        "spanNulls": false,
+                        "stacking": {
+                           "group": "A",
+                           "mode": "none"
+                        },
+                        "thresholdsStyle": {
+                           "mode": "off"
+                        }
+                     },
+                     "decimals": 2,
+                     "thresholds": {
+                        "mode": "absolute",
+                        "steps": [
+                           {
+                              "color": "green"
+                           }
+                        ]
+                     },
+                     "unit": "percentunit"
+                  },
+                  "overrides": [ ]
+               },
+               "gridPos": {
+                  "h": 7,
+                  "w": 8,
+                  "x": 16,
+                  "y": 30
+               },
+               "id": 26,
+               "options": {
+                  "legend": {
+                     "calcs": [
+                        "last"
+                     ],
+                     "displayMode": "table",
+                     "placement": "bottom",
+                     "showLegend": true,
+                     "sortBy": "Last",
+                     "sortDesc": true
+                  },
+                  "tooltip": {
+                     "mode": "multi",
+                     "sort": "desc"
+                  }
+               },
+               "pluginVersion": "9.1.3",
+               "targets": [
+                  {
+                     "datasource": "${DS_PROMETHEUS}",
+                     "expr": "topk(10, ceph_pool_bytes_used{cluster=~\"$Cluster\"}/ceph_pool_max_avail{cluster=~\"$Cluster\"} * on(pool_id, cluster) group_left(instance, name) ceph_pool_metadata{cluster=~\"$Cluster\"})",
+                     "format": "time_series",
+                     "instant": false,
+                     "intervalFactor": 1,
+                     "legendFormat": "{{cluster}} - {{name}}",
+                     "range": true,
+                     "refId": "A",
+                     "step": 300
+                  }
+               ],
+               "title": "Top 10 - Capacity Utilization(%) by Pool",
+               "type": "timeseries"
+            }
+         ],
+         "repeat": null,
+         "repeatIteration": null,
+         "repeatRowId": null,
+         "showTitle": true,
+         "title": "Cluster Stats",
+         "titleSize": "h6",
+         "type": "row"
+      }
+   ],
+   "refresh": "30s",
+   "rows": [ ],
+   "schemaVersion": 22,
+   "style": "dark",
+   "tags": [
+      "ceph-mixin"
+   ],
+   "templating": {
+      "list": [
+         {
+            "current": {
+               "text": "default",
+               "value": "default"
+            },
+            "hide": 0,
+            "label": "Data Source",
+            "name": "DS_PROMETHEUS",
+            "options": [ ],
+            "query": "prometheus",
+            "refresh": 1,
+            "regex": "",
+            "type": "datasource"
+         },
+         {
+            "allValue": null,
+            "current": {
+               "text": "All",
+               "value": "All"
+            },
+            "datasource": "$DS_PROMETHEUS",
+            "hide": 0,
+            "includeAll": true,
+            "label": null,
+            "multi": false,
+            "name": "Cluster",
+            "options": [ ],
+            "query": "label_values(ceph_health_status, cluster)",
+            "refresh": 2,
+            "regex": "",
+            "sort": 0,
+            "tagValuesQuery": "",
+            "tags": [ ],
+            "tagsQuery": "",
+            "type": "query",
+            "useTags": false
+         }
+      ]
+   },
+   "time": {
+      "from": "now-1h",
+      "to": "now"
+   },
+   "timepicker": {
+      "refresh_intervals": [
+         "5s",
+         "10s",
+         "30s",
+         "1m",
+         "5m",
+         "15m",
+         "30m",
+         "1h",
+         "2h",
+         "1d"
+      ],
+      "time_options": [
+         "5m",
+         "15m",
+         "1h",
+         "6h",
+         "12h",
+         "24h",
+         "2d",
+         "7d",
+         "30d"
+      ]
+   },
+   "timezone": "",
+   "title": "Ceph - Multi-cluster",
+   "uid": "BnxelG7Sz",
+   "version": 0
+}
index d7acec22bebbf62049205d90a3a7c1838d5e07b8..c918c2ec3c2407c701069c81296ad52a4d8fb586 100644 (file)
@@ -1,6 +1,8 @@
 # -*- coding: utf-8 -*-
 
+import base64
 import json
+import time
 
 import requests
 
@@ -8,8 +10,8 @@ from ..exceptions import DashboardException
 from ..security import Scope
 from ..settings import Settings
 from ..tools import configure_cors
-from . import APIDoc, APIRouter, CreatePermission, Endpoint, EndpointDoc, \
-    ReadPermission, RESTController, UIRouter, UpdatePermission
+from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \
+    EndpointDoc, ReadPermission, RESTController, UIRouter, UpdatePermission
 
 
 @APIRouter('/multi-cluster', Scope.CONFIG_OPT)
@@ -17,6 +19,8 @@ from . import APIDoc, APIRouter, CreatePermission, Endpoint, EndpointDoc, \
 class MultiCluster(RESTController):
     def _proxy(self, method, base_url, path, params=None, payload=None, verify=False,
                token=None):
+        if not base_url.endswith('/'):
+            base_url = base_url + '/'
         try:
             if token:
                 headers = {
@@ -48,12 +52,7 @@ class MultiCluster(RESTController):
     @CreatePermission
     @EndpointDoc("Authenticate to a remote cluster")
     def auth(self, url: str, cluster_alias: str, username=None,
-             password=None, token=None, hub_url=None):
-
-        multi_cluster_config = self.load_multi_cluster_config()
-
-        if not url.endswith('/'):
-            url = url + '/'
+             password=None, token=None, hub_url=None, cluster_fsid=None):
 
         if username and password:
             payload = {
@@ -67,41 +66,40 @@ class MultiCluster(RESTController):
                     http_status_code=400,
                     component='dashboard')
 
-            token = content['token']
+            cluster_token = content['token']
 
-        if token:
             self._proxy('PUT', url, 'ui-api/multi-cluster/set_cors_endpoint',
-                        payload={'url': hub_url}, token=token)
-            fsid = self._proxy('GET', url, 'api/health/get_cluster_fsid', token=token)
-            content = self._proxy('POST', url, 'api/auth/check', payload={'token': token},
-                                  token=token)
-            if 'username' in content:
-                username = content['username']
-
-            if 'config' not in multi_cluster_config:
-                multi_cluster_config['config'] = {}
-
-            if fsid in multi_cluster_config['config']:
-                existing_entries = multi_cluster_config['config'][fsid]
-                if not any(entry['user'] == username for entry in existing_entries):
-                    existing_entries.append({
-                        "name": fsid,
-                        "url": url,
-                        "cluster_alias": cluster_alias,
-                        "user": username,
-                        "token": token,
-                    })
-            else:
-                multi_cluster_config['current_user'] = username
-                multi_cluster_config['config'][fsid] = [{
+                        payload={'url': hub_url}, token=cluster_token)
+
+            fsid = self._proxy('GET', url, 'api/health/get_cluster_fsid', token=cluster_token)
+
+            self.set_multi_cluster_config(fsid, username, url, cluster_alias, cluster_token)
+
+        if token and cluster_fsid and username:
+            self.set_multi_cluster_config(cluster_fsid, username, url, cluster_alias, token)
+
+    def set_multi_cluster_config(self, fsid, username, url, cluster_alias, token):
+        multi_cluster_config = self.load_multi_cluster_config()
+        if fsid in multi_cluster_config['config']:
+            existing_entries = multi_cluster_config['config'][fsid]
+            if not any(entry['user'] == username for entry in existing_entries):
+                existing_entries.append({
                     "name": fsid,
                     "url": url,
                     "cluster_alias": cluster_alias,
                     "user": username,
                     "token": token,
-                }]
-
-            Settings.MULTICLUSTER_CONFIG = multi_cluster_config
+                })
+        else:
+            multi_cluster_config['current_user'] = username
+            multi_cluster_config['config'][fsid] = [{
+                "name": fsid,
+                "url": url,
+                "cluster_alias": cluster_alias,
+                "user": username,
+                "token": token,
+            }]
+        Settings.MULTICLUSTER_CONFIG = multi_cluster_config
 
     def load_multi_cluster_config(self):
         if isinstance(Settings.MULTICLUSTER_CONFIG, str):
@@ -124,13 +122,71 @@ class MultiCluster(RESTController):
         Settings.MULTICLUSTER_CONFIG = multicluster_config
         return Settings.MULTICLUSTER_CONFIG
 
-    @Endpoint('POST')
+    @Endpoint('PUT')
     @CreatePermission
-    # pylint: disable=R0911
-    def verify_connection(self, url: str, username=None, password=None, token=None):
-        if not url.endswith('/'):
-            url = url + '/'
+    # pylint: disable=unused-variable
+    def reconnect_cluster(self, url: str, username=None, password=None, token=None):
+        multicluster_config = self.load_multi_cluster_config()
+        if username and password:
+            payload = {
+                'username': username,
+                'password': password
+            }
+            content = self._proxy('POST', url, 'api/auth', payload=payload)
+            if 'token' not in content:
+                raise DashboardException(
+                    "Could not authenticate to remote cluster",
+                    http_status_code=400,
+                    component='dashboard')
 
+            token = content['token']
+
+        if username and token:
+            if "config" in multicluster_config:
+                for key, cluster_details in multicluster_config["config"].items():
+                    for cluster in cluster_details:
+                        if cluster["url"] == url and cluster["user"] == username:
+                            cluster['token'] = token
+            Settings.MULTICLUSTER_CONFIG = multicluster_config
+        return Settings.MULTICLUSTER_CONFIG
+
+    @Endpoint('PUT')
+    @UpdatePermission
+    # pylint: disable=unused-variable
+    def edit_cluster(self, url, cluster_alias, username):
+        multicluster_config = self.load_multi_cluster_config()
+        if "config" in multicluster_config:
+            for key, cluster_details in multicluster_config["config"].items():
+                for cluster in cluster_details:
+                    if cluster["url"] == url and cluster["user"] == username:
+                        cluster['cluster_alias'] = cluster_alias
+        Settings.MULTICLUSTER_CONFIG = multicluster_config
+        return Settings.MULTICLUSTER_CONFIG
+
+    @Endpoint(method='DELETE')
+    @DeletePermission
+    def delete_cluster(self, cluster_name, cluster_user):
+        multicluster_config = self.load_multi_cluster_config()
+        if "config" in multicluster_config:
+            keys_to_remove = []
+            for key, cluster_details in multicluster_config["config"].items():
+                cluster_details_copy = list(cluster_details)
+                for cluster in cluster_details_copy:
+                    if cluster["name"] == cluster_name and cluster["user"] == cluster_user:
+                        cluster_details.remove(cluster)
+                        if not cluster_details:
+                            keys_to_remove.append(key)
+
+            for key in keys_to_remove:
+                del multicluster_config["config"][key]
+
+        Settings.MULTICLUSTER_CONFIG = multicluster_config
+        return Settings.MULTICLUSTER_CONFIG
+
+    @Endpoint()
+    @ReadPermission
+    # pylint: disable=R0911
+    def verify_connection(self, url=None, username=None, password=None, token=None):
         if token:
             try:
                 payload = {
@@ -172,6 +228,37 @@ class MultiCluster(RESTController):
     def get_config(self):
         return Settings.MULTICLUSTER_CONFIG
 
+    def is_token_expired(self, jwt_token):
+        split_message = jwt_token.split(".")
+        base64_message = split_message[1]
+        decoded_token = json.loads(base64.urlsafe_b64decode(base64_message + "===="))
+        expiration_time = decoded_token['exp']
+        current_time = time.time()
+        return expiration_time < current_time
+
+    def check_token_status_expiration(self, token):
+        if self.is_token_expired(token):
+            return 1
+        return 0
+
+    def check_token_status_array(self, clusters_token_array):
+        token_status_map = {}
+
+        for item in clusters_token_array:
+            cluster_name = item['name']
+            token = item['token']
+            user = item['user']
+            status = self.check_token_status_expiration(token)
+            token_status_map[cluster_name] = {'status': status, 'user': user}
+
+        return token_status_map
+
+    @Endpoint()
+    @ReadPermission
+    def check_token_status(self, clustersTokenMap=None):
+        clusters_token_map = json.loads(clustersTokenMap)
+        return self.check_token_status_array(clusters_token_map)
+
 
 @UIRouter('/multi-cluster', Scope.CONFIG_OPT)
 class MultiClusterUi(RESTController):
index 48224c844d47bee01a8c1af0a7f76123b2cc8991..c54681b065f27043e7f4a70a05023ca10e9b00f6 100644 (file)
@@ -49,6 +49,7 @@ import { UpgradeComponent } from './ceph/cluster/upgrade/upgrade.component';
 import { CephfsVolumeFormComponent } from './ceph/cephfs/cephfs-form/cephfs-form.component';
 import { UpgradeProgressComponent } from './ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component';
 import { MultiClusterComponent } from './ceph/cluster/multi-cluster/multi-cluster.component';
+import { MultiClusterListComponent } from './ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component';
 
 @Injectable()
 export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
@@ -187,7 +188,22 @@ const routes: Routes = [
       },
       {
         path: 'multi-cluster',
-        component: MultiClusterComponent
+        children: [
+          {
+            path: 'overview',
+            component: MultiClusterComponent,
+            data: {
+              breadcrumbs: 'Multi-Cluster/Overview'
+            }
+          },
+          {
+            path: 'manage-clusters',
+            component: MultiClusterListComponent,
+            data: {
+              breadcrumbs: 'Multi-Cluster/Manage Clusters'
+            }
+          }
+        ]
       },
       {
         path: 'inventory',
index b1eb9275a462ceb84157758a84aa3fcff5711d1c..2f0734885d857b40a63a0427a6fe8d6903543073 100644 (file)
@@ -63,6 +63,7 @@ import { UpgradeStartModalComponent } from './upgrade/upgrade-form/upgrade-start
 import { UpgradeProgressComponent } from './upgrade/upgrade-progress/upgrade-progress.component';
 import { MultiClusterComponent } from './multi-cluster/multi-cluster.component';
 import { MultiClusterFormComponent } from './multi-cluster/multi-cluster-form/multi-cluster-form.component';
+import { MultiClusterListComponent } from './multi-cluster/multi-cluster-list/multi-cluster-list.component';
 
 @NgModule({
   imports: [
@@ -128,7 +129,8 @@ import { MultiClusterFormComponent } from './multi-cluster/multi-cluster-form/mu
     UpgradeStartModalComponent,
     UpgradeProgressComponent,
     MultiClusterComponent,
-    MultiClusterFormComponent
+    MultiClusterFormComponent,
+    MultiClusterListComponent
   ],
   providers: [NgbActiveModal]
 })
index cc9ed7453fc4d094e0509780a94a501071e8eb01..c875557306a855f3292b40c3825ff38cadea8423 100644 (file)
@@ -1,6 +1,6 @@
 <cd-modal [modalRef]="activeModal">
   <ng-container i18n="form title"
-                class="modal-title">Connect Cluster
+                class="modal-title">{{ action | titlecase }} Cluster
   </ng-container>
   <ng-container class="modal-content">
     <form name="remoteClusterForm"
                   *ngIf="remoteClusterForm.showError('clusterAlias', frm, 'required')"
                   i18n>This field is required.
             </span>
+            <span class="invalid-feedback"
+                  *ngIf="remoteClusterForm.showError('clusterAlias', frm, 'uniqueName')"
+                  i18n>The chosen alias name is already in use.
+            </span>
           </div>
         </div>
         <div class="form-group row"
-             *ngIf="!remoteClusterForm.getValue('showToken') && !showCrossOriginError">
+             *ngIf="action !== 'edit'">
           <label class="cd-col-form-label required"
-                 for="apiToken"
+                 for="username"
                  i18n>Username
           </label>
           <div class="cd-col-form-input">
                   *ngIf="remoteClusterForm.showError('username', frm, 'required')"
                   i18n>This field is required.
             </span>
+            <span class="invalid-feedback"
+                  *ngIf="remoteClusterForm.showError('username', frm, 'uniqueUrlandUser')"
+                  i18n>A cluster with the chosen user is already connected.
+            </span>
+          </div>
+        </div>
+        <div class="form-group row"
+             *ngIf="remoteClusterForm.getValue('showToken') && action !== 'edit'">
+          <label class="cd-col-form-label required"
+                 for="clusterFsid"
+                 i18n>Cluster FSID
+          </label>
+          <div class="cd-col-form-input">
+            <input id="clusterFsid"
+                   name="clusterFsid"
+                   class="form-control"
+                   type="text"
+                   formControlName="clusterFsid">
+            <span class="invalid-feedback"
+                  *ngIf="remoteClusterForm.showError('clusterFsid', frm, 'required')"
+                  i18n>This field is required.
+            </span>
           </div>
         </div>
         <div class="form-group row"
-             *ngIf="!remoteClusterForm.getValue('showToken') && !showCrossOriginError">
+             *ngIf="!remoteClusterForm.getValue('showToken') && !showCrossOriginError && action !== 'edit'">
           <label class="cd-col-form-label required"
                  for="password"
                  i18n>Password
             </span>
           </div>
         </div>
-        <div class="form-group row">
-          <div class="cd-col-form-offset">
-            <div class="custom-control custom-checkbox">
-              <input class="custom-control-input"
-                     id="showToken"
-                     type="checkbox"
-                     (click)="showToken = !showToken"
-                     formControlName="showToken"
-                     [readonly]="true">
-              <label class="custom-control-label"
-                     for="showToken"
-                     i18n>Auth with token</label>
-            </div>
-          </div>
-        </div>
         <div class="form-group row"
-             *ngIf="remoteClusterForm.getValue('showToken')">
+             *ngIf="remoteClusterForm.getValue('showToken') && action !== 'edit'">
           <label class="cd-col-form-label required"
                  for="apiToken"
                  i18n>Token
           </div>
         </div>
         <div class="form-group row"
-             *ngIf="!showCrossOriginError">
+             *ngIf="action !== 'edit'">
+          <div class="cd-col-form-offset">
+            <div class="custom-control custom-checkbox">
+              <input class="custom-control-input"
+                     id="showToken"
+                     type="checkbox"
+                     [checked]="showToken"
+                     (change)="toggleToken()"
+                     formControlName="showToken">
+              <label class="custom-control-label"
+                     for="showToken"
+                     i18n>Auth with token</label>
+            </div>
+          </div>
+        </div>
+        <div class="form-group row"
+             *ngIf="!showCrossOriginError && action !== 'edit' && !remoteClusterForm.getValue('showToken')">
           <div class="cd-col-form-offset">
             <div class="custom-control">
               <button class="btn btn-primary"
       </div>
       <div class="modal-footer">
         <cd-form-button-panel (submitActionEvent)="onSubmit()"
-                              [submitText]="actionLabels.CONNECT"
-                              [disabled]="!connectionVerified && !showCrossOriginError"
+                              [submitText]="(action | titlecase) + ' ' + 'Cluster'"
                               [form]="remoteClusterForm">
         </cd-form-button-panel>
       </div>
index 473a49dab7f79702f4dae91d4a9febeeca28960e..83eb9fb5d51ee8b18f5a76a22f3182d5641cf02c 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
 import { FormControl, Validators } from '@angular/forms';
 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
@@ -8,6 +8,7 @@ import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
 import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { MultiCluster } from '~/app/shared/models/multi-cluster';
 import { NotificationService } from '~/app/shared/services/notification.service';
 
 @Component({
@@ -16,6 +17,8 @@ import { NotificationService } from '~/app/shared/services/notification.service'
   styleUrls: ['./multi-cluster-form.component.scss']
 })
 export class MultiClusterFormComponent implements OnInit, OnDestroy {
+  @Output()
+  submitAction = new EventEmitter();
   readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,5}\/?$/;
   readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
   readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
@@ -26,6 +29,13 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy {
   private subs = new Subscription();
   showCrossOriginError = false;
   crossOriginCmd: string;
+  action: string;
+  cluster: MultiCluster;
+  clustersData: MultiCluster[];
+  clusterAliasNames: string[];
+  clusterUrls: string[];
+  clusterUsers: string[];
+  clusterUrlUserMap: Map<string, string>;
 
   constructor(
     public activeModal: NgbActiveModal,
@@ -35,21 +45,57 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy {
   ) {
     this.createForm();
   }
-  ngOnInit(): void {}
+  ngOnInit(): void {
+    if (this.action === 'edit') {
+      this.remoteClusterForm.get('remoteClusterUrl').setValue(this.cluster.url);
+      this.remoteClusterForm.get('remoteClusterUrl').disable();
+      this.remoteClusterForm.get('clusterAlias').setValue(this.cluster.cluster_alias);
+    }
+    if (this.action === 'reconnect') {
+      this.remoteClusterForm.get('remoteClusterUrl').setValue(this.cluster.url);
+      this.remoteClusterForm.get('remoteClusterUrl').disable();
+      this.remoteClusterForm.get('clusterAlias').setValue(this.cluster.cluster_alias);
+      this.remoteClusterForm.get('clusterAlias').disable();
+      this.remoteClusterForm.get('username').setValue(this.cluster.user);
+      this.remoteClusterForm.get('username').disable();
+      this.remoteClusterForm.get('clusterFsid').setValue(this.cluster.name);
+      this.remoteClusterForm.get('clusterFsid').disable();
+    }
+    [this.clusterAliasNames, this.clusterUrls, this.clusterUsers] = [
+      'cluster_alias',
+      'url',
+      'user'
+    ].map((prop) => this.clustersData?.map((cluster) => cluster[prop]));
+  }
 
   createForm() {
     this.remoteClusterForm = new CdFormGroup({
       showToken: new FormControl(false),
       username: new FormControl('', [
-        CdValidators.requiredIf({
-          showToken: false
+        CdValidators.custom('uniqueUrlandUser', (username: string) => {
+          let remoteClusterUrl = '';
+          if (
+            this.remoteClusterForm &&
+            this.remoteClusterForm.getValue('remoteClusterUrl') &&
+            this.remoteClusterForm.getValue('remoteClusterUrl').endsWith('/')
+          ) {
+            remoteClusterUrl = this.remoteClusterForm.getValue('remoteClusterUrl').slice(0, -1);
+          } else if (this.remoteClusterForm) {
+            remoteClusterUrl = this.remoteClusterForm.getValue('remoteClusterUrl');
+          }
+          return (
+            this.remoteClusterForm &&
+            this.clusterUrls?.includes(remoteClusterUrl) &&
+            this.clusterUsers?.includes(username)
+          );
         })
       ]),
-      password: new FormControl('', [
+      clusterFsid: new FormControl('', [
         CdValidators.requiredIf({
-          showToken: false
+          showToken: true
         })
       ]),
+      password: new FormControl('', []),
       remoteClusterUrl: new FormControl(null, {
         validators: [
           CdValidators.custom('endpoint', (value: string) => {
@@ -71,8 +117,17 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy {
           showToken: true
         })
       ]),
-      clusterAlias: new FormControl('', {
-        validators: [Validators.required]
+      clusterAlias: new FormControl(null, {
+        validators: [
+          Validators.required,
+          CdValidators.custom('uniqueName', (clusterAlias: string) => {
+            return (
+              (this.action === 'connect' || this.action === 'edit') &&
+              this.clusterAliasNames &&
+              this.clusterAliasNames.indexOf(clusterAlias) !== -1
+            );
+          })
+        ]
       })
     });
   }
@@ -83,27 +138,78 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy {
 
   onSubmit() {
     const url = this.remoteClusterForm.getValue('remoteClusterUrl');
+    const updatedUrl = url.endsWith('/') ? url.slice(0, -1) : url;
     const clusterAlias = this.remoteClusterForm.getValue('clusterAlias');
     const username = this.remoteClusterForm.getValue('username');
     const password = this.remoteClusterForm.getValue('password');
     const token = this.remoteClusterForm.getValue('apiToken');
+    const clusterFsid = this.remoteClusterForm.getValue('clusterFsid');
 
-    this.subs.add(
-      this.multiClusterService
-        .addCluster(url, clusterAlias, username, password, token, window.location.origin)
-        .subscribe({
+    if (this.action === 'edit') {
+      this.subs.add(
+        this.multiClusterService
+          .editCluster(this.cluster.url, clusterAlias, this.cluster.user)
+          .subscribe({
+            error: () => {
+              this.remoteClusterForm.setErrors({ cdSubmitButton: true });
+            },
+            complete: () => {
+              this.notificationService.show(
+                NotificationType.success,
+                $localize`Cluster updated successfully`
+              );
+              this.submitAction.emit();
+              this.activeModal.close();
+            }
+          })
+      );
+    }
+
+    if (this.action === 'reconnect') {
+      this.subs.add(
+        this.multiClusterService.reConnectCluster(updatedUrl, username, password, token).subscribe({
           error: () => {
             this.remoteClusterForm.setErrors({ cdSubmitButton: true });
           },
           complete: () => {
             this.notificationService.show(
               NotificationType.success,
-              $localize`Cluster added successfully`
+              $localize`Cluster reconnected successfully`
             );
+            this.submitAction.emit();
             this.activeModal.close();
           }
         })
-    );
+      );
+    }
+
+    if (this.action === 'connect') {
+      this.subs.add(
+        this.multiClusterService
+          .addCluster(
+            updatedUrl,
+            clusterAlias,
+            username,
+            password,
+            token,
+            window.location.origin,
+            clusterFsid
+          )
+          .subscribe({
+            error: () => {
+              this.remoteClusterForm.setErrors({ cdSubmitButton: true });
+            },
+            complete: () => {
+              this.notificationService.show(
+                NotificationType.success,
+                $localize`Cluster connected successfully`
+              );
+              this.submitAction.emit();
+              this.activeModal.close();
+            }
+          })
+      );
+    }
   }
 
   verifyConnection() {
@@ -149,4 +255,8 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy {
         })
     );
   }
+
+  toggleToken() {
+    this.showToken = !this.showToken;
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html
new file mode 100644 (file)
index 0000000..7aea2f4
--- /dev/null
@@ -0,0 +1,37 @@
+<nav ngbNav
+     #nav="ngbNav"
+     class="nav-tabs">
+  <ng-container ngbNavItem>
+    <a ngbNavLink
+       i18n>Clusters List</a>
+    <ng-template ngbNavContent>
+      <cd-table #table
+                [data]="data"
+                [columns]="columns"
+                columnMode="flex"
+                selectionType="single"
+                [maxLimit]="25"
+                (updateSelection)="updateSelection($event)">
+        <div class="table-actions btn-toolbar">
+          <cd-table-actions [permission]="permissions.user"
+                            [selection]="selection"
+                            class="btn-group"
+                            id="cluster-actions"
+                            [tableActions]="tableActions">
+          </cd-table-actions>
+        </div>
+      </cd-table>
+    </ng-template>
+  </ng-container>
+</nav>
+
+<ng-template #urlTpl
+             let-row="row">
+  <a target="_blank"
+     [href]="row.url">
+      {{ row.url.endsWith('/') ? row.url.slice(0, -1) : row.url }}
+    <i class="fa fa-external-link"></i>
+  </a>
+</ng-template>
+
+<div [ngbNavOutlet]="nav"></div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.spec.ts
new file mode 100644 (file)
index 0000000..d69b3a4
--- /dev/null
@@ -0,0 +1,30 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule } from 'ngx-toastr';
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { MultiClusterListComponent } from './multi-cluster-list.component';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('MultiClusterListComponent', () => {
+  let component: MultiClusterListComponent;
+  let fixture: ComponentFixture<MultiClusterListComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule, ToastrModule.forRoot(), NgbNavModule, SharedModule],
+      declarations: [MultiClusterListComponent],
+      providers: [CdDatePipe, TableActionsComponent]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(MultiClusterListComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.ts
new file mode 100644 (file)
index 0000000..4496b3a
--- /dev/null
@@ -0,0 +1,213 @@
+import { Component, TemplateRef, ViewChild } from '@angular/core';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { MultiClusterService } from '~/app/shared/api/multi-cluster.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { MultiClusterFormComponent } from '../multi-cluster-form/multi-cluster-form.component';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { Permissions } from '~/app/shared/models/permissions';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { MultiCluster } from '~/app/shared/models/multi-cluster';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { Router } from '@angular/router';
+
+@Component({
+  selector: 'cd-multi-cluster-list',
+  templateUrl: './multi-cluster-list.component.html',
+  styleUrls: ['./multi-cluster-list.component.scss']
+})
+export class MultiClusterListComponent {
+  @ViewChild(TableComponent)
+  table: TableComponent;
+  @ViewChild('urlTpl', { static: true })
+  public urlTpl: TemplateRef<any>;
+
+  permissions: Permissions;
+  tableActions: CdTableAction[];
+  clusterTokenStatus: object = {};
+  columns: Array<CdTableColumn> = [];
+  data: any;
+  selection = new CdTableSelection();
+  bsModalRef: NgbModalRef;
+  clustersTokenMap: Map<string, string> = new Map<string, string>();
+  newData: any;
+  modalRef: NgbModalRef;
+
+  constructor(
+    private multiClusterService: MultiClusterService,
+    private router: Router,
+    private summaryService: SummaryService,
+    public actionLabels: ActionLabelsI18n,
+    private notificationService: NotificationService,
+    private authStorageService: AuthStorageService,
+    private modalService: ModalService
+  ) {
+    this.tableActions = [
+      {
+        permission: 'create',
+        icon: Icons.add,
+        name: this.actionLabels.CONNECT,
+        click: () => this.openRemoteClusterInfoModal('connect')
+      },
+      {
+        permission: 'update',
+        icon: Icons.edit,
+        name: this.actionLabels.EDIT,
+        disable: (selection: CdTableSelection) => this.getDisable('edit', selection),
+        click: () => this.openRemoteClusterInfoModal('edit')
+      },
+      {
+        permission: 'update',
+        icon: Icons.refresh,
+        name: this.actionLabels.RECONNECT,
+        disable: (selection: CdTableSelection) => this.getDisable('reconnect', selection),
+        click: () => this.openRemoteClusterInfoModal('reconnect')
+      },
+      {
+        permission: 'delete',
+        icon: Icons.destroy,
+        name: this.actionLabels.DISCONNECT,
+        disable: (selection: CdTableSelection) => this.getDisable('disconnect', selection),
+        click: () => this.openDeleteClusterModal()
+      }
+    ];
+    this.permissions = this.authStorageService.getPermissions();
+  }
+
+  ngOnInit(): void {
+    this.multiClusterService.subscribe((resp: object) => {
+      if (resp && resp['config']) {
+        const clusterDetailsArray = Object.values(resp['config']).flat();
+        this.data = clusterDetailsArray;
+        this.checkClusterConnectionStatus();
+      }
+    });
+
+    this.columns = [
+      {
+        prop: 'cluster_alias',
+        name: $localize`Alias`,
+        flexGrow: 2
+      },
+      {
+        prop: 'cluster_connection_status',
+        name: $localize`Connection`,
+        flexGrow: 2,
+        cellTransformation: CellTemplate.badge,
+        customTemplateConfig: {
+          map: {
+            1: { value: 'DISCONNECTED', class: 'badge-danger' },
+            0: { value: 'CONNECTED', class: 'badge-success' },
+            2: { value: 'CHECKING..', class: 'badge-info' }
+          }
+        }
+      },
+      {
+        prop: 'name',
+        name: $localize`FSID`,
+        flexGrow: 2
+      },
+      {
+        prop: 'url',
+        name: $localize`URL`,
+        flexGrow: 2,
+        cellTemplate: this.urlTpl
+      },
+      {
+        prop: 'user',
+        name: $localize`User`,
+        flexGrow: 2
+      }
+    ];
+
+    this.multiClusterService.subscribeClusterTokenStatus((resp: object) => {
+      this.clusterTokenStatus = resp;
+      this.checkClusterConnectionStatus();
+    });
+  }
+
+  checkClusterConnectionStatus() {
+    if (this.clusterTokenStatus && this.data) {
+      this.data.forEach((cluster: MultiCluster) => {
+        const clusterStatus = this.clusterTokenStatus[cluster.name];
+
+        if (clusterStatus !== undefined) {
+          cluster.cluster_connection_status = clusterStatus.status;
+        } else {
+          cluster.cluster_connection_status = 2;
+        }
+
+        if (cluster.cluster_alias === 'local-cluster') {
+          cluster.cluster_connection_status = 0;
+        }
+      });
+    }
+  }
+
+  openRemoteClusterInfoModal(action: string) {
+    const initialState = {
+      clustersData: this.data,
+      action: action,
+      cluster: this.selection.first()
+    };
+    this.bsModalRef = this.modalService.show(MultiClusterFormComponent, initialState, {
+      size: 'xl'
+    });
+    this.bsModalRef.componentInstance.submitAction.subscribe(() => {
+      this.multiClusterService.refresh();
+      this.summaryService.refresh();
+      const currentRoute = this.router.url.split('?')[0];
+      if (currentRoute.includes('dashboard')) {
+        this.router.navigateByUrl('/pool', { skipLocationChange: true }).then(() => {
+          this.router.navigate([currentRoute]);
+        });
+      } else {
+        this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
+          this.router.navigate([currentRoute]);
+        });
+      }
+    });
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
+
+  openDeleteClusterModal() {
+    const cluster = this.selection.first();
+    this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+      actionDescription: $localize`Disconnect`,
+      itemDescription: $localize`Cluster`,
+      itemNames: [cluster['cluster_alias'] + ' - ' + cluster['user']],
+      submitAction: () =>
+        this.multiClusterService.deleteCluster(cluster['name'], cluster['user']).subscribe(() => {
+          this.modalRef.close();
+          this.notificationService.show(
+            NotificationType.success,
+            $localize`Disconnected cluster '${cluster['cluster_alias']}'`
+          );
+        })
+    });
+  }
+
+  getDisable(action: string, selection: CdTableSelection): string | boolean {
+    if (!selection.hasSelection) {
+      return $localize`Please select one or more clusters to ${action}`;
+    }
+    if (selection.hasSingleSelection) {
+      const cluster = selection.first();
+      if (cluster['cluster_alias'] === 'local-cluster') {
+        return $localize`Cannot ${action} local cluster`;
+      }
+    }
+    return false;
+  }
+}
index 5009909ea3fac59a67f03db296f98c11cd6d5fbe..c826f155c40bfa532095202fc6769c0f28299651 100644 (file)
   </span>
   <div *ngIf="dashboardClustersMap?.size > 1">
     <div *ngIf="!loading">
-      <div class="mt-4">
-        <div class="text-center">
-          <button class="btn btn-primary"
-                  (click)="openRemoteClusterInfoModal()">
-            <i class="mx-auto"
-               [ngClass]="icons.add">
-            </i> Connect Cluster
-          </button>
-        </div>
-      </div>
     </div>
   </div>
 </div>
index 2630c839a424527d105d357a77eaebbd0aefebfa..dbbf10e74848bd1b19b9dc9b43c4fc3aa2f12dd2 100644 (file)
@@ -50,7 +50,10 @@ export class MultiClusterComponent implements OnInit {
   }
 
   openRemoteClusterInfoModal() {
-    this.bsModalRef = this.modalService.show(MultiClusterFormComponent, {
+    const initialState = {
+      action: 'connect'
+    };
+    this.bsModalRef = this.modalService.show(MultiClusterFormComponent, initialState, {
       size: 'xl'
     });
   }
index 1d7c4bb751cb6ae1fba1f5f4b828fa1e8e5565bf..8ddbddf2fe81493fe93625139df5ddeec7f1844b 100644 (file)
@@ -27,6 +27,7 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy {
 
   ngOnInit() {
     this.subs.add(this.multiClusterService.startPolling());
+    this.subs.add(this.multiClusterService.startClusterTokenStatusPolling());
     this.subs.add(this.summaryService.startPolling());
     this.subs.add(this.taskManagerService.init(this.summaryService));
     this.faviconService.init();
index 6af3799b4ef84734abb22debccd18e6f67b6e40b..8f2633ed0a15a19fc3dfe1b8ccdc7e9ecb3a4e0a 100644 (file)
@@ -44,7 +44,8 @@
             <div ngbDropdownMenu>
               <ng-container *ngFor="let cluster of clustersMap | keyvalue">
                 <button ngbDropdownItem
-                        (click)="onClusterSelection(cluster.value)">
+                        (click)="onClusterSelection(cluster.value)"
+                        [disabled]="cluster.value.cluster_connection_status === 1">
                   <div class="dropdown-text">{{ cluster.value.name }}</div>
                   <div *ngIf="cluster.value.cluster_alias"
                        class="text-secondary">{{ cluster.value.cluster_alias }} - {{ cluster.value.user }}</div>
           <li routerLinkActive="active"
               class="tc_submenuitem tc_submenuitem_multiCluster_overview">
             <a i18n
-               routerLink="/multi-cluster">Overview</a>
+               routerLink="/multi-cluster/overview">Overview</a>
+          </li>
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_multiCluster_manage_clusters">
+            <a i18n
+               routerLink="/multi-cluster/manage-clusters">Manage Clusters</a>
           </li>
         </ul>
       </li>
index 10963042d25fdfd4ae0fa81680ddc62432ea1484..4ae8d1897e27e9c82b24e44f60b4975c2431263e 100644 (file)
@@ -6,6 +6,7 @@ import { Subscription } from 'rxjs';
 import { MultiClusterService } from '~/app/shared/api/multi-cluster.service';
 
 import { Icons } from '~/app/shared/enum/icons.enum';
+import { MultiCluster } from '~/app/shared/models/multi-cluster';
 import { Permissions } from '~/app/shared/models/permissions';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import {
@@ -31,6 +32,7 @@ export class NavigationComponent implements OnInit, OnDestroy {
 
   permissions: Permissions;
   enabledFeature$: FeatureTogglesMap$;
+  clusterTokenStatus: object = {};
   summaryData: any;
   icons = Icons;
 
@@ -62,22 +64,17 @@ export class NavigationComponent implements OnInit, OnDestroy {
 
   ngOnInit() {
     this.subs.add(
-      this.multiClusterService.subscribe((resp: any) => {
+      this.multiClusterService.subscribe((resp: object) => {
         const clustersConfig = resp['config'];
         if (clustersConfig) {
           Object.keys(clustersConfig).forEach((clusterKey: string) => {
             const clusterDetailsList = clustersConfig[clusterKey];
-            clusterDetailsList.forEach((clusterDetails: any) => {
-              const clusterName = clusterDetails['name'];
+            clusterDetailsList.forEach((clusterDetails: MultiCluster) => {
               const clusterUser = clusterDetails['user'];
               const clusterUrl = clusterDetails['url'];
               const clusterUniqueKey = `${clusterUrl}-${clusterUser}`;
-              this.clustersMap.set(clusterUniqueKey, {
-                name: clusterName,
-                cluster_alias: clusterDetails['cluster_alias'],
-                user: clusterDetails['user'],
-                url: clusterUrl
-              });
+              this.clustersMap.set(clusterUniqueKey, clusterDetails);
+              this.checkClusterConnectionStatus();
             });
           });
           this.selectedCluster =
@@ -111,12 +108,40 @@ export class NavigationComponent implements OnInit, OnDestroy {
         this.showTopNotification('motdNotificationEnabled', _.isPlainObject(motd));
       })
     );
+    this.subs.add(
+      this.multiClusterService.subscribeClusterTokenStatus((resp: object) => {
+        this.clusterTokenStatus = resp;
+        this.checkClusterConnectionStatus();
+      })
+    );
   }
 
   ngOnDestroy(): void {
     this.subs.unsubscribe();
   }
 
+  checkClusterConnectionStatus() {
+    this.clustersMap.forEach((clusterDetails, clusterName) => {
+      const clusterTokenStatus = this.clusterTokenStatus[clusterDetails.name];
+      const connectionStatus = clusterTokenStatus ? clusterTokenStatus.status : 0;
+      const user = clusterTokenStatus ? clusterTokenStatus.user : clusterDetails.user;
+
+      this.clustersMap.set(clusterName, {
+        ...clusterDetails,
+        cluster_connection_status: connectionStatus,
+        user: user
+      });
+
+      if (clusterDetails.cluster_alias === 'local-cluster') {
+        this.clustersMap.set(clusterName, {
+          ...clusterDetails,
+          cluster_connection_status: 0,
+          user: user
+        });
+      }
+    });
+  }
+
   blockHealthColor() {
     if (this.summaryData && this.summaryData.rbd_mirroring) {
       if (this.summaryData.rbd_mirroring.errors > 0) {
index 5a17645092df2726e06a713ac2ecfcb2b283f92a..7252e969e60ae490cea37799df508085705d13b0 100644 (file)
@@ -1,6 +1,6 @@
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpParams } from '@angular/common/http';
 import { Injectable } from '@angular/core';
-import { BehaviorSubject, Subscription } from 'rxjs';
+import { BehaviorSubject, Observable, Subscription } from 'rxjs';
 import { TimerService } from '../services/timer.service';
 import { filter } from 'rxjs/operators';
 
@@ -8,8 +8,11 @@ import { filter } from 'rxjs/operators';
   providedIn: 'root'
 })
 export class MultiClusterService {
+  TOKEN_CHECK_INTERVAL = 600000; // 10m interval
   private msSource = new BehaviorSubject<any>(null);
   msData$ = this.msSource.asObservable();
+  private tokenStatusSource = new BehaviorSubject<any>(null);
+  tokenStatusSource$ = this.tokenStatusSource.asObservable();
   constructor(private http: HttpClient, private timerService: TimerService) {}
 
   startPolling(): Subscription {
@@ -18,6 +21,44 @@ export class MultiClusterService {
       .subscribe(this.getClusterObserver());
   }
 
+  startClusterTokenStatusPolling() {
+    let clustersTokenMap = new Map<string, { token: string; user: string }>();
+    const dataSubscription = this.subscribe((resp: any) => {
+      const clustersConfig = resp['config'];
+      const tempMap = new Map<string, { token: string; user: string }>();
+      if (clustersConfig) {
+        Object.keys(clustersConfig).forEach((clusterKey: string) => {
+          const clusterDetailsList = clustersConfig[clusterKey];
+          clusterDetailsList.forEach((clusterDetails: any) => {
+            if (clusterDetails['token'] && clusterDetails['name'] && clusterDetails['user']) {
+              tempMap.set(clusterDetails['name'], {
+                token: clusterDetails['token'],
+                user: clusterDetails['user']
+              });
+            }
+          });
+        });
+
+        if (tempMap.size > 0) {
+          clustersTokenMap = tempMap;
+          dataSubscription.unsubscribe();
+          this.checkAndStartTimer(clustersTokenMap);
+        }
+      }
+    });
+  }
+
+  private checkAndStartTimer(clustersTokenMap: Map<string, { token: string; user: string }>) {
+    this.checkTokenStatus(clustersTokenMap).subscribe(this.getClusterTokenStatusObserver());
+    this.timerService
+      .get(() => this.checkTokenStatus(clustersTokenMap), this.TOKEN_CHECK_INTERVAL)
+      .subscribe(this.getClusterTokenStatusObserver());
+  }
+
+  subscribeClusterTokenStatus(next: (data: any) => void, error?: (error: any) => void) {
+    return this.tokenStatusSource$.pipe(filter((value) => !!value)).subscribe(next, error);
+  }
+
   refresh(): Subscription {
     return this.getCluster().subscribe(this.getClusterObserver());
   }
@@ -34,13 +75,26 @@ export class MultiClusterService {
     return this.http.get('api/multi-cluster/get_config');
   }
 
+  deleteCluster(clusterName: string, clusterUser: string): Observable<any> {
+    return this.http.delete(`api/multi-cluster/delete_cluster/${clusterName}/${clusterUser}`);
+  }
+
+  editCluster(url: any, clusterAlias: string, username: string) {
+    return this.http.put('api/multi-cluster/edit_cluster', {
+      url,
+      cluster_alias: clusterAlias,
+      username
+    });
+  }
+
   addCluster(
     url: any,
     clusterAlias: string,
     username: string,
     password: string,
     token = '',
-    hub_url = ''
+    hub_url = '',
+    clusterFsid = ''
   ) {
     return this.http.post('api/multi-cluster/auth', {
       url,
@@ -48,12 +102,13 @@ export class MultiClusterService {
       username,
       password,
       token,
-      hub_url
+      hub_url,
+      cluster_fsid: clusterFsid
     });
   }
 
-  verifyConnection(url: string, username: string, password: string, token = '') {
-    return this.http.post('api/multi-cluster/verify_connection', {
+  reConnectCluster(url: any, username: string, password: string, token = '') {
+    return this.http.put('api/multi-cluster/reconnect_cluster', {
       url,
       username,
       password,
@@ -61,9 +116,36 @@ export class MultiClusterService {
     });
   }
 
+  verifyConnection(url: string, username: string, password: string, token = ''): Observable<any> {
+    let params = new HttpParams()
+      .set('url', url)
+      .set('username', username)
+      .set('password', password)
+      .set('token', token);
+
+    return this.http.get('api/multi-cluster/verify_connection', { params });
+  }
+
   private getClusterObserver() {
     return (data: any) => {
       this.msSource.next(data);
     };
   }
+
+  private getClusterTokenStatusObserver() {
+    return (data: any) => {
+      this.tokenStatusSource.next(data);
+    };
+  }
+
+  checkTokenStatus(
+    clustersTokenMap: Map<string, { token: string; user: string }>
+  ): Observable<object> {
+    let data = [...clustersTokenMap].map(([key, { token, user }]) => ({ name: key, token, user }));
+
+    let params = new HttpParams();
+    params = params.set('clustersTokenMap', JSON.stringify(data));
+
+    return this.http.get<object>('api/multi-cluster/check_token_status', { params });
+  }
 }
index df007c93d8a602e1fd86296a159573fd4b8401f7..876e22bbc1b61c10934e77b02f8e53ef7515751d 100644 (file)
@@ -145,6 +145,8 @@ export class ActionLabelsI18n {
   DEACTIVATE: string;
   ATTACH: string;
   CONNECT: string;
+  DISCONNECT: string;
+  RECONNECT: string;
 
   constructor() {
     /* Create a new item */
@@ -228,6 +230,8 @@ export class ActionLabelsI18n {
 
     this.ATTACH = $localize`Attach`;
     this.CONNECT = $localize`Connect`;
+    this.DISCONNECT = $localize`Disconnect`;
+    this.RECONNECT = $localize`Reconnect`;
   }
 }
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/multi-cluster.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/multi-cluster.ts
new file mode 100644 (file)
index 0000000..ce4e026
--- /dev/null
@@ -0,0 +1,8 @@
+export interface MultiCluster {
+  name: string;
+  url: string;
+  user: string;
+  token: string;
+  cluster_alias: string;
+  cluster_connection_status: number;
+}
index e4e30d6a3682e37b7ee55e2a4a2d732d93e584fa..5cbd99911fb2d6709fd8e0a8029b037181f9da55 100644 (file)
@@ -76,7 +76,11 @@ export class ApiInterceptorService implements HttpInterceptor {
       });
     }
 
-    const apiUrl = localStorage.getItem('cluster_api_url');
+    let apiUrl = localStorage.getItem('cluster_api_url');
+
+    if (apiUrl && !apiUrl.endsWith('/')) {
+      apiUrl += '/';
+    }
     const currentRoute = this.router.url.split('?')[0];
 
     const ALWAYS_TO_HUB_APIs = [
index 912718d9ee779f5edc7339fb36a09d99c2096542..4e58517a9636fa854075216cae8cc4c024027565 100644 (file)
@@ -6968,6 +6968,8 @@ paths:
               properties:
                 cluster_alias:
                   type: string
+                cluster_fsid:
+                  type: string
                 hub_url:
                   type: string
                 password:
@@ -7007,9 +7009,14 @@ paths:
       summary: Authenticate to a remote cluster
       tags:
       - Multi-cluster
-  /api/multi-cluster/get_config:
+  /api/multi-cluster/check_token_status:
     get:
-      parameters: []
+      parameters:
+      - allowEmptyValue: true
+        in: query
+        name: clustersTokenMap
+        schema:
+          type: string
       responses:
         '200':
           content:
@@ -7029,7 +7036,44 @@ paths:
       - jwt: []
       tags:
       - Multi-cluster
-  /api/multi-cluster/set_config:
+  /api/multi-cluster/delete_cluster/{cluster_name}/{cluster_user}:
+    delete:
+      parameters:
+      - in: path
+        name: cluster_name
+        required: true
+        schema:
+          type: string
+      - in: path
+        name: cluster_user
+        required: true
+        schema:
+          type: string
+      responses:
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '204':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource deleted.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - Multi-cluster
+  /api/multi-cluster/edit_cluster:
     put:
       parameters: []
       requestBody:
@@ -7037,10 +7081,16 @@ paths:
           application/json:
             schema:
               properties:
-                config:
+                cluster_alias:
+                  type: string
+                url:
+                  type: string
+                username:
                   type: string
               required:
-              - config
+              - url
+              - cluster_alias
+              - username
               type: object
       responses:
         '200':
@@ -7066,8 +7116,30 @@ paths:
       - jwt: []
       tags:
       - Multi-cluster
-  /api/multi-cluster/verify_connection:
-    post:
+  /api/multi-cluster/get_config:
+    get:
+      parameters: []
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - Multi-cluster
+  /api/multi-cluster/reconnect_cluster:
+    put:
       parameters: []
       requestBody:
         content:
@@ -7086,11 +7158,48 @@ paths:
               - url
               type: object
       responses:
-        '201':
+        '200':
           content:
             application/vnd.ceph.api.v1.0+json:
               type: object
-          description: Resource created.
+          description: Resource updated.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - Multi-cluster
+  /api/multi-cluster/set_config:
+    put:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                config:
+                  type: string
+              required:
+              - config
+              type: object
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
         '202':
           content:
             application/vnd.ceph.api.v1.0+json:
@@ -7109,6 +7218,48 @@ paths:
       - jwt: []
       tags:
       - Multi-cluster
+  /api/multi-cluster/verify_connection:
+    get:
+      parameters:
+      - allowEmptyValue: true
+        in: query
+        name: url
+        schema:
+          type: string
+      - allowEmptyValue: true
+        in: query
+        name: username
+        schema:
+          type: string
+      - allowEmptyValue: true
+        in: query
+        name: password
+        schema:
+          type: string
+      - allowEmptyValue: true
+        in: query
+        name: token
+        schema:
+          type: string
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - Multi-cluster
   /api/nfs-ganesha/cluster:
     get:
       parameters: []