]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add e2e tests for the Orchestrator components 36365/head
authorKiefer Chang <kiefer.chang@suse.com>
Tue, 28 Jul 2020 01:57:33 +0000 (09:57 +0800)
committerKiefer Chang <kiefer.chang@suse.com>
Mon, 31 Aug 2020 08:50:16 +0000 (16:50 +0800)
Fixes: https://tracker.ceph.com/issues/44637
Signed-off-by: Kiefer Chang <kiefer.chang@suse.com>
21 files changed:
qa/suites/rados/cephadm/dashboard/% [new file with mode: 0644]
qa/suites/rados/cephadm/dashboard/.qa [new symlink]
qa/suites/rados/cephadm/dashboard/distro/.qa [new symlink]
qa/suites/rados/cephadm/dashboard/distro/centos_latest.yaml [new symlink]
qa/suites/rados/cephadm/dashboard/task/test_e2e.yaml [new file with mode: 0644]
qa/workunits/cephadm/create_iscsi_disks.sh [new file with mode: 0755]
qa/workunits/cephadm/test_dashboard_e2e.sh [new file with mode: 0755]
src/pybind/mgr/dashboard/frontend/cypress.json
src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/inventory.json [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/inventory.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/01-hosts.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/02-hosts-inventory.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/03-inventory.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/04-osds.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.po.ts

diff --git a/qa/suites/rados/cephadm/dashboard/% b/qa/suites/rados/cephadm/dashboard/%
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/qa/suites/rados/cephadm/dashboard/.qa b/qa/suites/rados/cephadm/dashboard/.qa
new file mode 120000 (symlink)
index 0000000..fea2489
--- /dev/null
@@ -0,0 +1 @@
+../.qa
\ No newline at end of file
diff --git a/qa/suites/rados/cephadm/dashboard/distro/.qa b/qa/suites/rados/cephadm/dashboard/distro/.qa
new file mode 120000 (symlink)
index 0000000..fea2489
--- /dev/null
@@ -0,0 +1 @@
+../.qa
\ No newline at end of file
diff --git a/qa/suites/rados/cephadm/dashboard/distro/centos_latest.yaml b/qa/suites/rados/cephadm/dashboard/distro/centos_latest.yaml
new file mode 120000 (symlink)
index 0000000..bd9854e
--- /dev/null
@@ -0,0 +1 @@
+.qa/distros/supported/centos_latest.yaml
\ No newline at end of file
diff --git a/qa/suites/rados/cephadm/dashboard/task/test_e2e.yaml b/qa/suites/rados/cephadm/dashboard/task/test_e2e.yaml
new file mode 100644 (file)
index 0000000..cb6ffb2
--- /dev/null
@@ -0,0 +1,23 @@
+roles:
+# 3 osd roles on host.a is required for cephadm task. It checks if the cluster is healthy.
+# More daemons will be deployed on both hosts in e2e tests.
+- - host.a
+  - osd.0
+  - osd.1
+  - osd.2
+  - mon.a
+  - mgr.a
+  - client.0
+- - host.b
+  - client.1
+tasks:
+- install:
+- cephadm:
+- workunit:
+    clients:
+      client.1:
+        - cephadm/create_iscsi_disks.sh
+- workunit:
+    clients:
+      client.0:
+        - cephadm/test_dashboard_e2e.sh
diff --git a/qa/workunits/cephadm/create_iscsi_disks.sh b/qa/workunits/cephadm/create_iscsi_disks.sh
new file mode 100755 (executable)
index 0000000..e43791d
--- /dev/null
@@ -0,0 +1,34 @@
+#!/bin/bash -ex
+# Create some file-backed iSCSI targets and attach them locally.
+
+# Exit if it's not CentOS
+if ! grep -q rhel /etc/*-release; then
+    echo "The script only supports CentOS."
+    exit 1
+fi
+
+[ -z "$SUDO" ] && SUDO=sudo
+
+# 15 GB
+DISK_FILE_SIZE="16106127360"
+
+$SUDO yum install -y targetcli iscsi-initiator-utils
+
+TARGET_NAME="iqn.2003-01.org.linux-iscsi.$(hostname).x8664:sn.foobar"
+$SUDO targetcli /iscsi create ${TARGET_NAME}
+$SUDO targetcli /iscsi/${TARGET_NAME}/tpg1/portals delete 0.0.0.0 3260
+$SUDO targetcli /iscsi/${TARGET_NAME}/tpg1/portals create 127.0.0.1 3260
+$SUDO targetcli /iscsi/${TARGET_NAME}/tpg1 set attribute generate_node_acls=1
+$SUDO targetcli /iscsi/${TARGET_NAME}/tpg1 set attribute demo_mode_write_protect=0
+
+for i in $(seq 3); do
+    # Create truncated files, and add them as luns
+    DISK_FILE="/tmp/disk${i}"
+    $SUDO truncate --size ${DISK_FILE_SIZE} ${DISK_FILE}
+
+    $SUDO targetcli /backstores/fileio create "lun${i}" ${DISK_FILE}
+    $SUDO targetcli /iscsi/${TARGET_NAME}/tpg1/luns create "/backstores/fileio/lun${i}"
+done
+
+$SUDO iscsiadm -m discovery -t sendtargets -p 127.0.0.1
+$SUDO iscsiadm -m node -p 127.0.0.1 -T ${TARGET_NAME} -l
diff --git a/qa/workunits/cephadm/test_dashboard_e2e.sh b/qa/workunits/cephadm/test_dashboard_e2e.sh
new file mode 100755 (executable)
index 0000000..3463340
--- /dev/null
@@ -0,0 +1,99 @@
+#!/bin/bash -ex
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+DASHBOARD_FRONTEND_DIR=${SCRIPT_DIR}/../../../src/pybind/mgr/dashboard/frontend
+
+[ -z "$SUDO" ] && SUDO=sudo
+
+install_common () {
+    if grep -q  debian /etc/*-release; then
+        $SUDO apt-get update
+        $SUDO apt-get install -y jq npm
+    elif grep -q rhel /etc/*-release; then
+        $SUDO yum install -y jq npm
+    else
+        echo "Unsupported distribution."
+        exit 1
+    fi
+}
+
+install_chrome () {
+    if grep -q  debian /etc/*-release; then
+        $SUDO bash -c 'echo "deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list'
+        curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | $SUDO apt-key add -
+        $SUDO apt-get update
+        $SUDO apt-get install -y google-chrome-stable
+        $SUDO apt-get install -y xvfb
+        $SUDO rm /etc/apt/sources.list.d/google-chrome.list
+    elif grep -q rhel /etc/*-release; then
+        $SUDO dd of=/etc/yum.repos.d/google-chrome.repo status=none <<EOF
+[google-chrome]
+name=google-chrome
+baseurl=https://dl.google.com/linux/chrome/rpm/stable/\$basearch
+enabled=1
+gpgcheck=1
+gpgkey=https://dl-ssl.google.com/linux/linux_signing_key.pub
+EOF
+        $SUDO yum install -y google-chrome-stable
+        $SUDO rm /etc/yum.repos.d/google-chrome.repo
+        # Cypress dependencies
+        $SUDO yum install -y xorg-x11-server-Xvfb gtk2-devel gtk3-devel libnotify-devel GConf2 nss.x86_64 libXScrnSaver alsa-lib
+    else
+        echo "Unsupported distribution."
+        exit 1
+    fi
+}
+
+cypress_run () {
+    local specs="$1"
+    local timeout="$2"
+    local override_config="ignoreTestFiles=*.po.ts,testFiles=${specs}"
+
+    if [ x"$timeout" != "x" ]; then
+        override_config="${override_config},defaultCommandTimeout=${timeout}"
+    fi
+    npx cypress run --browser chrome --headless --config "$override_config"
+}
+
+install_common
+install_chrome
+
+CYPRESS_BASE_URL=$(ceph mgr services | jq -r .dashboard)
+export CYPRESS_BASE_URL
+
+cd $DASHBOARD_FRONTEND_DIR
+
+# This is required for Cypress to understand typescript
+npm ci --unsafe-perm
+npx cypress verify
+npx cypress info
+
+# Remove device_health_metrics pool
+# Low pg count causes OSD removal failure.
+ceph device monitoring off
+ceph tell mon.\* injectargs '--mon-allow-pool-delete=true'
+ceph osd pool rm device_health_metrics device_health_metrics --yes-i-really-really-mean-it
+
+# Take `orch device ls` as ground truth.
+ceph orch device ls --refresh
+sleep 10  # the previous call is asynchronous
+ceph orch device ls --format=json | tee cypress/fixtures/orchestrator/inventory.json
+
+ceph dashboard ac-user-set-password admin admin --force-password
+
+# Run Dashboard e2e tests.
+# These tests are designed with execution order in mind, since orchestrator operations
+# are likely to change cluster state, we can't just run tests in arbitrarily order.
+# See /ceph/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/ folder.
+find cypress # List all specs
+
+cypress_run "orchestrator/01-hosts.e2e-spec.ts"
+
+# Hosts are removed and added in the previous step. Do a refresh again.
+ceph orch device ls --refresh
+sleep 10
+ceph orch device ls --format=json | tee cypress/fixtures/orchestrator/inventory.json
+
+cypress_run "orchestrator/02-hosts-inventory.e2e-spec.ts"
+cypress_run "orchestrator/03-inventory.e2e-spec.ts"
+cypress_run "orchestrator/04-osds.e2e-spec.ts" 300000
index ce9e5e8453caf7aaa92dd079e8c846d5a952d8b2..4448bf7ca0b0d8defb34774c83c3f35372bd1296 100644 (file)
@@ -1,7 +1,8 @@
 {
   "baseUrl": "http://localhost:4200/",
   "ignoreTestFiles": [
-    "*.po.ts"
+    "*.po.ts",
+    "**/orchestrator/**"
   ],
   "supportFile": "cypress/support/index.ts",
   "video": false,
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/inventory.json b/src/pybind/mgr/dashboard/frontend/cypress/fixtures/orchestrator/inventory.json
new file mode 100644 (file)
index 0000000..21386f2
--- /dev/null
@@ -0,0 +1,390 @@
+[
+  {
+    "addr": "node1",
+    "devices": [
+      {
+        "available": false,
+        "device_id": "",
+        "human_readable_type": "hdd",
+        "lvs": [],
+        "path": "/dev/vda",
+        "rejected_reasons": ["locked"],
+        "sys_api": {
+          "human_readable_size": "42.00 GB",
+          "locked": 1,
+          "model": "",
+          "nr_requests": "256",
+          "partitions": {
+            "vda1": {
+              "holders": [],
+              "human_readable_size": "2.00 MB",
+              "sectors": "4096",
+              "sectorsize": 512,
+              "size": 2097152.0,
+              "start": "2048"
+            },
+            "vda2": {
+              "holders": [],
+              "human_readable_size": "20.00 MB",
+              "sectors": "40960",
+              "sectorsize": 512,
+              "size": 20971520.0,
+              "start": "6144"
+            },
+            "vda3": {
+              "holders": [],
+              "human_readable_size": "41.98 GB",
+              "sectors": "88033247",
+              "sectorsize": 512,
+              "size": 45073022464.0,
+              "start": "47104"
+            }
+          },
+          "path": "/dev/vda",
+          "removable": "0",
+          "rev": "",
+          "ro": "0",
+          "rotational": "1",
+          "sas_address": "",
+          "sas_device_handle": "",
+          "scheduler_mode": "mq-deadline",
+          "sectors": 0,
+          "sectorsize": "512",
+          "size": 45097156608.0,
+          "support_discard": "512",
+          "vendor": "0x1af4"
+        }
+      },
+      {
+        "available": false,
+        "device_id": "641526",
+        "human_readable_type": "hdd",
+        "lvs": [
+          {
+            "block_uuid": "355c2I-e5kg-WWeT-bOsI-0Ez5-sfb7-7TZyE4",
+            "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+            "cluster_name": "ceph",
+            "name": "osd-data-3de18e23-8849-494c-83b0-458d97d32d72",
+            "osd_fsid": "a438ac13-f1bd-412c-9626-e2f063dbbf94",
+            "osd_id": "0",
+            "osdspec_affinity": "dashboard-admin-1597903910143",
+            "type": "block"
+          }
+        ],
+        "path": "/dev/vdb",
+        "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+        "sys_api": {
+          "human_readable_size": "8.00 GB",
+          "locked": 1,
+          "model": "",
+          "nr_requests": "256",
+          "partitions": {},
+          "path": "/dev/vdb",
+          "removable": "0",
+          "rev": "",
+          "ro": "0",
+          "rotational": "1",
+          "sas_address": "",
+          "sas_device_handle": "",
+          "scheduler_mode": "mq-deadline",
+          "sectors": 0,
+          "sectorsize": "512",
+          "size": 8589934592.0,
+          "support_discard": "512",
+          "vendor": "0x1af4"
+        }
+      },
+      {
+        "available": false,
+        "device_id": "467047",
+        "human_readable_type": "hdd",
+        "lvs": [
+          {
+            "block_uuid": "iGC2VU-MSTt-ZP05-kKCP-5EtO-F1Y3-DYAAeb",
+            "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+            "cluster_name": "ceph",
+            "name": "osd-data-2031893c-c83b-4ff0-bfa1-de548044f707",
+            "osd_fsid": "6f544fc4-a3ea-40f9-9c48-69b5ee866709",
+            "osd_id": "1",
+            "osdspec_affinity": "dashboard-admin-1597903910143",
+            "type": "block"
+          }
+        ],
+        "path": "/dev/vdc",
+        "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+        "sys_api": {
+          "human_readable_size": "8.00 GB",
+          "locked": 1,
+          "model": "",
+          "nr_requests": "256",
+          "partitions": {},
+          "path": "/dev/vdc",
+          "removable": "0",
+          "rev": "",
+          "ro": "0",
+          "rotational": "1",
+          "sas_address": "",
+          "sas_device_handle": "",
+          "scheduler_mode": "mq-deadline",
+          "sectors": 0,
+          "sectorsize": "512",
+          "size": 8589934592.0,
+          "support_discard": "512",
+          "vendor": "0x1af4"
+        }
+      },
+      {
+        "available": false,
+        "device_id": "900807",
+        "human_readable_type": "hdd",
+        "lvs": [
+          {
+            "block_uuid": "nO2VSn-IbXr-pxnx-ieXx-kIxk-B4hB-BM6ADc",
+            "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+            "cluster_name": "ceph",
+            "name": "osd-data-537f7b60-5887-440e-80c7-759c028db12d",
+            "osd_fsid": "adeddd37-5cc9-406a-88e5-2add3f81d089",
+            "osd_id": "2",
+            "osdspec_affinity": "dashboard-admin-1597903910143",
+            "type": "block"
+          }
+        ],
+        "path": "/dev/vdd",
+        "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+        "sys_api": {
+          "human_readable_size": "8.00 GB",
+          "locked": 1,
+          "model": "",
+          "nr_requests": "256",
+          "partitions": {},
+          "path": "/dev/vdd",
+          "removable": "0",
+          "rev": "",
+          "ro": "0",
+          "rotational": "1",
+          "sas_address": "",
+          "sas_device_handle": "",
+          "scheduler_mode": "mq-deadline",
+          "sectors": 0,
+          "sectorsize": "512",
+          "size": 8589934592.0,
+          "support_discard": "512",
+          "vendor": "0x1af4"
+        }
+      },
+      {
+        "available": false,
+        "device_id": "757404",
+        "human_readable_type": "hdd",
+        "lvs": [
+          {
+            "block_uuid": "3YSAlw-VMeK-XfUK-rbOB-IKD1-Z9ZI-hUzlDe",
+            "cluster_fsid": "68a32428-e2ab-11ea-9d25-525400ef4c6e",
+            "cluster_name": "ceph",
+            "name": "osd-data-15b39d59-f259-4e93-adc6-bdac7d490d88",
+            "osd_fsid": "840a7138-88e2-4ecb-b88d-6fa2d04d88e7",
+            "osd_id": "3",
+            "osdspec_affinity": "dashboard-admin-1597903910143",
+            "type": "block"
+          }
+        ],
+        "path": "/dev/vde",
+        "rejected_reasons": ["locked", "LVM detected", "Insufficient space (<5GB) on vgs"],
+        "sys_api": {
+          "human_readable_size": "8.00 GB",
+          "locked": 1,
+          "model": "",
+          "nr_requests": "256",
+          "partitions": {},
+          "path": "/dev/vde",
+          "removable": "0",
+          "rev": "",
+          "ro": "0",
+          "rotational": "1",
+          "sas_address": "",
+          "sas_device_handle": "",
+          "scheduler_mode": "mq-deadline",
+          "sectors": 0,
+          "sectorsize": "512",
+          "size": 8589934592.0,
+          "support_discard": "512",
+          "vendor": "0x1af4"
+        }
+      }
+    ],
+    "labels": [],
+    "name": "node1"
+  },
+  {
+    "addr": "node2",
+    "devices": [
+      {
+        "available": true,
+        "device_id": "115432",
+        "human_readable_type": "hdd",
+        "lvs": [],
+        "path": "/dev/vdb",
+        "rejected_reasons": [],
+        "sys_api": {
+          "human_readable_size": "8.00 GB",
+          "locked": 0,
+          "model": "",
+          "nr_requests": "256",
+          "partitions": {},
+          "path": "/dev/vdb",
+          "removable": "0",
+          "rev": "",
+          "ro": "0",
+          "rotational": "1",
+          "sas_address": "",
+          "sas_device_handle": "",
+          "scheduler_mode": "mq-deadline",
+          "sectors": 0,
+          "sectorsize": "512",
+          "size": 8589934592.0,
+          "support_discard": "512",
+          "vendor": "0x1af4"
+        }
+      },
+      {
+        "available": true,
+        "device_id": "937699",
+        "human_readable_type": "hdd",
+        "lvs": [],
+        "path": "/dev/vdc",
+        "rejected_reasons": [],
+        "sys_api": {
+          "human_readable_size": "8.00 GB",
+          "locked": 0,
+          "model": "",
+          "nr_requests": "256",
+          "partitions": {},
+          "path": "/dev/vdc",
+          "removable": "0",
+          "rev": "",
+          "ro": "0",
+          "rotational": "1",
+          "sas_address": "",
+          "sas_device_handle": "",
+          "scheduler_mode": "mq-deadline",
+          "sectors": 0,
+          "sectorsize": "512",
+          "size": 8589934592.0,
+          "support_discard": "512",
+          "vendor": "0x1af4"
+        }
+      },
+      {
+        "available": true,
+        "device_id": "854127",
+        "human_readable_type": "hdd",
+        "lvs": [],
+        "path": "/dev/vdd",
+        "rejected_reasons": [],
+        "sys_api": {
+          "human_readable_size": "8.00 GB",
+          "locked": 0,
+          "model": "",
+          "nr_requests": "256",
+          "partitions": {},
+          "path": "/dev/vdd",
+          "removable": "0",
+          "rev": "",
+          "ro": "0",
+          "rotational": "1",
+          "sas_address": "",
+          "sas_device_handle": "",
+          "scheduler_mode": "mq-deadline",
+          "sectors": 0,
+          "sectorsize": "512",
+          "size": 8589934592.0,
+          "support_discard": "512",
+          "vendor": "0x1af4"
+        }
+      },
+      {
+        "available": true,
+        "device_id": "122615",
+        "human_readable_type": "hdd",
+        "lvs": [],
+        "path": "/dev/vde",
+        "rejected_reasons": [],
+        "sys_api": {
+          "human_readable_size": "8.00 GB",
+          "locked": 0,
+          "model": "",
+          "nr_requests": "256",
+          "partitions": {},
+          "path": "/dev/vde",
+          "removable": "0",
+          "rev": "",
+          "ro": "0",
+          "rotational": "1",
+          "sas_address": "",
+          "sas_device_handle": "",
+          "scheduler_mode": "mq-deadline",
+          "sectors": 0,
+          "sectorsize": "512",
+          "size": 8589934592.0,
+          "support_discard": "512",
+          "vendor": "0x1af4"
+        }
+      },
+      {
+        "available": false,
+        "device_id": "",
+        "human_readable_type": "hdd",
+        "lvs": [],
+        "path": "/dev/vda",
+        "rejected_reasons": ["locked"],
+        "sys_api": {
+          "human_readable_size": "42.00 GB",
+          "locked": 1,
+          "model": "",
+          "nr_requests": "256",
+          "partitions": {
+            "vda1": {
+              "holders": [],
+              "human_readable_size": "2.00 MB",
+              "sectors": "4096",
+              "sectorsize": 512,
+              "size": 2097152.0,
+              "start": "2048"
+            },
+            "vda2": {
+              "holders": [],
+              "human_readable_size": "20.00 MB",
+              "sectors": "40960",
+              "sectorsize": 512,
+              "size": 20971520.0,
+              "start": "6144"
+            },
+            "vda3": {
+              "holders": [],
+              "human_readable_size": "41.98 GB",
+              "sectors": "88033247",
+              "sectorsize": 512,
+              "size": 45073022464.0,
+              "start": "47104"
+            }
+          },
+          "path": "/dev/vda",
+          "removable": "0",
+          "rev": "",
+          "ro": "0",
+          "rotational": "1",
+          "sas_address": "",
+          "sas_device_handle": "",
+          "scheduler_mode": "mq-deadline",
+          "sectors": 0,
+          "sectorsize": "512",
+          "size": 45097156608.0,
+          "support_discard": "512",
+          "vendor": "0x1af4"
+        }
+      }
+    ],
+    "labels": [],
+    "name": "node2"
+  }
+]
index 33b3e357a68d744f55fffe2d06303442babef102..0d18470e76c818ed129fac900bd87291068f936c 100644 (file)
@@ -20,7 +20,7 @@ describe('Configuration page', () => {
     });
 
     it('should verify that selected footer increases when an entry is clicked', () => {
-      configuration.getTableSelectedCount().should('eq', 1);
+      configuration.getTableCount('selected').should('eq', 1);
     });
 
     it('should check that details table opens (w/o tab header)', () => {
@@ -54,12 +54,12 @@ describe('Configuration page', () => {
 
     it('should show only modified configurations', () => {
       configuration.filterTable('Modified', 'yes');
-      configuration.getTableFoundCount().should('eq', 1);
+      configuration.getTableCount('found').should('eq', 1);
     });
 
     it('should hide all modified configurations', () => {
       configuration.filterTable('Modified', 'no');
-      configuration.getTableFoundCount().should('gt', 1);
+      configuration.getTableCount('found').should('gt', 1);
     });
   });
 });
index d3e75400bba5a8799f0afd74e83fe7619d537c3c..c7a6b91795e1af2919d33ec84b9247dbb5612842 100644 (file)
@@ -1,10 +1,20 @@
 import { PageHelper } from '../page-helper.po';
 
+const pages = {
+  index: { url: '#/hosts', id: 'cd-hosts' },
+  create: { url: '#/hosts/create', id: 'cd-host-form' }
+};
+
 export class HostsPageHelper extends PageHelper {
-  pages = { index: { url: '#/hosts', id: 'cd-hosts' } };
+  pages = pages;
+
+  columnIndex = {
+    hostname: 2,
+    labels: 4
+  };
 
   check_for_host() {
-    this.getTableTotalCount().should('not.be.eq', 0);
+    this.getTableCount('total').should('not.be.eq', 0);
   }
 
   // function that checks all services links work for first
@@ -28,4 +38,82 @@ export class HostsPageHelper extends PageHelper {
         expect(links_tested).gt(0);
       });
   }
+
+  @PageHelper.restrictTo(pages.index.url)
+  clickHostTab(hostname: string, tabName: string) {
+    this.getExpandCollapseElement(hostname).click();
+    cy.get('cd-host-details').within(() => {
+      this.getTab(tabName).click();
+    });
+  }
+
+  @PageHelper.restrictTo(pages.create.url)
+  add(hostname: string, exist?: boolean) {
+    cy.get(`${this.pages.create.id}`).within(() => {
+      cy.get('#hostname').type(hostname);
+      cy.get('cd-submit-button').click();
+    });
+    if (exist) {
+      cy.get('#hostname').should('have.class', 'ng-invalid');
+    } else {
+      // back to host list
+      cy.get(`${this.pages.index.id}`);
+    }
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  checkExist(hostname: string, exist: boolean) {
+    this.getTableCell(this.columnIndex.hostname, hostname).should(($elements) => {
+      const hosts = $elements.map((_, el) => el.textContent).get();
+      if (exist) {
+        expect(hosts).to.include(hostname);
+      } else {
+        expect(hosts).to.not.include(hostname);
+      }
+    });
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  delete(hostname: string) {
+    super.delete(hostname, this.columnIndex.hostname);
+  }
+
+  // Add or remove labels on a host, then verify labels in the table
+  @PageHelper.restrictTo(pages.index.url)
+  editLabels(hostname: string, labels: string[], add: boolean) {
+    this.getTableCell(this.columnIndex.hostname, hostname).click();
+    this.clickActionButton('edit');
+
+    // add or remove label badges
+    if (add) {
+      cy.get('cd-modal').find('.select-menu-edit').click();
+      for (const label of labels) {
+        cy.contains('cd-modal .badge', new RegExp(`^${label}$`)).should('not.exist');
+        cy.get('.popover-body input').type(`${label}{enter}`);
+      }
+    } else {
+      for (const label of labels) {
+        cy.contains('cd-modal .badge', new RegExp(`^${label}$`))
+          .find('.badge-remove')
+          .click();
+      }
+    }
+    cy.get('cd-modal cd-submit-button').click();
+
+    // Verify labels are added or removed from Labels column
+    // First find row with hostname, then find labels in the row
+    this.getTableCell(this.columnIndex.hostname, hostname)
+      .parent()
+      .find(`datatable-body-cell:nth-child(${this.columnIndex.labels})`)
+      .should(($ele) => {
+        const newLabels = $ele.text().split(', ');
+        for (const label of labels) {
+          if (add) {
+            expect(newLabels).to.include(label);
+          } else {
+            expect(newLabels).to.not.include(label);
+          }
+        }
+      });
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/inventory.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/inventory.po.ts
new file mode 100644 (file)
index 0000000..ee8e43f
--- /dev/null
@@ -0,0 +1,22 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+  index: { url: '#/inventory', id: 'cd-inventory' }
+};
+
+export class InventoryPageHelper extends PageHelper {
+  pages = pages;
+
+  identify() {
+    // Nothing we can do, just verify the form is there
+    this.getFirstTableCell().click();
+    cy.contains('cd-table-actions button', 'Identify').click();
+    cy.get('cd-modal').within(() => {
+      cy.get('#duration').select('15 minutes');
+      cy.get('#duration').select('10 minutes');
+      cy.get('cd-back-button').click();
+    });
+    cy.get('cd-modal').should('not.visible');
+    cy.get(`${this.pages.index.id}`);
+  }
+}
index 7b610fb39d4133cc093ee10efb949cb9b994a42b..bf5c7d8420e9ad1a9442845df6d02ccdab48dbc6 100644 (file)
@@ -22,7 +22,7 @@ describe('OSDs page', () => {
 
   describe('check existence of fields on OSD page', () => {
     it('should check that number of rows and count in footer match', () => {
-      osds.getTableTotalCount().then((text) => {
+      osds.getTableCount('total').then((text) => {
         osds.getTableRows().its('length').should('equal', text);
       });
     });
@@ -38,7 +38,7 @@ describe('OSDs page', () => {
       });
 
       it('should verify that selected footer increases', () => {
-        osds.getTableSelectedCount().should('equal', 1);
+        osds.getTableCount('selected').should('equal', 1);
       });
 
       it('should show the correct text for the tab labels', () => {
index 36e0b4feb55b6bfe4f0a967fd69568e47d421f7c..7e0da4e7ea00e9b8f9ca39c10a9690718ee13f67 100644 (file)
@@ -1,5 +1,82 @@
 import { PageHelper } from '../page-helper.po';
 
+const pages = {
+  index: { url: '#/osd', id: 'cd-osd-list' },
+  create: { url: '#/osd/create', id: 'cd-osd-form' }
+};
+
 export class OSDsPageHelper extends PageHelper {
-  pages = { index: { url: '#/osd', id: 'cd-osd-list' } };
+  pages = pages;
+
+  columnIndex = {
+    id: 4,
+    status: 5
+  };
+
+  @PageHelper.restrictTo(pages.create.url)
+  create(deviceType: 'hdd' | 'ssd') {
+    // Click Primary devices Add button
+    cy.get('cd-osd-devices-selection-groups[name="Primary"]').as('primaryGroups');
+    cy.get('@primaryGroups').find('button').click();
+
+    // Select all devices with `deviceType`
+    cy.get('cd-osd-devices-selection-modal').within(() => {
+      cy.get('.modal-footer .tc_submitButton').as('addButton').should('be.disabled');
+      this.filterTable('Type', deviceType);
+      cy.get('@addButton').click();
+    });
+
+    cy.get('@primaryGroups').within(() => {
+      this.getTableCount('total').as('newOSDCount');
+    });
+
+    cy.get(`${pages.create.id} .card-footer .tc_submitButton`).click();
+    cy.get(`cd-osd-creation-preview-modal .modal-footer .tc_submitButton`).click();
+  }
+
+  getRowByID(id: number) {
+    return this.getTableCell(this.columnIndex.id, `${id}`).parent();
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  checkStatus(id: number, status: string[]) {
+    this.getRowByID(id)
+      .find(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`)
+      .should(($ele) => {
+        const allStatus = $ele.toArray().map((v) => v.innerText);
+        for (const s of status) {
+          expect(allStatus).to.include(s);
+        }
+      });
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  ensureNoOsd(id: number) {
+    cy.get(`datatable-body-row datatable-body-cell:nth-child(${this.columnIndex.id})`).should(
+      ($ele) => {
+        const osdIds = $ele.toArray().map((v) => v.innerText);
+        expect(osdIds).to.not.include(`${id}`);
+      }
+    );
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  deleteByIDs(osdIds: number[], replace?: boolean) {
+    this.getTableRows().each(($el) => {
+      const rowOSD = Number(
+        $el.find('datatable-body-cell .datatable-body-cell-label').get(this.columnIndex.id - 1)
+          .textContent
+      );
+      if (osdIds.includes(rowOSD)) {
+        cy.wrap($el).click();
+      }
+    });
+    this.clickActionButton('delete');
+    if (replace) {
+      cy.get('cd-modal label[for="preserve"]').click();
+    }
+    cy.get('cd-modal label[for="confirmation"]').click();
+    cy.contains('cd-modal button', 'Delete').click();
+    cy.get('cd-modal').should('not.exist');
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/01-hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/01-hosts.e2e-spec.ts
new file mode 100644 (file)
index 0000000..e760945
--- /dev/null
@@ -0,0 +1,57 @@
+import { HostsPageHelper } from '../cluster/hosts.po';
+
+describe('Hosts page', () => {
+  const hosts = new HostsPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    hosts.navigateTo();
+  });
+
+  describe('when Orchestrator is available', () => {
+    beforeEach(function () {
+      cy.fixture('orchestrator/inventory.json').as('hosts');
+    });
+
+    it('should not add an exsiting host', function () {
+      const hostname = Cypress._.sample(this.hosts).name;
+      hosts.navigateTo('create');
+      hosts.add(hostname, true);
+    });
+
+    it('should delete a host and add it back', function () {
+      const host = Cypress._.last(this.hosts)['name'];
+      hosts.delete(host);
+
+      // add it back
+      hosts.navigateTo('create');
+      hosts.add(host);
+      hosts.checkExist(host, true);
+    });
+
+    it('should display inventory', function () {
+      for (const host of this.hosts) {
+        hosts.clickHostTab(host.name, 'Inventory');
+        cy.get('cd-host-details').within(() => {
+          hosts.getTableCount('total').should('be.gte', 0);
+        });
+      }
+    });
+
+    it('should display daemons', function () {
+      for (const host of this.hosts) {
+        hosts.clickHostTab(host.name, 'Daemons');
+        cy.get('cd-host-details').within(() => {
+          hosts.getTableCount('total').should('be.gte', 0);
+        });
+      }
+    });
+
+    it('should edit host labels', function () {
+      const hostname = Cypress._.sample(this.hosts).name;
+      const labels = ['foo', 'bar'];
+      hosts.editLabels(hostname, labels, true);
+      hosts.editLabels(hostname, labels, false);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/02-hosts-inventory.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/02-hosts-inventory.e2e-spec.ts
new file mode 100644 (file)
index 0000000..3b74de2
--- /dev/null
@@ -0,0 +1,25 @@
+import { HostsPageHelper } from '../cluster/hosts.po';
+
+describe('Hosts page', () => {
+  const hosts = new HostsPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    hosts.navigateTo();
+  });
+
+  describe('when Orchestrator is available', () => {
+    beforeEach(function () {
+      cy.fixture('orchestrator/inventory.json').as('hosts');
+    });
+
+    it('should display correct inventory', function () {
+      for (const host of this.hosts) {
+        hosts.clickHostTab(host.name, 'Inventory');
+        cy.get('cd-host-details').within(() => {
+          hosts.getTableCount('total').should('be.eq', host.devices.length);
+        });
+      }
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/03-inventory.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/03-inventory.e2e-spec.ts
new file mode 100644 (file)
index 0000000..41dcdec
--- /dev/null
@@ -0,0 +1,25 @@
+import { InventoryPageHelper } from '../cluster/inventory.po';
+
+describe('Inventory page', () => {
+  const inventory = new InventoryPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    inventory.navigateTo();
+  });
+
+  it('should have correct devices', () => {
+    cy.fixture('orchestrator/inventory.json').then((hosts) => {
+      const totalDiskCount = Cypress._.sumBy(hosts, 'devices.length');
+      inventory.getTableCount('total').should('be.eq', totalDiskCount);
+      for (const host of hosts) {
+        inventory.filterTable('Hostname', host['name']);
+        inventory.getTableCount('found').should('be.eq', host.devices.length);
+      }
+    });
+  });
+
+  it('should identify device', () => {
+    inventory.identify();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/04-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/04-osds.e2e-spec.ts
new file mode 100644 (file)
index 0000000..f1a66e0
--- /dev/null
@@ -0,0 +1,48 @@
+import { OSDsPageHelper } from '../cluster/osds.po';
+import { DashboardPageHelper } from '../ui/dashboard.po';
+
+describe('OSDs page', () => {
+  const osds = new OSDsPageHelper();
+  const dashboard = new DashboardPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    osds.navigateTo();
+  });
+
+  describe('when Orchestrator is available', () => {
+    it('should create and delete OSDs', () => {
+      osds.getTableCount('total').as('initOSDCount');
+      osds.navigateTo('create');
+      osds.create('hdd');
+
+      cy.get('@newOSDCount').then((newCount) => {
+        cy.get('@initOSDCount').then((oldCount) => {
+          const expectedCount = Number(oldCount) + Number(newCount);
+
+          // check total rows
+          osds.expectTableCount('total', expectedCount);
+
+          // landing page is easier to check OSD status
+          dashboard.navigateTo();
+          dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} total`);
+          dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} up`);
+          dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} in`);
+
+          expect(Number(newCount)).to.be.gte(2);
+          // Delete the first OSD we created
+          osds.navigateTo();
+          const deleteOsdId = Number(oldCount);
+          osds.deleteByIDs([deleteOsdId], false);
+          osds.ensureNoOsd(deleteOsdId);
+
+          // Replace the second OSD we created
+          const replaceID = Number(oldCount) + 1;
+          osds.deleteByIDs([replaceID], true);
+          // deleting OSDs doesn't work in cephadm right now, skip checking
+          osds.checkStatus(replaceID, ['destroyed']);
+        });
+      });
+    });
+  });
+});
index b1a2fdfda3775c0a188dd6e8920c7bfbbb5c6e72..38edfc451604e5b667af1f32c7a7f1d78425b485 100644 (file)
@@ -73,6 +73,10 @@ export abstract class PageHelper {
     return cy.get('.nav.nav-tabs li');
   }
 
+  getTab(tabName: string) {
+    return cy.contains('.nav.nav-tabs li', new RegExp(`^${tabName}$`));
+  }
+
   getTabText(index: number) {
     return this.getTabs().its(index).text();
   }
@@ -123,42 +127,29 @@ export abstract class PageHelper {
     return cy.get('cd-table .dataTables_wrapper');
   }
 
-  getTableTotalCount() {
-    this.waitDataTableToLoad();
-
-    return cy.get('.datatable-footer-inner .page-count span').then(($elem) => {
-      const text = $elem
-        .filter((_i, e) => e.innerText.includes('total'))
-        .first()
-        .text();
-
-      return Number(text.match(/(\d+)\s+total/)[1]);
-    });
+  private getTableCountSpan(spanType: 'selected' | 'found' | 'total') {
+    return cy.contains('.datatable-footer-inner .page-count span', spanType);
   }
 
-  getTableSelectedCount() {
+  // Get 'selected', 'found', or 'total' row count of a table.
+  getTableCount(spanType: 'selected' | 'found' | 'total') {
     this.waitDataTableToLoad();
-
-    return cy.get('.datatable-footer-inner .page-count span').then(($elem) => {
+    return this.getTableCountSpan(spanType).then(($elem) => {
       const text = $elem
-        .filter((_i, e) => e.innerText.includes('selected'))
+        .filter((_i, e) => e.innerText.includes(spanType))
         .first()
         .text();
 
-      return Number(text.match(/(\d+)\s+selected/)[1]);
+      return Number(text.match(/(\d+)\s+\w*/)[1]);
     });
   }
 
-  getTableFoundCount() {
+  // Wait until selected', 'found', or 'total' row count of a table equal to a number.
+  expectTableCount(spanType: 'selected' | 'found' | 'total', count: number) {
     this.waitDataTableToLoad();
-
-    return cy.get('.datatable-footer-inner .page-count span').then(($elem) => {
-      const text = $elem
-        .filter((_i, e) => e.innerText.includes('found'))
-        .first()
-        .text();
-
-      return Number(text.match(/(\d+)\s+found/)[1]);
+    this.getTableCountSpan(spanType).should(($elem) => {
+      const text = $elem.first().text();
+      expect(Number(text.match(/(\d+)\s+\w*/)[1])).to.equal(count);
     });
   }
 
@@ -190,6 +181,15 @@ export abstract class PageHelper {
     }
   }
 
+  getTableCell(columnIndex: number, exactContent: string) {
+    this.waitDataTableToLoad();
+    this.seachTable(exactContent);
+    return cy.contains(
+      `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
+      new RegExp(`^${exactContent}$`)
+    );
+  }
+
   getExpandCollapseElement(content?: string) {
     this.waitDataTableToLoad();
 
@@ -226,10 +226,14 @@ export abstract class PageHelper {
     cy.contains(`.tc_filter_option .dropdown-item`, option).click();
   }
 
+  setPageSize(size: string) {
+    cy.get('cd-table .dataTables_paginate input').first().clear().type(size);
+  }
+
   seachTable(text: string) {
     this.waitDataTableToLoad();
 
-    cy.get('cd-table .dataTables_paginate input').first().clear().type('10');
+    this.setPageSize('10');
     cy.get('cd-table .search input').first().clear().type(text);
   }
 
@@ -239,18 +243,28 @@ export abstract class PageHelper {
     return cy.get('cd-table .search button').click();
   }
 
+  // Click the action button
+  clickActionButton(action: string) {
+    cy.get('.table-actions button.dropdown-toggle').first().click(); // open submenu
+    cy.get(`button.${action}`).click(); // click on "action" menu item
+  }
+
   /**
    * This is a generic method to delete table rows.
    * It will select the first row that contains the provided name and delete it.
    * After that it will wait until the row is no longer displayed.
+   * @param name The string to search in table cells.
+   * @param columnIndex If provided, search string in columnIndex column.
    */
-  delete(name: string) {
+  delete(name: string, columnIndex?: number) {
     // Selects row
-    this.getFirstTableCell(name).click();
+    const getRow = columnIndex
+      ? this.getTableCell.bind(this, columnIndex)
+      : this.getFirstTableCell.bind(this);
+    getRow(name).click();
 
     // Clicks on table Delete button
-    cy.get('.table-actions button.dropdown-toggle').first().click(); // open submenu
-    cy.get('button.delete').click(); // click on "delete" menu item
+    this.clickActionButton('delete');
 
     // Confirms deletion
     cy.get('cd-modal .custom-control-label').click();
@@ -260,6 +274,6 @@ export abstract class PageHelper {
     cy.get('cd-modal').should('not.exist');
 
     // Waits for item to be removed from table
-    this.getFirstTableCell(name).should('not.exist');
+    getRow(name).should('not.exist');
   }
 }
index 397745f9745d060253df88edad364d43c4021d45..c33112be72e6be719256e5b428e938f68695a046 100644 (file)
@@ -110,7 +110,7 @@ describe('Dashboard Main Page', () => {
         }
 
         spec.pageObject.navigateTo();
-        spec.pageObject.getTableTotalCount().then((tableCount) => {
+        spec.pageObject.getTableCount('total').then((tableCount) => {
           expect(tableCount).to.eq(
             dashCount,
             `Text of card "${spec.cardName}" and regex "${spec.regexMatcher}" resulted in ${dashCount} ` +
index 02125739723e6383ab97b5949a8f737c16e5b355..42d63ef44117c70aa1b1090715bc29846798d046 100644 (file)
@@ -24,4 +24,8 @@ export class DashboardPageHelper extends PageHelper {
   infoCardBodyText(infoCard: string) {
     return this.infoCard(infoCard).find('.card-text').text();
   }
+
+  infoCardBody(infoCard: string) {
+    return this.infoCard(infoCard).find('.card-text');
+  }
 }