From 2b9d58bba53a917bb980b66662228c09259f1aa1 Mon Sep 17 00:00:00 2001 From: yifei jin Date: Fri, 4 Dec 2020 15:20:00 -0500 Subject: [PATCH] release-1.3.0 (#14) * release-1.3.0 --- ...r for Dell EMC VxFlex OS Product Guide.pdf | Bin 207759 -> 0 bytes ...r for Dell EMC VxFlex OS Release Notes.pdf | Bin 49132 -> 0 bytes Dockerfile | 55 +- README.md | 7 +- ReleaseNotes.md | 18 + csi-vxflexos.sh | 2 +- dell-csi-helm-installer/.gitignore | 2 + dell-csi-helm-installer/README.md | 1 + dell-csi-helm-installer/common.sh | 96 +- dell-csi-helm-installer/csi-install.sh | 147 +- dell-csi-helm-installer/csi-offline-bundle.md | 219 +++ dell-csi-helm-installer/csi-offline-bundle.sh | 366 +++++ dell-csi-helm-installer/csi-uninstall.sh | 59 +- dell-csi-helm-installer/verify.sh | 265 ++-- env.sh | 3 - go.mod | 20 +- helm/csi-vxflexos/Chart.yaml | 5 +- helm/csi-vxflexos/driver-image.yaml | 11 +- helm/csi-vxflexos/k8s-1.17-values.yaml | 17 +- helm/csi-vxflexos/k8s-1.18-values.yaml | 18 +- helm/csi-vxflexos/k8s-1.19-values.yaml | 14 +- helm/csi-vxflexos/k8s-1.20-values.yaml | 23 + helm/csi-vxflexos/templates/controller.yaml | 77 +- helm/csi-vxflexos/templates/csidriver.yaml | 7 - helm/csi-vxflexos/templates/node.yaml | 170 ++- .../templates/storageclass-xfs.yaml | 1 + helm/csi-vxflexos/templates/storageclass.yaml | 1 + helm/csi-vxflexos/values.yaml | 114 +- helm/sdc-repo-secret.yaml | 11 + k8sutils/k8sutils.go | 41 + main.go | 35 +- overrides.mk | 6 + patch-notes.md | 24 - service/controller.go | 93 +- .../controller_publish_unpublish.feature | 464 +++--- service/features/delete_volume.feature | 126 +- service/features/get_system_instances.json | 2 +- service/features/list_volumes.feature | 336 ++--- .../features/node_publish_unpublish.feature | 158 +- service/features/service.feature | 1288 +++++++++-------- service/identity.go | 7 + service/service.go | 1 + service/service_test.go | 2 +- service/step_defs_test.go | 29 +- test/helm/2vols+clone/Chart.yaml | 9 + .../templates/createFromVolume.yaml | 15 + test/helm/2vols+clone/templates/pvc0.yaml | 13 + test/helm/2vols+clone/templates/pvc1.yaml | 13 + test/helm/2vols+clone/templates/test.yaml | 43 + test/helm/2vols/Chart.yaml | 1 - test/helm/postgres.sh | 42 - test/helm/postgres/.helmignore | 2 - test/helm/postgres/Chart.yaml | 21 - test/helm/postgres/OWNERS | 14 - test/helm/postgres/README.md | 278 ---- test/helm/postgres/files/README.md | 1 - test/helm/postgres/files/conf.d/README.md | 4 - .../docker-entrypoint-initdb.d/README.md | 3 - test/helm/postgres/templates/NOTES.txt | 60 - test/helm/postgres/templates/_helpers.tpl | 152 -- test/helm/postgres/templates/configmap.yaml | 26 - .../templates/extended-config-configmap.yaml | 21 - .../templates/initialization-configmap.yaml | 24 - test/helm/postgres/templates/metrics-svc.yaml | 26 - .../postgres/templates/networkpolicy.yaml | 29 - test/helm/postgres/templates/secrets.yaml | 25 - .../templates/statefulset-slaves.yaml | 211 --- test/helm/postgres/templates/statefulset.yaml | 300 ---- .../helm/postgres/templates/svc-headless.yaml | 19 - test/helm/postgres/templates/svc-read.yaml | 31 - test/helm/postgres/templates/svc.yaml | 32 - test/helm/postgres/values-production.yaml | 283 ---- test/helm/postgres/values.yaml | 289 ---- test/helm/snaprestoretest.sh | 8 +- test/helm/starttest.sh | 6 +- test/helm/verify_leader_election.sh | 240 +++ test/helm/volumeclonetest.sh | 53 + test/integration/features/integration.feature | 160 +- test/integration/integration_test.go | 11 +- test/integration/pool.yml | 2 +- test/integration/step_defs_test.go | 63 +- test/sanity/README.md | 23 +- test/sanity/run.sh | 2 +- test/sanity/secrets.yaml | 20 +- test/sanity/start_driver.sh | 6 +- test/sanity/volParams.yaml | 2 +- 86 files changed, 3406 insertions(+), 3518 deletions(-) delete mode 100644 CSI Driver for Dell EMC VxFlex OS Product Guide.pdf delete mode 100644 CSI Driver for Dell EMC VxFlex OS Release Notes.pdf create mode 100644 ReleaseNotes.md create mode 100644 dell-csi-helm-installer/.gitignore create mode 100644 dell-csi-helm-installer/csi-offline-bundle.md create mode 100755 dell-csi-helm-installer/csi-offline-bundle.sh create mode 100644 helm/csi-vxflexos/k8s-1.20-values.yaml create mode 100644 helm/sdc-repo-secret.yaml create mode 100644 k8sutils/k8sutils.go delete mode 100644 patch-notes.md create mode 100644 test/helm/2vols+clone/Chart.yaml create mode 100644 test/helm/2vols+clone/templates/createFromVolume.yaml create mode 100644 test/helm/2vols+clone/templates/pvc0.yaml create mode 100644 test/helm/2vols+clone/templates/pvc1.yaml create mode 100644 test/helm/2vols+clone/templates/test.yaml delete mode 100755 test/helm/postgres.sh delete mode 100644 test/helm/postgres/.helmignore delete mode 100644 test/helm/postgres/Chart.yaml delete mode 100644 test/helm/postgres/OWNERS delete mode 100644 test/helm/postgres/README.md delete mode 100644 test/helm/postgres/files/README.md delete mode 100644 test/helm/postgres/files/conf.d/README.md delete mode 100644 test/helm/postgres/files/docker-entrypoint-initdb.d/README.md delete mode 100644 test/helm/postgres/templates/NOTES.txt delete mode 100644 test/helm/postgres/templates/_helpers.tpl delete mode 100644 test/helm/postgres/templates/configmap.yaml delete mode 100644 test/helm/postgres/templates/extended-config-configmap.yaml delete mode 100644 test/helm/postgres/templates/initialization-configmap.yaml delete mode 100644 test/helm/postgres/templates/metrics-svc.yaml delete mode 100644 test/helm/postgres/templates/networkpolicy.yaml delete mode 100644 test/helm/postgres/templates/secrets.yaml delete mode 100644 test/helm/postgres/templates/statefulset-slaves.yaml delete mode 100644 test/helm/postgres/templates/statefulset.yaml delete mode 100644 test/helm/postgres/templates/svc-headless.yaml delete mode 100644 test/helm/postgres/templates/svc-read.yaml delete mode 100644 test/helm/postgres/templates/svc.yaml delete mode 100644 test/helm/postgres/values-production.yaml delete mode 100644 test/helm/postgres/values.yaml create mode 100755 test/helm/verify_leader_election.sh create mode 100644 test/helm/volumeclonetest.sh diff --git a/CSI Driver for Dell EMC VxFlex OS Product Guide.pdf b/CSI Driver for Dell EMC VxFlex OS Product Guide.pdf deleted file mode 100644 index 9a8f7784d1d424b98eefceef710978cea130504e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 207759 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

6&*?`NV2S3wlmEFdD*Kv59S9zwiIz;bhW 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π|tl+G4}91oUjPq1rfMW;P&s#C9(mG)};dW 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 U?fZ*qvWC}$LD2~44{G!O0IN2IX8-^I diff --git a/CSI Driver for Dell EMC VxFlex OS Release Notes.pdf b/CSI Driver for Dell EMC VxFlex OS Release Notes.pdf deleted file mode 100644 index d566724c608507b7f87f586c7e38f3f1ffb1b0df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49132 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%0D2++l~43x4KGg@Wqo&) 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 UB)}=qo5Vt5jPOQ;Z#?Ax0P+bFnE(I) diff --git a/Dockerfile b/Dockerfile index 919dbbaa..e9020889 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,16 @@ WORKDIR /go/src/ RUN CGO_ENABLED=0 \ make build +# stage to grab drv_cfg from the PowerFlex SDC package +FROM $BASEIMAGE as rpmgrabber +RUN dnf install -y \ + cpio \ + wget +# get SDC RPM file +RUN wget --no-check-certificate RPM_FILE_LINK +RUN rpm2cpio ./RPM_FILE_NAME i| cpio -idmv +RUN find /usr -name drv_cfg -exec cp -u {} /tmp \; + # Stage to build the driver image FROM $BASEIMAGE AS driver # install necessary packages @@ -20,60 +30,31 @@ FROM $BASEIMAGE AS driver RUN yum update -y && \ yum install -y \ e4fsprogs \ - kmod \ + kmod \ libaio \ libuuid \ numactl \ xfsprogs && \ yum clean all && \ - rpm -e --nodeps sqlite-libs + rpm -e --nodeps sqlite-libs ENTRYPOINT ["/csi-vxflexos.sh"] +# copy in the drv_cfg +RUN mkdir -p /bin/emc/scaleio +COPY --from=rpmgrabber /tmp/drv_cfg /bin/emc/scaleio/drv_cfg # copy in the driver COPY --from=builder /go/src/csi-vxflexos / COPY "csi-vxflexos.sh" / RUN chmod +x /csi-vxflexos.sh - -# stage to run gosec -FROM builder as gosec -RUN go get github.com/securego/gosec/cmd/gosec -RUN cd /go/src && \ - gosec ./... -# Stage to check for critical and high CVE issues via Trivy (https://github.com/aquasecurity/trivy) -# will break image build if CRITICAL issues found -# will print out all HIGH issues found -FROM driver as cvescan -# run trivy and clean up all traces after -RUN curl https://raw.githubusercontent.com/aquasecurity/trivy/master/contrib/install.sh | sh && \ - trivy fs -s CRITICAL --exit-code 1 / && \ - trivy fs -s HIGH / && \ - trivy image --reset && \ - rm ./bin/trivy - -# Stage to run antivirus scans via clamav (https://www.clamav.net/)) -# will break image build if anything found -FROM driver as virusscan -# run trivy and clean up all traces after -RUN curl -o sqlite.rpm http://mirror.centos.org/centos/8/BaseOS/x86_64/os/Packages/sqlite-libs-3.26.0-6.el8.x86_64.rpm && \ - rpm -iv sqlite.rpm && \ - cd /etc/pki/ca-trust/source/anchors && curl -o dell.crt http://pki.dell.com/linux/dellca2018-bundle.crt && \ - curl -o emc.crt http://aia.dell.com/int/root/emcroot.crt && update-ca-trust && cd / && \ - yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \ - yum install -y clamav clamav-update && \ - freshclam && \ - clamscan -r -i --exclude-dir=/sys / && \ - yum erase -y clamav clamav-update epel-release - # final stage # simple stage to use the driver image as the resultant image -FROM driver as final +FROM driver as final + LABEL vendor="Dell Inc." \ name="csi-powerflex" \ summary="CSI Driver for Dell EMC PowerFlex" \ description="CSI Driver for provisioning persistent storage from Dell EMC PowerFlex" \ - version="1.2.0" \ + version="1.3.0" \ license="Apache-2.0" COPY ./licenses /licenses - - diff --git a/README.md b/README.md index 1e8a9c40..29b64f84 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ This project may be compiled as a stand-alone binary using Golang that, when run, provides a valid CSI endpoint. This project can also be built as a Golang plug-in in order to extend the functionality of other programs. +For Documentation, please go to [Dell CSI Driver Documentation](https://dell.github.io/storage-plugin-docs/). + ## Support The CSI Driver for Dell EMC PowerFlex image, which is the built driver code, is available on Dockerhub and is officially supported by Dell EMC. @@ -20,9 +22,6 @@ The source code for CSI Driver for Dell EMC PowerFlex available on Github is uns For any CSI driver issues, questions or feedback, join the [Dell EMC Container community](https://www.dell.com/community/Containers/bd-p/Containers). -## Patches -Patch notes for the CSI Driver for Dell EMC PowerFlex are described in [patch-notes.md](patch-notes.md) - ## Building This project is a Go module (see golang.org Module information for explanation). @@ -32,7 +31,7 @@ To build the source, execute `make clean build`. To run unit tests, execute `make unit-test`. -To build a docker image, execute `make docker`. +To build a docker image, edit Dockerfile. Replace RPM_FILE_LINK with SDC rpm file download link and replace RPM_FILE_NAME with downloaded file name. Then execute `make docker`. You can run an integration test on a Linux system by populating the file `env.sh` with values for your PowerFlex system and then run "make integration-test". diff --git a/ReleaseNotes.md b/ReleaseNotes.md new file mode 100644 index 00000000..fa53587d --- /dev/null +++ b/ReleaseNotes.md @@ -0,0 +1,18 @@ +# Release Notes - CSI PowerFlex v1.3.0 + +## New Features/Changes +- Added support for OpenShift 4.5/4.6 with RHEL and CoreOS worker nodes +- Added automatic SDC deployment on OpenShift CoreOS nodes +- Added support for Red Hat Enterprise Linux (RHEL) 7.9 +- Added support for Ubuntu 20.04 +- Added support for volume cloning +- Added support for Controller high availability (multiple-controllers) + +## Fixed Issues +There are no fixed issues in this release. + +## Known Issues + +| Issue | Workaround | +|-------|------------| +| Slow volume attached/detach | If your Kubernetes 1.17 or 1.18 cluster has a lot of VolumeAttachment objects, the attach/detach operations will be very slow. This is a known issue and affects all CSI plugins. It is tracked here: CSI VolumeAttachment slows pod startup time. To get around this problem you can upgrade to latest Kubernetes/OpenShift patches, which contains a partial fix: 1.17.8+, 1.18.5+| diff --git a/csi-vxflexos.sh b/csi-vxflexos.sh index a4b9cc79..368f2fdd 100755 --- a/csi-vxflexos.sh +++ b/csi-vxflexos.sh @@ -11,4 +11,4 @@ else fi [ -e $socket_file ] && rm $socket_file fi -exec "/csi-vxflexos" +exec "/csi-vxflexos" "$@" diff --git a/dell-csi-helm-installer/.gitignore b/dell-csi-helm-installer/.gitignore new file mode 100644 index 00000000..f77bf861 --- /dev/null +++ b/dell-csi-helm-installer/.gitignore @@ -0,0 +1,2 @@ +images.manifest +images.tar diff --git a/dell-csi-helm-installer/README.md b/dell-csi-helm-installer/README.md index 211717a4..57253b03 100644 --- a/dell-csi-helm-installer/README.md +++ b/dell-csi-helm-installer/README.md @@ -20,6 +20,7 @@ Installing any of the Dell EMC CSI Drivers requires a few utilities to be instal | ------------- | ----- | | `kubectl` | Kubectl is used to validate that the Kubernetes system meets the requirements of the driver. | | `helm` | Helm v3 is used as the deployment tool for Charts. See, [Install HELM 3](https://helm.sh/docs/intro/install/) for instructions to install HELM 3. | +| `sshpass` | sshpass is used to check certain pre-requisities in worker nodes (in chosen drivers). | In order to use these tools, a valid `KUBECONFIG` is required. Ensure that either a valid configuration is in the default location or that the `KUBECONFIG` environment variable points to a valid confiugration before using these tools. diff --git a/dell-csi-helm-installer/common.sh b/dell-csi-helm-installer/common.sh index f4b8730e..215bc280 100644 --- a/dell-csi-helm-installer/common.sh +++ b/dell-csi-helm-installer/common.sh @@ -16,27 +16,44 @@ YELLOW='\033[1;33m' DARK_GRAY='\033[1;30m' NC='\033[0m' # No Color +function decho() { + if [ -n "${DEBUGLOG}" ]; then + echo "$@" | tee -a "${DEBUGLOG}" + fi +} + +function debuglog_only() { + if [ -n "${DEBUGLOG}" ]; then + echo "$@" >> "${DEBUGLOG}" + fi +} + function log() { case $1 in separator) - echo "------------------------------------------------------" + decho "------------------------------------------------------" ;; error) - echo + decho log separator printf "${RED}Error: $2\n" printf "${RED}Installation cannot continue${NC}\n" + debuglog_only "Error: $2" + debuglog_only "Installation cannot continue" exit 1 ;; step) printf "|\n|- %-65s" "$2" + debuglog_only "${2}" ;; small_step) printf "%-61s" "$2" + debuglog_only "${2}" ;; section) log separator printf "> %s\n" "$2" + debuglog_only "${2}" log separator ;; smart_step) @@ -91,7 +108,7 @@ function get_drivers() { D="${1}" TTT=$(pwd) while read -r line; do - DDD=$(echo $line | awk -F '/' '{print $(NF-1)}') + DDD=$(decho $line | awk -F '/' '{print $(NF-1)}') VALIDDRIVERS+=("$DDD") done < <(find "${D}" -maxdepth 2 -type f -name Chart.yaml | sort) } @@ -104,11 +121,80 @@ function get_drivers() { function get_release_name() { local D="${1}" if [ ! -z "${RELEASE}" ]; then - echo "${RELEASE}" + decho "${RELEASE}" return fi local PREFIX="csi-" R=${D#"$PREFIX"} - echo "${R}" + decho "${R}" } + +function run_command() { + local RC=0 + if [ -n "${DEBUGLOG}" ]; then + local ME=$(basename "${0}") + echo "---------------" >> "${DEBUGLOG}" + echo "${ME}:${BASH_LINENO[0]} - Running command: $@" >> "${DEBUGLOG}" + debuglog_only "Results:" + eval "$@" | tee -a "${DEBUGLOG}" + RC=${PIPESTATUS[0]} + echo "---------------" >> "${DEBUGLOG}" + else + eval "$@" + RC=$? + fi + return $RC +} + +# dump out information about a helm chart to the debug file +# takes a few arguments +# $1 the namespace +# $2 the release +function debuglog_helm_status() { + local NS="${1}" + local RLS="${2}" + + debuglog_only "Getting information about Helm release: ${RLS}" + debuglog_only "****************" + debuglog_only "Helm Status:" + helm status "${RLS}" -n "${NS}" >> "${DEBUGLOG}" + debuglog_only "****************" + debuglog_only "Manifest" + helm get manifest "${RLS}" -n "${NS}" >> "${DEBUGLOG}" + debuglog_only "****************" + debuglog_only "Status of resources" + helm get manifest "${RLS}" -n "${NS}" | kubectl get -f - >> "${DEBUGLOG}" + +} + +# determines if the current KUBECONFIG is pointing to an OpenShift cluster +# echos "true" or "false" +function isOpenShift() { + # check if the securitycontextconstraints.security.openshift.io crd exists + run_command kubectl get crd | grep securitycontextconstraints.security.openshift.io --quiet >/dev/null 2>&1 + local O=$? + if [[ ${O} == 0 ]]; then + # this is openshift + echo "true" + else + echo "false" + fi +} + +# determines the version of OpenShift +# echos version, or empty string if not OpenShift +function OpenShiftVersion() { + # check if this is OpenShift + local O=$(isOpenShift) + if [ "${O}" == "false" ]; then + # this is not openshift + echo "" + else + local V=$(run_command kubectl get clusterversions -o jsonpath="{.items[*].status.desired.version}") + local MAJOR=$(echo "${V}" | awk -F '.' '{print $1}') + local MINOR=$(echo "${V}" | awk -F '.' '{print $2}') + echo "${MAJOR}.${MINOR}" + fi +} + diff --git a/dell-csi-helm-installer/csi-install.sh b/dell-csi-helm-installer/csi-install.sh index 5445960c..7c14fa06 100755 --- a/dell-csi-helm-installer/csi-install.sh +++ b/dell-csi-helm-installer/csi-install.sh @@ -16,35 +16,40 @@ PROG="${0}" NODE_VERIFY=1 VERIFY=1 MODE="install" +WATCHLIST="" # version of Snapshot CRD to install. Default is none ("") INSTALL_CRD="" - +# export the name of the debug log, so child processes will see it +export DEBUGLOG="${SCRIPTDIR}/install-debug.log" declare -a VALIDDRIVERS source "$SCRIPTDIR"/common.sh +if [ -f "${DEBUGLOG}" ]; then + rm -f "${DEBUGLOG}" +fi # # usage will print command execution help and then exit function usage() { - echo - echo "Help for $PROG" - echo - echo "Usage: $PROG options..." - echo "Options:" - echo " Required" - echo " --namespace[=] Kubernetes namespace containing the CSI driver" - echo " --values[=] Values file, which defines configuration values" - - echo " Optional" - echo " --release[=] Name to register with helm, default value will match the driver name" - echo " --upgrade Perform an upgrade of the specified driver, default is false" - echo " --node-verify-user[=] Username to SSH to worker nodes as, used to validate node requirements. Default is root" - echo " --skip-verify Skip the kubernetes configuration verification to use the CSI driver, default will run verification" - echo " --skip-verify-node Skip worker node verification checks" - echo " --snapshot-crd Install snapshot CRDs. Default will not install Snapshot classes." - echo " -h Help" - echo + decho + decho "Help for $PROG" + decho + decho "Usage: $PROG options..." + decho "Options:" + decho " Required" + decho " --namespace[=] Kubernetes namespace containing the CSI driver" + decho " --values[=] Values file, which defines configuration values" + + decho " Optional" + decho " --release[=] Name to register with helm, default value will match the driver name" + decho " --upgrade Perform an upgrade of the specified driver, default is false" + decho " --node-verify-user[=] Username to SSH to worker nodes as, used to validate node requirements. Default is root" + decho " --skip-verify Skip the kubernetes configuration verification to use the CSI driver, default will run verification" + decho " --skip-verify-node Skip worker node verification checks" + decho " --snapshot-crd Install snapshot CRDs. Default will not install Snapshot classes." + decho " -h Help" + decho exit 0 } @@ -54,17 +59,17 @@ function warning() { log separator printf "${YELLOW}WARNING:${NC}\n" for N in "$@"; do - echo $N + decho $N done - echo + decho if [ "${ASSUMEYES}" == "true" ]; then - echo "Continuing as '-Y' argument was supplied" + decho "Continuing as '-Y' argument was supplied" return fi read -n 1 -p "Press 'y' to continue or any other key to exit: " CONT - echo + decho if [ "${CONT}" != "Y" -a "${CONT}" != "y" ]; then - echo "quitting at user request" + decho "quitting at user request" exit 2 fi } @@ -79,8 +84,10 @@ function header() { # check_for_driver will see if the driver is already installed within the namespace provided function check_for_driver() { log section "Checking to see if CSI Driver is already installed" - NUM=$(helm list --namespace "${NS}" | grep "^${RELEASE}\b" | wc -l) + NUM=$(run_command helm list --namespace "${NS}" | grep "^${RELEASE}\b" | wc -l) if [ "${1}" == "install" -a "${NUM}" != "0" ]; then + # grab the status of the existing chart release + debuglog_helm_status "${NS}" "${RELEASE}" log error "The CSI Driver is already installed" fi if [ "${1}" == "upgrade" -a "${NUM}" == "0" ]; then @@ -93,31 +100,31 @@ function check_for_driver() { function validate_params() { # make sure the driver was specified if [ -z "${DRIVER}" ]; then - echo "No driver specified" + decho "No driver specified" usage exit 1 fi # make sure the driver name is valid if [[ ! "${VALIDDRIVERS[@]}" =~ "${DRIVER}" ]]; then - echo "Driver: ${DRIVER} is invalid." - echo "Valid options are: ${VALIDDRIVERS[@]}" + decho "Driver: ${DRIVER} is invalid." + decho "Valid options are: ${VALIDDRIVERS[@]}" usage exit 1 fi # the namespace is required if [ -z "${NS}" ]; then - echo "No namespace specified" + decho "No namespace specified" usage exit 1 fi # values file if [ -z "${VALUES}" ]; then - echo "No values file was specified" + decho "No values file was specified" usage exit 1 fi if [ ! -f "${VALUES}" ]; then - echo "Unable to read values file at: ${VALUES}" + decho "Unable to read values file at: ${VALUES}" usage exit 1 fi @@ -133,18 +140,27 @@ function install_driver() { fi HELMOUTPUT="/tmp/csi-install.$$.out" - helm ${1} --values "${DRIVERDIR}/${DRIVER}/k8s-${kMajorVersion}.${kMinorVersion}-values.yaml" --values "${DRIVERDIR}/${DRIVER}/driver-image.yaml" --values "${VALUES}" --namespace ${NS} "${RELEASE}" "${DRIVERDIR}/${DRIVER}" >"${HELMOUTPUT}" 2>&1 + run_command helm ${1} \ + --set openshift=${OPENSHIFT} \ + --values "${DRIVERDIR}/${DRIVER}/k8s-${kMajorVersion}.${kMinorVersion}-values.yaml" \ + --values "${DRIVERDIR}/${DRIVER}/driver-image.yaml" \ + --values "${VALUES}" \ + --namespace ${NS} "${RELEASE}" \ + "${DRIVERDIR}/${DRIVER}" >"${HELMOUTPUT}" 2>&1 + if [ $? -ne 0 ]; then cat "${HELMOUTPUT}" log error "Helm operation failed, output can be found in ${HELMOUTPUT}. The failure should be examined, before proceeding. Additionally, running csi-uninstall.sh may be needed to clean up partial deployments." fi log step_success + getWhatToWatch "${NS}" "${RELEASE}" # wait for the deployment to finish, use the default timeout - waitOnRunning "${NS}" "statefulset ${RELEASE}-controller,daemonset ${RELEASE}-node" + waitOnRunning "${NS}" "${WATCHLIST}" if [ $? -eq 1 ]; then warning "Timed out waiting for the operation to complete." \ "This does not indicate a fatal error, pods may take a while to start." \ "Progress can be checked by running \"kubectl get pods -n ${NS}\"" + debuglog_helm_status "${NS}" "${RELEASE}" fi } @@ -153,6 +169,40 @@ function summary() { log section "Operation complete" } +# getWhatToWatch +# will retrieve the list of statefulsets, deployments, and daemonsets running in a target namespace +# and sets a global variable formatted such that it can be passed to waitOnRunning to monitor the rollout +# +# This expects resources to be named with a prefix of the helm release name +# +# expects two argumnts: +# $1: required: namespace +# $2: required: helm release name +function getWhatToWatch() { + if [ -z "${2}" ]; then + decho "No namespace and/or helm release name were supplied These fields are required for getWhatToWatch" + exit 1 + fi + + local NS="${1}" + local RN="${2}" + + for T in StatefulSet Deployment DaemonSet; do + ALL=$(run_command kubectl -n "${NS}" get "${T}" -o jsonpath="{.items[*].metadata.name}") + for ENTITY in $ALL; do + if [[ "${ENTITY}" == ${RN}-* ]]; then + if [ "${ENTITY}" != "" ]; then + if [ "${WATCHLIST}" != "" ]; then + WATCHLIST="${WATCHLIST}," + fi + WATCHLIST="${WATCHLIST}${T} ${ENTITY}" + fi + fi + done + done + +} + # waitOnRunning # will wait, for a timeout period, for a number of pods to go into Running state within a namespace # arguments: @@ -162,7 +212,7 @@ function summary() { # $3: optional: timeout value, 300 seconds is the default. function waitOnRunning() { if [ -z "${2}" ]; then - echo "No namespace and/or list of deployments was supplied. This field is required for waitOnRunning" + decho "No namespace and/or list of deployments was supplied. This field is required for waitOnRunning" return 1 fi # namespace @@ -179,7 +229,7 @@ function waitOnRunning() { for D in "${PODS[@]}"; do log arrow log smart_step "Waiting for $D to be ready" "small" - kubectl -n "${NS}" rollout status --timeout=${TIMEOUT}s ${D} >/dev/null 2>&1 + run_command kubectl -n "${NS}" rollout status --timeout=${TIMEOUT}s ${D} >/dev/null 2>&1 if [ $? -ne 0 ]; then error=1 log step_failure @@ -198,7 +248,10 @@ function kubectl_safe() { eval "kubectl $1" exitcode=$? if [[ $exitcode != 0 ]]; then - echo "$2" + decho "$2" + decho "Command was: kubectl $1" + decho "Output was:" + eval "kubectl $1" exit $exitcode fi } @@ -226,7 +279,7 @@ function install_snapshot_crd() { if [[ $? -ne 0 ]]; then # make sure CRD exists if [ ! -f "${SNAPCLASSDIR}/${SNAPCLASSES[$C]}" ]; then - echo "Unable to to find Snapshot Classes at ${SNAPCLASSDIR}" + decho "Unable to to find Snapshot Classes at ${SNAPCLASSDIR}" exit 1 fi # create the custom resource @@ -244,7 +297,7 @@ function install_snapshot_crd() { function verify_kubernetes() { EXTRA_OPTS="" if [ $VERIFY -eq 0 ]; then - echo "Skipping verification at user request" + decho "Skipping verification at user request" else if [ $NODE_VERIFY -eq 0 ]; then EXTRA_OPTS="$EXTRA_OPTS --skip-verify-node" @@ -336,8 +389,8 @@ while getopts ":h-:" optchar; do HODEUSER=${OPTARG#*=} ;; *) - echo "Unknown option --${OPTARG}" - echo "For help, run $PROG -h" + decho "Unknown option --${OPTARG}" + decho "For help, run $PROG -h" exit 1 ;; esac @@ -346,8 +399,8 @@ while getopts ":h-:" optchar; do usage ;; *) - echo "Unknown option -${OPTARG}" - echo "For help, run $PROG -h" + decho "Unknown option -${OPTARG}" + decho "For help, run $PROG -h" exit 1 ;; esac @@ -360,18 +413,20 @@ NODEUSER="${NODEUSER:-root}" # make sure kubectl is available kubectl --help >&/dev/null || { - echo "kubectl required for installation... exiting" + decho "kubectl required for installation... exiting" exit 2 } # make sure helm is available helm --help >&/dev/null || { - echo "helm required for installation... exiting" + decho "helm required for installation... exiting" exit 2 } +OPENSHIFT=$(isOpenShift) + # Get the kubernetes major and minor version numbers. -kMajorVersion=$(kubectl version | grep 'Server Version' | sed -e 's/^.*Major:"//' -e 's/[^0-9].*//g') -kMinorVersion=$(kubectl version | grep 'Server Version' | sed -e 's/^.*Minor:"//' -e 's/[^0-9].*//g') +kMajorVersion=$(run_command kubectl version | grep 'Server Version' | sed -e 's/^.*Major:"//' -e 's/[^0-9].*//g') +kMinorVersion=$(run_command kubectl version | grep 'Server Version' | sed -e 's/^.*Minor:"//' -e 's/[^0-9].*//g') # validate the parameters passed in validate_params "${MODE}" diff --git a/dell-csi-helm-installer/csi-offline-bundle.md b/dell-csi-helm-installer/csi-offline-bundle.md new file mode 100644 index 00000000..bce4692c --- /dev/null +++ b/dell-csi-helm-installer/csi-offline-bundle.md @@ -0,0 +1,219 @@ +# Offline Installation of Dell EMC CSI Storage Providers + +## Description + +The `csi-offline-bundle.sh` script can be used to create a package usable for offline installation of the Dell EMC CSI Storage Providers, via either Helm +or the Dell CSI Operator. + +This includes the following drivers: +* [PowerFlex](https://github.com/dell/csi-vxflexos) +* [PowerMax](https://github.com/dell/csi-powermax) +* [PowerScale](https://github.com/dell/csi-powerscale) +* [PowerStore](https://github.com/dell/csi-powerstore) +* [Unity](https://github.com/dell/csi-unity) + +As well as the Dell CSI Operator +* [Dell CSI Operator](https://github.com/dell/dell-csi-operator) + +## Dependencies + +Multiple linux based systems may be required to create and process an offline bundle for use. +* One linux based system, with internet access, will be used to create the bundle. This involved the user cloning a git repository hosted on github.com and then invoking a script that utilizes `docker` or `podman` to pull and save container images to file. +* One linux based system, with access to an image registry, to invoke a script that uses `docker` or `podman` to restore container images from file and push them to a registry + +If one linux system has both internet access and access to an internal registry, that system can be used for both steps. + +Preparing an offline bundle requires the following utilities: + +| Dependency | Usage | +| --------------------- | ----- | +| `docker` or `podman` | `docker` or `podman` will be used to pull images from public image registries, tag them, and push them to a private registry. | +| | One of these will be required on both the system building the offline bundle as well as the system preparing for installation. | +| | Tested version(s) are `docker` 19.03+ and `podman` 1.6.4+ +| `git` | `git` will be used to manually clone one of the above repos in order to create and offline bundle. +| | This is only needed on the system preparing the offline bundle. +| | Tested version(s) are `git` 1.8+ but any version should work. + +## Workflow + +To perform an offline installation of a driver or the Operator, the following steps should be performed: +1. Build an offline bundle +2. Unpacking an offline bundle and preparing for installation +3. Perform either a Helm installation or Operator installation + +### Building an offline bundle + +This needs to be performed on a linux system with access to the internet as a git repo will need to be cloned, and container images pulled from public registries. + +The build an offline bundle, the following steps are needed: +1. Perform a `git clone` of the desired repository. For a helm based install, the specific driver repo should be cloned. For an Operator based deployment, the Dell CSI Operator repo should be cloned +2. Run the `csi-offline-bundle.sh` script with an argument of `-c` in order to create an offline bundle + - For Helm installs, the `csi-offline-bundle.sh` script will be found in the `dell-csi-helm-installer` directory + - For Operator installs, the `csi-offline-bundle.sh` script will be found in the `scripts` directory + +The script will perform the following steps: + - Determine required images by parsing either the driver Helm charts (if run from a cloned CSI Driver git repository) or the Dell CSI Operator configuration files (if run from a clone of the Dell CSI Operator repository) + - Perform an image `pull` of each image required + - Save all required images to a file by running `docker save` or `podman save` + - Build a `tar.gz` file containing the images as well as files required to installer the driver and/or Operator + +The resulting offline bundle file can be copied to another machine, if necessary, to gain access to the desired image registry. + +For example, here is the output of a request to build an offline bundle for the Dell CSI Operator: +``` +[user@anothersystem /home/user]# git clone https://github.com/dell/dell-csi-operator.git + +``` +``` +[user@anothersystem /home/user]# cd dell-csi-operator +``` +``` +[user@system /home/user/dell-csi-operator]# scripts/csi-offline-bundle.sh -c + +* +* Building image manifest file + + +* +* Pulling container images + + dellemc/csi-isilon:v1.2.0 + dellemc/csi-isilon:v1.3.0.000R + dellemc/csipowermax-reverseproxy:v1.0.0.000R + dellemc/csi-powermax:v1.2.0.000R + dellemc/csi-powermax:v1.4.0.000R + dellemc/csi-powerstore:v1.1.0.000R + dellemc/csi-unity:v1.3.0.000R + dellemc/csi-vxflexos:v1.1.5.000R + dellemc/csi-vxflexos:v1.2.0.000R + dellemc/dell-csi-operator:v1.1.0.000R + quay.io/k8scsi/csi-attacher:v2.0.0 + quay.io/k8scsi/csi-attacher:v2.2.0 + quay.io/k8scsi/csi-node-driver-registrar:v1.2.0 + quay.io/k8scsi/csi-provisioner:v1.4.0 + quay.io/k8scsi/csi-provisioner:v1.6.0 + quay.io/k8scsi/csi-resizer:v0.5.0 + quay.io/k8scsi/csi-snapshotter:v2.1.1 + +* +* Saving images + + +* +* Copying necessary files + + /dell/git/dell-csi-operator/config + /dell/git/dell-csi-operator/deploy + /dell/git/dell-csi-operator/samples + /dell/git/dell-csi-operator/scripts + /dell/git/dell-csi-operator/README.md + /dell/git/dell-csi-operator/LICENSE + +* +* Compressing release + +dell-csi-operator-bundle/ +dell-csi-operator-bundle/samples/ +... +

+... +dell-csi-operator-bundle/LICENSE +dell-csi-operator-bundle/README.md + +* +* Complete + +Offline bundle file is: /dell/git/dell-csi-operator/dell-csi-operator-bundle.tar.gz + +``` + +### Unpacking an offline bundle and preparing for installation + +This needs to be performed on a linux system with access to an image registry that will host container images. If the registry requires `login`, that should be done before proceeding. + +To prepare for driver or Operator installation, the following steps need to be performed: +1. Copy the offline bundle file to a system with access to an image registry available to your Kubernetes/OpenShift cluster +2. Expand the bundle file by running `tar xvfz ` +3. Run the `csi-offline-bundle.sh` script and supply the `-p` option as well as the path to the internal registry with the `-r` option + +The script will then perform the following steps: + - Load the required container images into the local system + - Tag the images according to the user supplied registry information + - Push the newly tagged images to the registry + - Modify the Helm charts or Operator configuration to refer to the newly tagged/pushed images + + +An example of preparing the bundle for installation (192.168.75.40:5000 refers to a image registry accessible to Kubernetes/OpenShift): +``` +[user@anothersystem /tmp]# tar xvfz dell-csi-operator-bundle.tar.gz +dell-csi-operator-bundle/ +dell-csi-operator-bundle/samples/ +... ++... +dell-csi-operator-bundle/LICENSE +dell-csi-operator-bundle/README.md +``` +``` +[user@anothersystem /tmp]# cd dell-csi-operator-bundle +``` +``` +[user@anothersystem /tmp/dell-csi-operator-bundle]# scripts/csi-offline-bundle.sh -p -r 192.168.75.40:5000/operator +Preparing a offline bundle for installation + +* +* Loading docker images + + +* +* Tagging and pushing images + + dellemc/csi-isilon:v1.2.0 -> 192.168.75.40:5000/operator/csi-isilon:v1.2.0 + dellemc/csi-isilon:v1.3.0.000R -> 192.168.75.40:5000/operator/csi-isilon:v1.3.0.000R + dellemc/csipowermax-reverseproxy:v1.0.0.000R -> 192.168.75.40:5000/operator/csipowermax-reverseproxy:v1.0.0.000R + dellemc/csi-powermax:v1.2.0.000R -> 192.168.75.40:5000/operator/csi-powermax:v1.2.0.000R + dellemc/csi-powermax:v1.4.0.000R -> 192.168.75.40:5000/operator/csi-powermax:v1.4.0.000R + dellemc/csi-powerstore:v1.1.0.000R -> 192.168.75.40:5000/operator/csi-powerstore:v1.1.0.000R + dellemc/csi-unity:v1.3.0.000R -> 192.168.75.40:5000/operator/csi-unity:v1.3.0.000R + dellemc/csi-vxflexos:v1.1.5.000R -> 192.168.75.40:5000/operator/csi-vxflexos:v1.1.5.000R + dellemc/csi-vxflexos:v1.2.0.000R -> 192.168.75.40:5000/operator/csi-vxflexos:v1.2.0.000R + dellemc/dell-csi-operator:v1.1.0.000R -> 192.168.75.40:5000/operator/dell-csi-operator:v1.1.0.000R + quay.io/k8scsi/csi-attacher:v2.0.0 -> 192.168.75.40:5000/operator/csi-attacher:v2.0.0 + quay.io/k8scsi/csi-attacher:v2.2.0 -> 192.168.75.40:5000/operator/csi-attacher:v2.2.0 + quay.io/k8scsi/csi-node-driver-registrar:v1.2.0 -> 192.168.75.40:5000/operator/csi-node-driver-registrar:v1.2.0 + quay.io/k8scsi/csi-provisioner:v1.4.0 -> 192.168.75.40:5000/operator/csi-provisioner:v1.4.0 + quay.io/k8scsi/csi-provisioner:v1.6.0 -> 192.168.75.40:5000/operator/csi-provisioner:v1.6.0 + quay.io/k8scsi/csi-resizer:v0.5.0 -> 192.168.75.40:5000/operator/csi-resizer:v0.5.0 + quay.io/k8scsi/csi-snapshotter:v2.1.1 -> 192.168.75.40:5000/operator/csi-snapshotter:v2.1.1 + +* +* Preparing operator files within /tmp/dell-csi-operator-bundle + + changing: dellemc/csi-isilon:v1.2.0 -> 192.168.75.40:5000/operator/csi-isilon:v1.2.0 + changing: dellemc/csi-isilon:v1.3.0.000R -> 192.168.75.40:5000/operator/csi-isilon:v1.3.0.000R + changing: dellemc/csipowermax-reverseproxy:v1.0.0.000R -> 192.168.75.40:5000/operator/csipowermax-reverseproxy:v1.0.0.000R + changing: dellemc/csi-powermax:v1.2.0.000R -> 192.168.75.40:5000/operator/csi-powermax:v1.2.0.000R + changing: dellemc/csi-powermax:v1.4.0.000R -> 192.168.75.40:5000/operator/csi-powermax:v1.4.0.000R + changing: dellemc/csi-powerstore:v1.1.0.000R -> 192.168.75.40:5000/operator/csi-powerstore:v1.1.0.000R + changing: dellemc/csi-unity:v1.3.0.000R -> 192.168.75.40:5000/operator/csi-unity:v1.3.0.000R + changing: dellemc/csi-vxflexos:v1.1.5.000R -> 192.168.75.40:5000/operator/csi-vxflexos:v1.1.5.000R + changing: dellemc/csi-vxflexos:v1.2.0.000R -> 192.168.75.40:5000/operator/csi-vxflexos:v1.2.0.000R + changing: dellemc/dell-csi-operator:v1.1.0.000R -> 192.168.75.40:5000/operator/dell-csi-operator:v1.1.0.000R + changing: quay.io/k8scsi/csi-attacher:v2.0.0 -> 192.168.75.40:5000/operator/csi-attacher:v2.0.0 + changing: quay.io/k8scsi/csi-attacher:v2.2.0 -> 192.168.75.40:5000/operator/csi-attacher:v2.2.0 + changing: quay.io/k8scsi/csi-node-driver-registrar:v1.2.0 -> 192.168.75.40:5000/operator/csi-node-driver-registrar:v1.2.0 + changing: quay.io/k8scsi/csi-provisioner:v1.4.0 -> 192.168.75.40:5000/operator/csi-provisioner:v1.4.0 + changing: quay.io/k8scsi/csi-provisioner:v1.6.0 -> 192.168.75.40:5000/operator/csi-provisioner:v1.6.0 + changing: quay.io/k8scsi/csi-resizer:v0.5.0 -> 192.168.75.40:5000/operator/csi-resizer:v0.5.0 + changing: quay.io/k8scsi/csi-snapshotter:v2.1.1 -> 192.168.75.40:5000/operator/csi-snapshotter:v2.1.1 + +* +* Complete + +``` + +### Perform either a Helm installation or Operator installation + +Now that the required images have been made available and the Helm Charts/Operator configuration updated, installation can proceed by following the instructions that are documented within the driver or Operator repo. + + diff --git a/dell-csi-helm-installer/csi-offline-bundle.sh b/dell-csi-helm-installer/csi-offline-bundle.sh new file mode 100755 index 00000000..997b4c0d --- /dev/null +++ b/dell-csi-helm-installer/csi-offline-bundle.sh @@ -0,0 +1,366 @@ +#!/bin/bash +# +# Copyright (c) 2020 Dell Inc., or its subsidiaries. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +# bundle a CSI driver helm chart, installation scripts, and +# container images into a tarball that can be used for offline installations + +# display some usage information +usage() { + echo + echo "$0" + echo "Make a package for offline installation of a CSI driver" + echo + echo "Arguments:" + echo "-c Create an offline bundle" + echo "-p Prepare this bundle for installation" + echo "-r Required if preparing offline bundle with '-p'" + echo " Supply the registry name/path which will hold the images" + echo " For example: my.registry.com:5000/dell/csi" + echo "-h Displays this information" + echo + echo "Exactly one of '-c' or '-p' needs to be specified" + echo +} + +# status +# echos a brief status sttement to stdout +status() { + echo + echo "*" + echo "* $@" + echo +} + +# run_command +# runs a shell command +# exits and prints stdout/stderr when a non-zero return code occurs +run_command() { + CMDOUT=$(eval "${@}" 2>&1) + local rc=$? + + if [ $rc -ne 0 ]; then + echo + echo "ERROR" + echo "Received a non-zero return code ($rc) from the following comand:" + echo " ${@}" + echo + echo "Output was:" + echo "${CMDOUT}" + echo + echo "Exiting" + exit 1 + fi +} + +# build_image_manifest +# builds a manifest of all the images referred to by the helm chart +build_image_manifest() { + local REGEX="([-_./:A-Za-z0-9]{3,}):([-_.A-Za-z0-9]{1,})" + + status "Building image manifest file" + if [ -e "${IMAGEFILEDIR}" ]; then + rm -rf "${IMAGEFILEDIR}" + fi + if [ -f "${IMAGEMANIFEST}" ]; then + rm -rf "${IMAGEMANIFEST}" + fi + + for D in ${DIRS_FOR_IMAGE_NAMES[@]}; do + echo " Processing files in ${D}" + if [ ! -d "${D}" ]; then + echo "Unable to find directory, ${D}. Skipping" + else + # look for strings that appear to be image names, this will + # - search all files in a diectory looking for strings that make $REGEX + # - exclude anything with double '//'' as that is a URL and not an image name + # - make sure at least one '/' is found + find "${D}" -type f -exec egrep -oh "${REGEX}" {} \; | egrep -v '//' | egrep '/' >> "${IMAGEMANIFEST}.tmp" + fi + done + + # sort and uniqify the list + cat "${IMAGEMANIFEST}.tmp" | sort | uniq > "${IMAGEMANIFEST}" + rm "${IMAGEMANIFEST}.tmp" +} + +# archive_images +# archive the necessary docker images by pulling them locally and then saving them +archive_images() { + status "Pulling and saving container images" + + if [ ! -d "${IMAGEFILEDIR}" ]; then + mkdir -p "${IMAGEFILEDIR}" + fi + + # the images, pull first in case some are not local + while read line; do + echo " $line" + run_command "${DOCKER}" pull "${line}" + IMAGEFILE=$(echo "${line}" | sed 's|[/:]|-|g') + # if we already have the image exported, skip it + if [ ! -f "${IMAGEFILEDIR}/${IMAGEFILE}.tar" ]; then + run_command "${DOCKER}" save -o "${IMAGEFILEDIR}/${IMAGEFILE}.tar" "${line}" + fi + done < "${IMAGEMANIFEST}" + +} + +# restore_images +# load the images from an archive into the local registry +# then push them to the target registry +restore_images() { + status "Loading docker images" + find "${IMAGEFILEDIR}" -name \*.tar -exec "${DOCKER}" load -i {} \; 2>/dev/null + + status "Tagging and pushing images" + while read line; do + local NEWNAME="${REGISTRY}${line##*/}" + echo " $line -> ${NEWNAME}" + run_command "${DOCKER}" tag "${line}" "${NEWNAME}" + run_command "${DOCKER}" push "${NEWNAME}" + done < "${IMAGEMANIFEST}" +} + +# copy in any necessary files +copy_files() { + status "Copying necessary files" + for f in ${REQUIRED_FILES[@]}; do + echo " ${f}" + cp -R "${f}" "${DISTDIR}" + if [ $? -ne 0 ]; then + echo "Unable to copy ${f} to the distribution directory" + exit 1 + fi + done +} + +# fix any references in the helm charts or operator configuration +fixup_files() { + + local ROOTDIR="${HELMDIR}" + + if [ "${MODE}" == "operator" ]; then + ROOTDIR="${REPODIR}" + fi + + status "Preparing ${MODE} files within ${ROOTDIR}" + + # for each image in the manifest, replace the old name with the new + while read line; do + local NEWNAME="${REGISTRY}${line##*/}" + echo " changing: $line -> ${NEWNAME}" + find "${ROOTDIR}" -type f -not -path "${SCRIPTDIR}/*" -exec sed -i "s|$line|$NEWNAME|g" {} \; + done < "${IMAGEMANIFEST}" +} + +# compress the whole bundle +compress_bundle() { + status "Compressing release" + cd "${DISTBASE}" && tar cvfz "${DISTFILE}" "${DRIVERDIR}" + if [ $? -ne 0 ]; then + echo "Unable to package build" + exit 1 + fi + rm -rf "${DISTDIR}" +} + +# copy_helm_dir +# make a copy of the helm directory if one does not already exist +copy_helm_dir() { + if [ "${MODE}" != "helm" ]; then + return + fi + + status "Ensuring a copy of the helm directory exists" + if [ -d "${HELMBACKUPDIR}" ]; then + return + fi + + mkdir -p "${HELMBACKUPDIR}" + cp -R "${HELMDIR}"/* "${HELMBACKUPDIR}" +} + +# set_mode +# figure out if we are working from: +# - a driver repo and using helm +# - the operator repo which means we are using an operator +set_mode() { + # default is helm + MODE="helm" + + if [ ! -d "${HELMDIR}" ]; then + MODE="operator" + fi +} + +#------------------------------------------------------------------------------ +# +# Main script logic starts here +# + +# default values, overridable by users +CREATE="false" +PREPARE="false" +REGISTRY="" + +# some directories +SCRIPTDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +REPODIR="$( dirname "${SCRIPTDIR}" )" +HELMDIR="${REPODIR}/helm" +HELMBACKUPDIR="${REPODIR}/helm-original" + +# mode we are using for install, "helm" or "operator" +set_mode + +if [ "${MODE}" == "helm" ]; then + INSTALLERDIR="${REPODIR}/dell-csi-helm-installer" + CHARTFILE=$(find "${HELMDIR}" -maxdepth 2 -type f -name Chart.yaml) + + # some output files + DRIVERNAME=$(grep -oh "^name:\s.*" "${CHARTFILE}" | awk '{print $2}') + DRIVERNAME=${DRIVERNAME:-"dell-csi-driver"} + DRIVERVERSION=$(grep -oh "^version:\s.*" "${CHARTFILE}" | awk '{print $2}') + DRIVERVERSION=${DRIVERVERSION:-unknown} + DISTBASE="${REPODIR}" + DRIVERDIR="${DRIVERNAME}-bundle-${DRIVERVERSION}" + DISTDIR="${DISTBASE}/${DRIVERDIR}" + DISTFILE="${DISTBASE}/${DRIVERDIR}.tar.gz" + IMAGEMANIFEST="${INSTALLERDIR}/images.manifest" + IMAGEFILEDIR="${INSTALLERDIR}/images.tar" + + # directories to search all files for image names + DIRS_FOR_IMAGE_NAMES=( + "${HELMDIR}" + ) + # list of all files to be included + REQUIRED_FILES=( + "${HELMDIR}" + "${INSTALLERDIR}" + "${REPODIR}/*.pdf" + "${REPODIR}/*.md" + "${REPODIR}/LICENSE" + ) +else + DRIVERNAME="dell-csi-operator" + DISTBASE="${REPODIR}" + DRIVERDIR="${DRIVERNAME}-bundle" + DISTDIR="${DISTBASE}/${DRIVERDIR}" + DISTFILE="${DISTBASE}/${DRIVERDIR}.tar.gz" + IMAGEMANIFEST="${REPODIR}/scripts/images.manifest" + IMAGEFILEDIR="${REPODIR}/scripts/images.tar" + + + # directories to search all files for image names + DIRS_FOR_IMAGE_NAMES=( + "${REPODIR}/driverconfig" + "${REPODIR}/deploy" + "${REPODIR}/samples" + ) + + # list of all files to be included + REQUIRED_FILES=( + "${REPODIR}/driverconfig" + "${REPODIR}/deploy" + "${REPODIR}/samples" + "${REPODIR}/scripts" + "${REPODIR}/*.md" + "${REPODIR}/LICENSE" + ) +fi + +while getopts "cpr:h" opt; do + case $opt in + c) + CREATE="true" + ;; + p) + PREPARE="true" + ;; + r) + REGISTRY="${OPTARG}" + ;; + h) + usage + exit 0 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + exit 1 + ;; + esac +done + +# make sure exatly one option for create/prepare was specified +if [ "${CREATE}" == "${PREPARE}" ]; then + usage + exit 1 +fi + +# validate prepare arguments +if [ "${PREPARE}" == "true" ]; then + if [ "${REGISTRY}" == "" ]; then + usage + exit 1 + fi +fi + +if [ "${REGISTRY: -1}" != "/" ]; then + REGISTRY="${REGISTRY}/" +fi + +# figure out if we should use docker or podman, preferring docker +DOCKER=$(which docker 2>/dev/null || which podman 2>/dev/null) +if [ "${DOCKER}" == "" ]; then + echo "Unable to find either docker or podman in $PATH" + exit 1 +fi + +# create a bundle +if [ "${CREATE}" == "true" ]; then + if [ -d "${DISTDIR}" ]; then + rm -rf "${DISTDIR}" + fi + if [ ! -d "${DISTDIR}" ]; then + mkdir -p "${DISTDIR}" + fi + if [ -f "${DISTFILE}" ]; then + rm -f "${DISTFILE}" + fi + build_image_manifest + archive_images + copy_files + compress_bundle + + status "Complete" + echo "Offline bundle file is: ${DISTFILE}" +fi + +# prepare a bundle for installation +if [ "${PREPARE}" == "true" ]; then + echo "Preparing a offline bundle for installation" + restore_images + copy_helm_dir + fixup_files + + status "Complete" + + if [ "${MODE}" == "helm" ]; then + echo "Installation of the ${DRIVERNAME} driver can now be performed via" + echo "the scripts in ${INSTALLERDIR}" + fi +fi + +echo + +exit 0 diff --git a/dell-csi-helm-installer/csi-uninstall.sh b/dell-csi-helm-installer/csi-uninstall.sh index e3e8e5cf..e800318b 100755 --- a/dell-csi-helm-installer/csi-uninstall.sh +++ b/dell-csi-helm-installer/csi-uninstall.sh @@ -12,24 +12,31 @@ SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" DRIVERDIR="${SCRIPTDIR}/../helm" PROG="${0}" +# export the name of the debug log, so child processes will see it +export DEBUGLOG="${SCRIPTDIR}/uninstall-debug.log" + declare -a VALIDDRIVERS source "$SCRIPTDIR"/common.sh +if [ -f "${DEBUGLOG}" ]; then + rm -f "${DEBUGLOG}" +fi + # # usage will print command execution help and then exit function usage() { - echo "Help for $PROG" - echo - echo "Usage: $PROG options..." - echo "Options:" - echo " Required" - echo " --namespace[=] Kubernetes namespace to uninstall the CSI driver from" - - echo " Optional" - echo " --release[=] Name to register with helm, default value will match the driver name" - echo " -h Help" - echo + decho "Help for $PROG" + decho + decho "Usage: $PROG options..." + decho "Options:" + decho " Required" + decho " --namespace[=] Kubernetes namespace to uninstall the CSI driver from" + + decho " Optional" + decho " --release[=] Name to register with helm, default value will match the driver name" + decho " -h Help" + decho exit 0 } @@ -41,18 +48,19 @@ function usage() { function validate_params() { # make sure the driver was specified if [ -z "${DRIVER}" ]; then - echo "No driver specified" + decho "No driver specified" exit 1 fi # make sure the driver name is valid if [[ ! "${VALIDDRIVERS[@]}" =~ "${DRIVER}" ]]; then - echo "Driver: ${DRIVER} is invalid." - echo "Valid options are: ${VALIDDRIVERS[@]}" + decho "Driver: ${DRIVER} is invalid." + decho "Valid options are: ${VALIDDRIVERS[@]}" exit 1 fi # the namespace is required if [ -z "${NAMESPACE}" ]; then - echo "No namespace specified" + decho "No namespace specified" + usage exit 1 fi } @@ -60,15 +68,16 @@ function validate_params() { # check_for_driver will see if the driver is installed within the namespace provided function check_for_driver() { - NUM=$(helm list --namespace "${NAMESPACE}" | grep "^${RELEASE}\b" | wc -l) + NUM=$(run_command helm list --namespace "${NAMESPACE}" | grep "^${RELEASE}\b" | wc -l) if [ "${NUM}" == "0" ]; then - echo "The CSI Driver is not installed." + log error "The CSI Driver is not installed." exit 1 fi } # get the list of valid CSI Drivers, this will be the list of directories in drivers/ that contain helm charts get_drivers "${DRIVERDIR}" + # if only one driver was found, set the DRIVER to that one if [ ${#VALIDDRIVERS[@]} -eq 1 ]; then DRIVER="${VALIDDRIVERS[0]}" @@ -95,8 +104,8 @@ while getopts ":h-:" optchar; do RELEASE=${OPTARG#*=} ;; *) - echo "Unknown option --${OPTARG}" - echo "For help, run $PROG -h" + decho "Unknown option --${OPTARG}" + decho "For help, run $PROG -h" exit 1 ;; esac @@ -105,8 +114,8 @@ while getopts ":h-:" optchar; do usage ;; *) - echo "Unknown option -${OPTARG}" - echo "For help, run $PROG -h" + decho "Unknown option -${OPTARG}" + decho "For help, run $PROG -h" exit 1 ;; esac @@ -119,12 +128,12 @@ RELEASE=$(get_release_name "${DRIVER}") validate_params check_for_driver -helm delete -n "${NAMESPACE}" "${RELEASE}" +run_command helm delete -n "${NAMESPACE}" "${RELEASE}" if [ $? -ne 0 ]; then - echo "Removal of the CSI Driver was unsuccessful" + decho "Removal of the CSI Driver was unsuccessful" exit 1 fi -echo "Removal of the CSI Driver is in progress." -echo "It may take a few minutes for all pods to terminate." +decho "Removal of the CSI Driver is in progress." +decho "It may take a few minutes for all pods to terminate." diff --git a/dell-csi-helm-installer/verify.sh b/dell-csi-helm-installer/verify.sh index 4ec05f84..0ebb497c 100755 --- a/dell-csi-helm-installer/verify.sh +++ b/dell-csi-helm-installer/verify.sh @@ -12,11 +12,19 @@ SCRIPTDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" PROG="${0}" source "$SCRIPTDIR"/common.sh +if [ -z "${DEBUGLOG}" ]; then + export DEBUGLOG="${SCRIPTDIR}/install-debug.log" + if [ -f "${DEBUGLOG}" ]; then + rm -f "${DEBUGLOG}" + fi +fi + declare -a VALIDDRIVERS # verify-csi-powermax method function verify-csi-powermax() { - verify_k8s_versions "1" "17" "1" "19" + verify_k8s_versions "1.17" "1.20" + verify_openshift_versions "4.5" "4.6" verify_namespace "${NS}" verify_required_secrets "${RELEASE}-creds" verify_optional_secrets "${RELEASE}-certs" @@ -30,7 +38,8 @@ function verify-csi-powermax() { # # verify-csi-isilon method function verify-csi-isilon() { - verify_k8s_versions "1" "17" "1" "19" + verify_k8s_versions "1.17" "1.20" + verify_openshift_versions "4.5" "4.6" verify_namespace "${NS}" verify_required_secrets "${RELEASE}-creds" verify_optional_secrets "${RELEASE}-certs" @@ -42,7 +51,8 @@ function verify-csi-isilon() { # # verify-csi-vxflexos method function verify-csi-vxflexos() { - verify_k8s_versions "1" "17" "1" "19" + verify_k8s_versions "1.17" "1.20" + verify_openshift_versions "4.5" "4.6" verify_namespace "${NS}" verify_required_secrets "${RELEASE}-creds" verify_sdc_installation @@ -53,7 +63,8 @@ function verify-csi-vxflexos() { # verify-csi-powerstore method function verify-csi-powerstore() { - verify_k8s_versions "1" "17" "1" "19" + verify_k8s_versions "1.17" "1.20" + verify_openshift_versions "4.5" "4.6" verify_namespace "${NS}" verify_required_secrets "${RELEASE}-creds" verify_alpha_snap_resources @@ -64,20 +75,27 @@ function verify-csi-powerstore() { # verify-csi-unity method function verify-csi-unity() { - verify_k8s_versions "1" "17" "1" "19" + verify_k8s_versions "1.17" "1.20" + verify_openshift_versions "4.5" "4.6" verify_namespace "${NS}" verify_required_secrets "${RELEASE}-creds" verify_required_secrets "${RELEASE}-certs-0" verify_alpha_snap_resources - verify_beta_snap_requirements + verify_unity_protocol_installation + verify_beta_snap_requirements verify_helm_3 } +# if testing routines are found, source them for possible execution +if [ -f "${SCRIPTDIR}/test-functions.sh" ]; then + source "${SCRIPTDIR}/test-functions.sh" +fi + # # verify-driver will call the proper method to verify a specific driver function verify-driver() { if [ -z "${1}" ]; then - echo "Expected one argument, the driver name, to verify-driver. Received none." + decho "Expected one argument, the driver name, to verify-driver. Received none." exit $EXIT_ERROR fi local D="${1}" @@ -86,12 +104,12 @@ function verify-driver() { # if yes, check to see if it should be run and run it FNTYPE=$(type -t verify-$D) if [ "$FNTYPE" != "function" ]; then - echo "ERROR: verify-$D function does not exist" + decho "ERROR: verify-$D function does not exist" exit $EXIT_ERROR else header log step "Driver: ${D}" - echo + decho verify-$D summary fi @@ -99,22 +117,22 @@ function verify-driver() { # Print usage information function usage() { - echo - echo "Help for $PROG" - echo - echo "Usage: $PROG options..." - echo "Options:" - echo " Required" - echo " --namespace[=] Kubernetes namespace to install the CSI driver" - echo " --values[=] Values file, which defines configuration values" - - echo " Optional" - echo " --skip-verify-node Skip worker node verification checks" - echo " --release[=] Name to register with helm, default value will match the driver name" - echo " --node-verify-user[=] Username to SSH to worker nodes as, used to validate node requirements. Default is root" - echo " --snapshot-crd Signifies that the Snapshot CRDs will be installed as part of installation." - echo " -h Help" - echo + decho + decho "Help for $PROG" + decho + decho "Usage: $PROG options..." + decho "Options:" + decho " Required" + decho " --namespace[=] Kubernetes namespace to install the CSI driver" + decho " --values[=] Values file, which defines configuration values" + + decho " Optional" + decho " --skip-verify-node Skip worker node verification checks" + decho " --release[=] Name to register with helm, default value will match the driver name" + decho " --node-verify-user[=] Username to SSH to worker nodes as, used to validate node requirements. Default is root" + decho " --snapshot-crd Signifies that the Snapshot CRDs will be installed as part of installation." + decho " -h Help" + decho exit $EXIT_WARNING } @@ -132,11 +150,13 @@ function verify_sdc_installation() { fi log step "Verifying the SDC installation" + local SDC_MINION_NODES=$(run_command kubectl get nodes -o wide | grep -v -e master -e INTERNAL -e infra | awk ' { print $6; }') + error=0 missing=() - for node in $MINION_NODES; do + for node in $SDC_MINION_NODES; do # check is the scini kernel module is loaded - ssh ${NODEUSER}@$node "/sbin/lsmod | grep scini" >/dev/null 2>&1 + run_command ssh ${NODEUSER}@$node "/sbin/lsmod | grep scini" >/dev/null 2>&1 rv=$? if [ $rv -ne 0 ]; then missing+=($node) @@ -153,7 +173,7 @@ function verify_powerstore_node_configuration() { fi log step "Verifying PowerStore node configuration" - echo + decho if ls "${VALUES}" >/dev/null; then if grep -c "scsiProtocol:[[:blank:]]\+FC" "${VALUES}" >/dev/null; then @@ -191,23 +211,65 @@ function verify_iscsi_installation() { error=0 for node in $MINION_NODES; do # check if the iSCSI client is installed - ssh ${NODEUSER}@"${node}" "cat /etc/iscsi/initiatorname.iscsi" >/dev/null 2>&1 + run_command ssh ${NODEUSER}@"${node}" "cat /etc/iscsi/initiatorname.iscsi" >/dev/null 2>&1 rv=$? if [ $rv -ne 0 ]; then error=1 - found_warning "iSCSI client was not found on node: $node" + found_warning "Either iSCSI client was not found on node: $node or not able to verify" fi - ssh ${NODEUSER}@"${node}" pgrep iscsid &>/dev/null + run_command ssh ${NODEUSER}@"${node}" pgrep iscsid &>/dev/null rv=$? if [ $rv -ne 0 ]; then error=1 - found_warning "iscsid is not running on node: $node" + found_warning "Either iscsid service is not running on node: $node or not able to verify" fi done check_error error } + +function verify_unity_protocol_installation() { +if [ ${NODE_VERIFY} -eq 0 ]; then + return + fi + + log smart_step "Verifying sshpass installation.." + SSHPASS=$(which sshpass) + if [ -z "$SSHPASS" ]; then + found_warning "sshpass is not installed. It is mandatory to have ssh pass software for multi node kubernetes setup." + fi + + + log smart_step "Verifying iSCSI installation" "$1" + + error=0 + for node in $MINION_NODES; do + # check if the iSCSI client is installed + echo + echo -n "Enter the ${NODEUSER} password of ${node}: " + read -s nodepassword + echo + echo "$nodepassword" > protocheckfile + chmod 0400 protocheckfile + unset nodepassword + run_command sshpass -f protocheckfile ssh -o StrictHostKeyChecking=no ${NODEUSER}@"${node}" "cat /etc/iscsi/initiatorname.iscsi" > /dev/null 2>&1 + rv=$? + if [ $rv -ne 0 ]; then + error=1 + found_warning "iSCSI client is either not found on node: $node or not able to verify" + fi + run_command sshpass -f protocheckfile ssh -o StrictHostKeyChecking=no ${NODEUSER}@"${node}" "pgrep iscsid" > /dev/null 2>&1 + rv1=$? + if [ $rv1 -ne 0 ]; then + error=1 + found_warning "iscsid service is either not running on node: $node or not able to verify" + fi + rm -f protocheckfile + done + check_error error +} + # Check if the fc is installed function verify_fc_installation() { if [ ${NODE_VERIFY} -eq 0 ]; then @@ -219,7 +281,7 @@ function verify_fc_installation() { error=0 for node in $MINION_NODES; do # check if FC hosts are available - ssh ${NODEUSER}@${node} 'ls --hide=* /sys/class/fc_host/* 1>/dev/null' &>/dev/null + run_command ssh ${NODEUSER}@${node} 'ls --hide=* /sys/class/fc_host/* 1>/dev/null' &>/dev/null rv=$? if [[ ${rv} -ne 0 ]]; then error=1 @@ -237,7 +299,7 @@ function verify_required_secrets() { error=0 for N in "${@}"; do # Make sure the secret has already been established - kubectl get secrets -n "${NS}" 2>/dev/null | grep "${N}" --quiet + run_command kubectl get secrets -n "${NS}" 2>/dev/null | grep "${N}" --quiet if [ $? -ne 0 ]; then error=1 found_error "Required secret, ${N}, does not exist." @@ -252,7 +314,7 @@ function verify_optional_secrets() { error=0 for N in "${@}"; do # Make sure the secret has already been established - kubectl get secrets -n "${NS}" 2>/dev/null | grep "${N}" --quiet + run_command kubectl get secrets -n "${NS}" 2>/dev/null | grep "${N}" --quiet if [ $? -ne 0 ]; then error=1 found_warning "Optional secret, ${N}, does not exist." @@ -263,45 +325,66 @@ function verify_optional_secrets() { # verify minimum and maximum k8s versions function verify_k8s_versions() { + if [ "${OPENSHIFT}" == "true" ]; then + return + fi log step "Verifying Kubernetes versions" - echo - log arrow - verify_min_k8s_version "$1" "$2" "small" - log arrow - verify_max_k8s_version "$3" "$4" "small" -} - -# verify minimum k8s version -function verify_min_k8s_version() { - log smart_step "Verifying minimum Kubernetes version" "$3" + decho + local MIN=${1} + local MAX=${2} + local V="${kMajorVersion}.${kMinorVersion}" + # check minimum + log arrow + log smart_step "Verifying minimum Kubernetes version" "small" error=0 - if [[ "${1}" -gt "${kMajorVersion}" ]]; then + if [[ ${V} < ${MIN} ]]; then error=1 - found_error "Kubernetes version, ${kMajorVersion}.${kMinorVersion}, is too old. Minimum required version is: ${1}.${2}" + found_error "Kubernetes version, ${V}, is too old. Minimum required version is: ${MIN}" fi - if [[ "${2}" -gt "${kMinorVersion}" ]]; then + check_error error + + # check maximum + log arrow + log smart_step "Verifying maximum Kubernetes version" "small" + error=0 + if [[ ${V} > ${MAX} ]]; then error=1 - found_error "Kubernetes version, ${kMajorVersion}.${kMinorVersion}, is too old. Minimum required version is: ${1}.${2}" + found_warning "Kubernetes version, ${V}, is newer than has been tested. Latest tested version is: ${MAX}" fi - check_error error + } -# verify maximum k8s version -function verify_max_k8s_version() { - log smart_step "Verifying maximum Kubernetes version" "$3" +# verify minimum and maximum openshift versions +function verify_openshift_versions() { + if [ "${OPENSHIFT}" != "true" ]; then + return + fi + log step "Verifying OpenShift versions" + decho + local MIN=${1} + local MAX=${2} + local V=$(OpenShiftVersion) + # check minimum + log arrow + log smart_step "Verifying minimum OpenShift version" "small" error=0 - if [[ "${1}" -lt "${kMajorVersion}" ]]; then + if [[ ${V} < ${MIN} ]]; then error=1 - found_warning "Kubernetes version, ${kMajorVersion}.${kMinorVersion}, is newer than has been tested. Last tested version is: ${1}.${2}" + found_error "OpenShift version, ${V}, is too old. Minimum required version is: ${MIN}" fi - if [[ "${2}" -lt "${kMinorVersion}" ]]; then + check_error error + + # check maximum + log arrow + log smart_step "Verifying maximum OpenShift version" "small" + error=0 + if [[ ${V} > ${MAX} ]]; then error=1 - found_warning "Kubernetes version, ${kMajorVersion}.${kMinorVersion}, is newer than has been tested. Last tested version is: ${1}.${2}" + found_warning "OpenShift version, ${V}, is newer than has been tested. Latest tested version is: ${MAX}" fi - check_error error } @@ -312,7 +395,7 @@ function verify_namespace() { error=0 for N in "${@}"; do # Make sure the namespace exists - kubectl describe namespace "${N}" >/dev/null 2>&1 + run_command kubectl describe namespace "${N}" >/dev/null 2>&1 if [ $? -ne 0 ]; then error=1 found_error "Namespace does not exist: ${N}" @@ -325,7 +408,7 @@ function verify_namespace() { # verify that the no alpha version of volume snapshot resource is present on the system function verify_alpha_snap_resources() { log step "Verifying alpha snapshot resources" - echo + decho log arrow log smart_step "Verifying that alpha snapshot CRDs are not installed" "small" @@ -334,11 +417,11 @@ function verify_alpha_snap_resources() { CRDS=("VolumeSnapshotClasses" "VolumeSnapshotContents" "VolumeSnapshots") for C in "${CRDS[@]}"; do # Verify that alpha snapshot related CRDs/CRs are not there on the system. - kubectl explain ${C} 2> /dev/null | grep "^VERSION.*v1alpha1$" --quiet + run_command kubectl explain ${C} 2> /dev/null | grep "^VERSION.*v1alpha1$" --quiet if [ $? -eq 0 ]; then error=1 found_error "The alhpa CRD for ${C} is installed. Please uninstall it" - if [[ $(kubectl get ${C} -A --no-headers 2>/dev/null | wc -l) -ne 0 ]]; then + if [[ $(run_command kubectl get ${C} -A --no-headers 2>/dev/null | wc -l) -ne 0 ]]; then found_error " Found CR for alpha CRD ${C}. Please delete it" fi fi @@ -349,7 +432,7 @@ function verify_alpha_snap_resources() { # verify that the requirements for beta snapshot support exist function verify_beta_snap_requirements() { log step "Verifying beta snapshot support" - echo + decho log arrow log smart_step "Verifying that beta snapshot CRDs are available" "small" @@ -358,7 +441,7 @@ function verify_beta_snap_requirements() { CRDS=("VolumeSnapshotClasses" "VolumeSnapshotContents" "VolumeSnapshots") for C in "${CRDS[@]}"; do # Verify if snapshot related CRDs are there on the system. If not install them. - kubectl explain ${C} 2> /dev/null | grep "^VERSION.*v1beta1$" --quiet + run_command kubectl explain ${C} 2> /dev/null | grep "^VERSION.*v1beta1$" --quiet if [ $? -ne 0 ]; then error=1 if [ "${INSTALL_CRD}" == "yes" ]; then @@ -375,7 +458,7 @@ function verify_beta_snap_requirements() { error=0 # check for the snapshot-controller. These are strongly suggested but not required - kubectl get pods -A | grep snapshot-controller --quiet + run_command kubectl get pods -A | grep snapshot-controller --quiet if [ $? -ne 0 ]; then error=1 found_warning "The Snapshot Controller does not seem to be deployed. The Snapshot Controller should be provided by the Kubernetes vendor or administrator." @@ -396,7 +479,7 @@ function verify_helm_3() { return } - helm version | grep "v3." --quiet + run_command helm version | grep "v3." --quiet if [ $? -ne 0 ]; then error=1 found_error "Driver installation is supported only using helm 3" @@ -421,15 +504,22 @@ function found_warning() { # Print a nice summary at the end function summary() { - echo - log section "Verification Complete" + local VERSTATUS="Success" + if [ "${#WARNINGS[@]}" -ne 0 ]; then + VERSTATUS="With Warnings" + fi + if [ "${#ERRORS[@]}" -ne 0 ]; then + VERSTATUS="With Errors" + fi + decho + log section "Verification Complete - ${VERSTATUS}" # print all the WARNINGS NON_CRD_WARNINGS=0 if [ "${#WARNINGS[@]}" -ne 0 ]; then log warnings for E in "${WARNINGS[@]}"; do - echo "- ${E}" - echo ${E} | grep --quiet "^The beta CRD for VolumeSnapshot" + decho "- ${E}" + decho ${E} | grep --quiet "^The beta CRD for VolumeSnapshot" if [ $? -ne 0 ]; then NON_CRD_WARNINGS=1 fi @@ -444,7 +534,7 @@ function summary() { if [ "${#ERRORS[@]}" -ne 0 ]; then log errors for E in "${ERRORS[@]}"; do - echo "- ${E}" + decho "- ${E}" done RC=$EXIT_ERROR fi @@ -457,31 +547,31 @@ function summary() { function validate_params() { # make sure the driver was specified if [ -z "${DRIVER}" ]; then - echo "No driver specified" + decho "No driver specified" usage exit 1 fi # make sure the driver name is valid if [[ ! "${VALIDDRIVERS[@]}" =~ "${DRIVER}" ]]; then - echo "Driver: ${DRIVER} is invalid." - echo "Valid options are: ${VALIDDRIVERS[@]}" + decho "Driver: ${DRIVER} is invalid." + decho "Valid options are: ${VALIDDRIVERS[@]}" usage exit 1 fi # the namespace is required if [ -z "${NS}" ]; then - echo "No namespace specified" + decho "No namespace specified" usage exit 1 fi # values file if [ -z "${VALUES}" ]; then - echo "No values file was specified" + decho "No values file was specified" usage exit 1 fi if [ ! -f "${VALUES}" ]; then - echo "Unable to read values file at: ${VALUES}" + decho "Unable to read values file at: ${VALUES}" usage exit 1 fi @@ -507,16 +597,16 @@ INSTALL_CRD="no" # make sure kubectl is available kubectl --help >&/dev/null || { - echo "kubectl required for verification... exiting" + decho "kubectl required for verification... exiting" exit $EXIT_ERROR } # Determine the nodes -MINION_NODES=$(kubectl get nodes -o wide | grep -v -e master -e INTERNAL | awk ' { print $6; }') -MASTER_NODES=$(kubectl get nodes -o wide | awk ' /master/{ print $6; }') +MINION_NODES=$(run_command kubectl get nodes -o wide | grep -v -e master -e INTERNAL | awk ' { print $6; }') +MASTER_NODES=$(run_command kubectl get nodes -o wide | awk ' /master/{ print $6; }') # Get the kubernetes major and minor version numbers. -kMajorVersion=$(kubectl version | grep 'Server Version' | sed -e 's/^.*Major:"//' -e 's/[^0-9].*//g') -kMinorVersion=$(kubectl version | grep 'Server Version' | sed -e 's/^.*Minor:"//' -e 's/[^0-9].*//g') +kMajorVersion=$(run_command kubectl version | grep 'Server Version' | sed -e 's/^.*Major:"//' -e 's/[^0-9].*//g') +kMinorVersion=$(run_command kubectl version | grep 'Server Version' | sed -e 's/^.*Minor:"//' -e 's/[^0-9].*//g') # get the list of valid CSI Drivers, this will be the list of directories in drivers/ that contain helm charts get_drivers "${SCRIPTDIR}/../helm" @@ -571,11 +661,11 @@ while getopts ":h-:" optchar; do OPTIND=$((OPTIND + 1)) ;; node-verify-user=*) - HODEUSER=${OPTARG#*=} + NODEUSER=${OPTARG#*=} ;; *) - echo "Unknown option --${OPTARG}" - echo "For help, run $PROG -h" + decho "Unknown option --${OPTARG}" + decho "For help, run $PROG -h" exit $EXIT_ERROR ;; esac @@ -584,8 +674,8 @@ while getopts ":h-:" optchar; do usage ;; *) - echo "Unknown option -${OPTARG}" - echo "For help, run $PROG -h" + decho "Unknown option -${OPTARG}" + decho "For help, run $PROG -h" exit $EXIT_ERROR ;; esac @@ -600,6 +690,7 @@ NODEUSER="${NODEUSER:-root}" # validate the parameters passed in validate_params "${MODE}" +OPENSHIFT=$(isOpenShift) verify-driver "${DRIVER}" exit $? diff --git a/env.sh b/env.sh index fd021388..e4d2bc1a 100644 --- a/env.sh +++ b/env.sh @@ -1,8 +1,5 @@ #!/bin/sh -#unisphere: https://10.247.73.217:8443 -unisphere: https://10.247.73.133:8443 - # This should be like https://111.222.333.444 export X_CSI_VXFLEXOS_ENDPOINT="" export X_CSI_VXFLEXOS_USER="" diff --git a/go.mod b/go.mod index 01c80f13..5706b376 100644 --- a/go.mod +++ b/go.mod @@ -15,14 +15,22 @@ require ( github.com/container-storage-interface/spec v1.1.0 github.com/dell/gofsutil v1.4.0 github.com/dell/goscaleio v1.2.0 - github.com/gogo/protobuf v1.2.0 // indirect - github.com/golang/protobuf v1.3.1 + github.com/golang/protobuf v1.4.2 + github.com/google/gofuzz v1.2.0 // indirect + github.com/googleapis/gnostic v0.4.0 // indirect github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/mux v1.6.2 + github.com/kr/pretty v0.2.0 // indirect + github.com/kubernetes-csi/csi-lib-utils v0.7.0 github.com/rexray/gocsi v1.1.0 github.com/sirupsen/logrus v1.4.2 - github.com/stretchr/testify v1.3.0 - golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect - golang.org/x/net v0.0.0-20200226121028-0de0cce0169b - google.golang.org/grpc v1.19.0 + github.com/stretchr/testify v1.5.1 + golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect + golang.org/x/net v0.0.0-20200822124328-c89045814202 + golang.org/x/tools v0.0.0-20201001191422-af0a1b5f3ca7 // indirect + google.golang.org/grpc v1.27.0 + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + k8s.io/client-go v0.18.6 + k8s.io/utils v0.0.0-20200912215256-4140de9c8800 // indirect + ) diff --git a/helm/csi-vxflexos/Chart.yaml b/helm/csi-vxflexos/Chart.yaml index ba427939..f38ed209 100644 --- a/helm/csi-vxflexos/Chart.yaml +++ b/helm/csi-vxflexos/Chart.yaml @@ -1,6 +1,7 @@ +--- name: csi-vxflexos -version: 1.2.1 -appVersion: 1.2.1 +version: 1.3.0 +appVersion: 1.3.0 apiVersion: v2 description: | VxFlex OS CSI (Container Storage Interface) driver Kubernetes diff --git a/helm/csi-vxflexos/driver-image.yaml b/helm/csi-vxflexos/driver-image.yaml index 29869c6c..9e1dac76 100644 --- a/helm/csi-vxflexos/driver-image.yaml +++ b/helm/csi-vxflexos/driver-image.yaml @@ -1,4 +1,9 @@ -# IT IS RECOMMENDED YOU DO NOT CHANGE THE IMAGES TO BE DOWNLOADED. +--- +# IT IS RECOMMENDED YOU DO NOT CHANGE THE IMAGES TO BE USED. images: - # "images.driver" defines the container images used for the driver container. - driver: dellemc/csi-vxflexos:v1.2.1 + # "driver" defines the container image, used for the driver container. + driver: dellemc/csi-vxflexos:v1.3.0.000R + + + # "powerflexSdc" defines the SDC image, used for RedHat CoreOS only. + powerflexSdc: dellemc/sdc:3.5.1.1 diff --git a/helm/csi-vxflexos/k8s-1.17-values.yaml b/helm/csi-vxflexos/k8s-1.17-values.yaml index 4a43ca75..e2eb34c6 100644 --- a/helm/csi-vxflexos/k8s-1.17-values.yaml +++ b/helm/csi-vxflexos/k8s-1.17-values.yaml @@ -1,22 +1,23 @@ -# IT IS RECOMMENDED YOU DO NOT CHANGE THE IMAGES TO BE DOWNLOADED. +--- +# IT IS RECOMMENDED YOU DO NOT CHANGE THE IMAGES TO BE USED. kubeversion: "v1.17" images: # "images.attacher" defines the container images used for the csi attacher # container. - attacher: quay.io/k8scsi/csi-attacher:v2.2.0 + attacher: k8s.gcr.io/sig-storage/csi-attacher:v3.0.0 # "images.provisioner" defines the container images used for the csi provisioner # container. - provisioner: quay.io/k8scsi/csi-provisioner:v1.5.0 + provisioner: k8s.gcr.io/sig-storage/csi-provisioner:v2.0.2 - # "images.snapshotter" defines the container image used for the csi snapshotter - snapshotter: quay.io/k8scsi/csi-snapshotter:v2.1.1 + # "images.snapshotter" defines the container image used for the csi external snapshotter + snapshotter: k8s.gcr.io/sig-storage/csi-snapshotter:v3.0.2 # "images.registrar" defines the container images used for the csi registrar # container. - registrar: quay.io/k8scsi/csi-node-driver-registrar:v1.2.0 + registrar: k8s.gcr.io/sig-storage/csi-node-driver-registrar:v2.0.1 # "images.resizer" defines the container images used for the csi resizer - #container. - resizer: quay.io/k8scsi/csi-resizer:v0.5.0 + # container. + resizer: quay.io/k8scsi/csi-resizer:v1.0.0 diff --git a/helm/csi-vxflexos/k8s-1.18-values.yaml b/helm/csi-vxflexos/k8s-1.18-values.yaml index 77e8cf68..0e706b54 100644 --- a/helm/csi-vxflexos/k8s-1.18-values.yaml +++ b/helm/csi-vxflexos/k8s-1.18-values.yaml @@ -1,23 +1,23 @@ -# IT IS RECOMMENDED YOU DO NOT CHANGE THE IMAGES TO BE DOWNLOADED. +--- +# IT IS RECOMMENDED YOU DO NOT CHANGE THE IMAGES TO BE USED. kubeversion: "v1.18" images: # "images.attacher" defines the container images used for the csi attacher # container. - attacher: quay.io/k8scsi/csi-attacher:v2.2.0 + attacher: k8s.gcr.io/sig-storage/csi-attacher:v3.0.0 # "images.provisioner" defines the container images used for the csi provisioner # container. - provisioner: quay.io/k8scsi/csi-provisioner:v1.6.0 + provisioner: k8s.gcr.io/sig-storage/csi-provisioner:v2.0.2 - # "images.snapshotter" defines the container image used for the csi snapshotter - snapshotter: quay.io/k8scsi/csi-snapshotter:v2.1.1 + # "images.snapshotter" defines the container image used for the csi external snapshotter + snapshotter: k8s.gcr.io/sig-storage/csi-snapshotter:v3.0.2 # "images.registrar" defines the container images used for the csi registrar # container. - registrar: quay.io/k8scsi/csi-node-driver-registrar:v1.2.0 + registrar: k8s.gcr.io/sig-storage/csi-node-driver-registrar:v2.0.1 # "images.resizer" defines the container images used for the csi resizer - #container. - resizer: quay.io/k8scsi/csi-resizer:v0.5.0 - + # container. + resizer: quay.io/k8scsi/csi-resizer:v1.0.0 diff --git a/helm/csi-vxflexos/k8s-1.19-values.yaml b/helm/csi-vxflexos/k8s-1.19-values.yaml index 25a445a9..6289af68 100644 --- a/helm/csi-vxflexos/k8s-1.19-values.yaml +++ b/helm/csi-vxflexos/k8s-1.19-values.yaml @@ -1,23 +1,23 @@ +--- # IT IS RECOMMENDED YOU DO NOT CHANGE THE IMAGES TO BE DOWNLOADED. kubeversion: "v1.19" images: # "images.attacher" defines the container images used for the csi attacher # container. - attacher: quay.io/k8scsi/csi-attacher:v2.2.0 + attacher: k8s.gcr.io/sig-storage/csi-attacher:v3.0.0 # "images.provisioner" defines the container images used for the csi provisioner # container. - provisioner: quay.io/k8scsi/csi-provisioner:v1.6.0 + provisioner: k8s.gcr.io/sig-storage/csi-provisioner:v2.0.2 - # "images.snapshotter" defines the container image used for the csi snapshotter - snapshotter: quay.io/k8scsi/csi-snapshotter:v2.1.1 + # "images.snapshotter" defines the container image used for the csi external snapshotter + snapshotter: k8s.gcr.io/sig-storage/csi-snapshotter:v3.0.2 # "images.registrar" defines the container images used for the csi registrar # container. - registrar: quay.io/k8scsi/csi-node-driver-registrar:v1.2.0 + registrar: k8s.gcr.io/sig-storage/csi-node-driver-registrar:v2.0.1 # "images.resizer" defines the container images used for the csi resizer # container. - resizer: quay.io/k8scsi/csi-resizer:v0.5.0 - + resizer: quay.io/k8scsi/csi-resizer:v1.0.0 diff --git a/helm/csi-vxflexos/k8s-1.20-values.yaml b/helm/csi-vxflexos/k8s-1.20-values.yaml new file mode 100644 index 00000000..fefed677 --- /dev/null +++ b/helm/csi-vxflexos/k8s-1.20-values.yaml @@ -0,0 +1,23 @@ +--- +# IT IS RECOMMENDED YOU DO NOT CHANGE THE IMAGES TO BE DOWNLOADED. +kubeversion: "v1.20" + +images: + # "images.attacher" defines the container images used for the csi attacher + # container. + attacher: k8s.gcr.io/sig-storage/csi-attacher:v3.0.0 + + # "images.provisioner" defines the container images used for the csi provisioner + # container. + provisioner: k8s.gcr.io/sig-storage/csi-provisioner:v2.0.2 + + # "images.snapshotter" defines the container image used for the csi external snapshotter + snapshotter: k8s.gcr.io/sig-storage/csi-snapshotter:v3.0.2 + + # "images.registrar" defines the container images used for the csi registrar + # container. + registrar: k8s.gcr.io/sig-storage/csi-node-driver-registrar:v2.0.1 + + # "images.resizer" defines the container images used for the csi resizer + # container. + resizer: quay.io/k8scsi/csi-resizer:v1.0.0 diff --git a/helm/csi-vxflexos/templates/controller.yaml b/helm/csi-vxflexos/templates/controller.yaml index 4aec8cfc..17d1b803 100644 --- a/helm/csi-vxflexos/templates/controller.yaml +++ b/helm/csi-vxflexos/templates/controller.yaml @@ -9,6 +9,9 @@ apiVersion: rbac.authorization.k8s.io/v1 metadata: name: {{ .Release.Name }}-controller rules: + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get", "watch", "list", "delete", "update", "create"] - apiGroups: [""] resources: ["events"] verbs: ["list", "watch", "create", "update", "patch"] @@ -29,13 +32,27 @@ rules: verbs: ["get", "list", "watch"] - apiGroups: ["storage.k8s.io"] resources: ["volumeattachments"] +{{ if .Values.podmon.enabled }} + verbs: ["get", "list", "watch", "update", "patch", "delete"] +{{ else }} verbs: ["get", "list", "watch", "update", "patch"] +{{ end }} - apiGroups: ["storage.k8s.io"] resources: ["csinodes"] verbs: ["get", "list", "watch", "update"] + - apiGroups: ["storage.k8s.io"] + resources: ["volumeattachments/status"] + verbs: ["patch"] - apiGroups: ["csi.storage.k8s.io"] resources: ["csinodeinfos"] verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["pods"] +{{ if .Values.podmon.enabled }} + verbs: ["get", "list", "watch", "update", "delete"] +{{ else }} + verbs: ["get", "list", "watch"] +{{ end }} # below for snapshotter - apiGroups: [""] resources: ["secrets"] @@ -69,7 +86,7 @@ roleRef: name: {{ .Release.Name }}-controller apiGroup: rbac.authorization.k8s.io --- -kind: StatefulSet +kind: Deployment apiVersion: apps/v1 metadata: name: {{ .Release.Name }}-controller @@ -77,21 +94,52 @@ metadata: spec: selector: matchLabels: - app: {{ .Release.Name }}-controller - serviceName: {{ .Release.Name }}-controller + name: {{ .Release.Name }}-controller replicas: {{ required "Must provide the number of controller instances to create." .Values.controllerCount }} template: metadata: labels: - app: {{ .Release.Name }}-controller + name: {{ .Release.Name }}-controller spec: - serviceAccount: {{ .Release.Name }}-controller + affinity: + nodeSelector: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: name + operator: In + values: + - {{ .Release.Name }}-controller + topologyKey: kubernetes.io/hostname + serviceAccountName: {{ .Release.Name }}-controller + {{ if .Values.controller.nodeSelector }} + nodeSelector: + {{- toYaml .Values.controller.nodeSelector | nindent 8 }} + {{ end }} + {{ if .Values.controller.tolerations }} + tolerations: + {{- toYaml .Values.controller.tolerations | nindent 6 }} + {{ end }} containers: +{{ if .Values.podmon.enabled }} + - name: podmon + imagePullPolicy: Always + image: {{ required "Must provide the podmon container image." .Values.podmon.image }} + args: + {{- toYaml .Values.podmon.controller.args | nindent 12 }} + volumeMounts: + - name: socket-dir + mountPath: /var/run/csi + - name: usr-bin + mountPath: /usr-bin +{{ end }} - name: attacher image: {{ required "Must provide the CSI attacher container image." .Values.images.attacher }} args: - "--csi-address=$(ADDRESS)" - "--v=5" + - "--leader-election=true" env: - name: ADDRESS value: /var/run/csi/csi.sock @@ -102,15 +150,13 @@ spec: image: {{ required "Must provide the CSI provisioner container image." .Values.images.provisioner }} args: - "--csi-address=$(ADDRESS)" + - "--feature-gates=Topology=true" - "--volume-name-prefix={{ required "Must provide a value to prefix to driver created volume names" .Values.volumeNamePrefix }}" - "--volume-name-uuid-length=10" - {{- if eq .Values.kubeversion "v1.13" }} - - "--connection-timeout=300s" - - "--provisioner=csi-vxflexos.dellemc.com" - {{- else }} + - "--leader-election=true" - "--timeout=120s" - {{- end}} - "--v=5" + - "--default-fstype={{ .Values.defaultFsType | default "ext4" }}" env: - name: ADDRESS value: /var/run/csi/csi.sock @@ -118,12 +164,12 @@ spec: - name: socket-dir mountPath: /var/run/csi - name: snapshotter - #image: quay.io/k8scsi/csi-snapshotter:v1.0.0 image: {{ required "Must provide the CSI snapshotter container image." .Values.images.snapshotter }} args: - "--csi-address=$(ADDRESS)" - "--timeout=120s" - "--v=5" + - "--leader-election=true" env: - name: ADDRESS value: /var/run/csi/csi.sock @@ -136,6 +182,7 @@ spec: args: - "--csi-address=$(ADDRESS)" - "--v=5" + - "--leader-election=true" env: - name: ADDRESS value: /var/run/csi/csi.sock @@ -146,6 +193,8 @@ spec: image: {{ required "Must provide the VxFlex OS driver container image." .Values.images.driver }} imagePullPolicy: Always command: [ "/csi-vxflexos.sh" ] + args: + - "--leader-election" env: - name: CSI_ENDPOINT value: /var/run/csi/csi.sock @@ -179,3 +228,9 @@ spec: volumes: - name: socket-dir emptyDir: +{{ if .Values.podmon.enabled }} + - name: usr-bin + hostPath: + path: /usr/bin + type: Directory +{{ end }} diff --git a/helm/csi-vxflexos/templates/csidriver.yaml b/helm/csi-vxflexos/templates/csidriver.yaml index 735740ba..d8169c25 100644 --- a/helm/csi-vxflexos/templates/csidriver.yaml +++ b/helm/csi-vxflexos/templates/csidriver.yaml @@ -1,13 +1,6 @@ -{{ if eq .Values.kubeversion "v1.13" }} -apiVersion: csi.storage.k8s.io/v1alpha1 -{{ else }} apiVersion: storage.k8s.io/v1beta1 -{{ end }} kind: CSIDriver metadata: name: vxflexos spec: attachRequired: true - {{ if eq .Values.kubeversion "v1.13" }} - podInfoOnMountVersion: "v1" - {{ end }} diff --git a/helm/csi-vxflexos/templates/node.yaml b/helm/csi-vxflexos/templates/node.yaml index ada4b405..9929e188 100644 --- a/helm/csi-vxflexos/templates/node.yaml +++ b/helm/csi-vxflexos/templates/node.yaml @@ -30,7 +30,18 @@ rules: - apiGroups: ["storage.k8s.io"] resources: ["volumeattachments"] verbs: ["get", "list", "watch", "update"] - + - apiGroups: ["security.openshift.io"] + resourceNames: ["privileged"] + resources: ["securitycontextconstraints"] + verbs: ["use"] +{{ if .Values.podmon.enabled }} + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "update", "delete"] + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get", "watch", "list", "delete", "update", "create"] +{{ end }} --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 @@ -61,13 +72,45 @@ spec: spec: serviceAccount: {{ .Release.Name }}-node hostNetwork: true + {{ if and .Values.monitor.enabled .Values.monitor.hostPID }} + hostPID: true + {{ else }} + hostPID: false + {{ end }} containers: - - name: driver +{{ if .Values.podmon.enabled }} + - name: podmon securityContext: privileged: true capabilities: add: ["SYS_ADMIN"] allowPrivilegeEscalation: true + imagePullPolicy: Always + image: {{ required "Must provide the podmon container image." .Values.podmon.image }} + args: + {{- toYaml .Values.podmon.node.args | nindent 12 }} + env: + - name: KUBE_NODE_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: spec.nodeName + - name: X_CSI_PRIVATE_MOUNT_DIR + value: "/var/lib/kubelet/plugins/vxflexos.emc.dell.com/disks" + volumeMounts: + - name: kubelet-pods + mountPath: /var/lib/kubelet/pods + - name: driver-path + mountPath: /var/lib/kubelet/plugins/vxflexos.emc.dell.com + - name: usr-bin + mountPath: /usr-bin +{{ end }} + - name: driver + securityContext: + privileged: true + allowPrivilegeEscalation: true + capabilities: + add: ["SYS_ADMIN"] image: {{ required "Must provide the VxFlex OS driver container image." .Values.images.driver }} imagePullPolicy: Always command: [ "/csi-vxflexos.sh" ] @@ -107,16 +150,11 @@ spec: mountPath: /dev - name: scaleio-path-opt mountPath: /opt/emc/scaleio/sdc/bin - - name: scaleio-path-bin - mountPath: /bin/emc - name: registrar image: {{ required "Must provide the CSI node registrar container image." .Values.images.registrar }} args: - "--v=5" - "--csi-address=$(ADDRESS)" - #- --mode=node-register - #- --driver-requires-attachment=true - #- --pod-info-mount-version=v1 - --kubelet-registration-path=/var/lib/kubelet/plugins/vxflexos.emc.dell.com/csi_sock env: - name: ADDRESS @@ -131,6 +169,98 @@ spec: mountPath: /registration - name: driver-path mountPath: /csi + {{ if eq .Values.monitor.enabled true }} + - name: sdc-monitor + securityContext: + privileged: true + image: {{ required "Must provide the PowerFlex SDC container image." .Values.images.powerflexSdc }} + imagePullPolicy: Always + env: + {{ if eq .Values.monitor.hostPID true }} + - name: HOST_PID + value: "1" + {{ else }} + - name: HOST_PID + value: "0" + {{ end }} + - name: HOST_NET + value: "1" + - name: NODENAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: MDM + value: {{ join "," .Values.mdmIP }} + - name: MODE + value: "monitoring" + - name: REPO_ADDRESS + value: {{ .Values.sdcKernelMirror.repoUrl }} + - name: REPO_USER + valueFrom: + secretKeyRef: + name: sdc-repo-creds + key: username + - name: REPO_PASSWORD + valueFrom: + secretKeyRef: + name: sdc-repo-creds + key: password + - name: HOST_DRV_CFG_PATH + value: /opt/emc/scaleio/sdc/bin + volumeMounts: + - name: dev + mountPath: /dev + - name: os-release + mountPath: /host-os-release + - name: sdc-storage + mountPath: /storage + - name: udev-d + mountPath: /rules.d + - name: scaleio-path-opt + mountPath: /host_drv_cfg_path + {{ end }} + initContainers: + - name: sdc + securityContext: + privileged: true + image: {{ required "Must provide the PowerFlex SDC container image." .Values.images.powerflexSdc }} + imagePullPolicy: Always + env: + - name: NODENAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: MODE + value: "config" + - name: MDM + value: {{ join "," .Values.mdmIP }} + - name: REPO_ADDRESS + value: {{ .Values.sdcKernelMirror.repoUrl }} + - name: REPO_USER + valueFrom: + secretKeyRef: + name: sdc-repo-creds + key: username + - name: REPO_PASSWORD + valueFrom: + secretKeyRef: + name: sdc-repo-creds + key: password + - name: HOST_DRV_CFG_PATH + value: /opt/emc/scaleio/sdc/bin + volumeMounts: + - name: dev + mountPath: /dev + - name: os-release + mountPath: /host-os-release + - name: sdc-storage + mountPath: /storage + - name: udev-d + mountPath: /rules.d + - name: scaleio-path-opt + mountPath: /host_drv_cfg_path + - name: sdc-config + mountPath: /config volumes: - name: registration-dir hostPath: @@ -155,8 +285,30 @@ spec: - name: scaleio-path-opt hostPath: path: /opt/emc/scaleio/sdc/bin + type: DirectoryOrCreate + - name: sdc-storage + hostPath: + path: /var/emc-scaleio + type: DirectoryOrCreate + - name: udev-d + hostPath: + path: /etc/udev/rules.d + type: Directory + - name: os-release + hostPath: + path: /etc/os-release + type: File + - name: sdc-config + hostPath: + path: /var/sio-config + type: DirectoryOrCreate +{{ if .Values.podmon.enabled }} + - name: usr-bin + hostPath: + path: /usr/bin type: Directory - - name: scaleio-path-bin + - name: kubelet-pods hostPath: - path: {{ required "Location of drv_cfg binary" .Values.vxflexosbinpath }} + path: /var/lib/kubelet/pods type: Directory +{{ end }} diff --git a/helm/csi-vxflexos/templates/storageclass-xfs.yaml b/helm/csi-vxflexos/templates/storageclass-xfs.yaml index 6a34f380..1c45c425 100644 --- a/helm/csi-vxflexos/templates/storageclass-xfs.yaml +++ b/helm/csi-vxflexos/templates/storageclass-xfs.yaml @@ -3,6 +3,7 @@ kind: StorageClass metadata: name: {{ required "Must provide a storage class name." .Values.storageClass.name}}-xfs annotations: + "helm.sh/resource-policy": keep provisioner: csi-vxflexos.dellemc.com reclaimPolicy: {{ required "Must provide a storage class reclaim policy." .Values.storageClass.reclaimPolicy }} allowVolumeExpansion: true diff --git a/helm/csi-vxflexos/templates/storageclass.yaml b/helm/csi-vxflexos/templates/storageclass.yaml index 81b696e1..a9b926c2 100644 --- a/helm/csi-vxflexos/templates/storageclass.yaml +++ b/helm/csi-vxflexos/templates/storageclass.yaml @@ -3,6 +3,7 @@ kind: StorageClass metadata: name: {{ required "Must provide a storage class name." .Values.storageClass.name}} annotations: + "helm.sh/resource-policy": keep storageclass.beta.kubernetes.io/is-default-class: {{ .Values.storageClass.isDefault | quote }} provisioner: csi-vxflexos.dellemc.com reclaimPolicy: {{ required "Must provide a storage class reclaim policy." .Values.storageClass.reclaimPolicy }} diff --git a/helm/csi-vxflexos/values.yaml b/helm/csi-vxflexos/values.yaml index 3fc575a4..303e6736 100644 --- a/helm/csi-vxflexos/values.yaml +++ b/helm/csi-vxflexos/values.yaml @@ -1,47 +1,115 @@ +--- # "systemName" defines the name of the VxFlex OS system from which volumes will -# be provisioned. This must either be set to the VxFlex OS system name or system ID. -# systemName: systemname +# be provisioned. This must be set to the VxFlex OS system name or system ID. +systemName: systemname -# "restGateway" defines the VxFlex OS REST API endpoint, with full URL, typically leveraging HTTPS. +# "defaultFsType" is used to set the default FS type which will be used +# for mount volumes if FsType is not specified in the storage class +defaultFsType: ext4 + +# "restGateway" defines the VxFlex OS REST API endpoint, with full URL. +# Typically this leverages HTTPS. # You must set this for your VxFlex OS installations REST gateway. -# restGateway: https://123.0.0.1 +restGateway: https://123.0.0.1 -# "storagePool" defines the VxFlex OS storage pool from which this driver will # provision volumes. +# "storagePool" defines the VxFlex OS storage pool from which this driver will +# provision volumes. # You must set this for the primary storage pool to be used -# storagePool: sp +storagePool: sp -#"mdmIP" defines the MDM(s) the SDC's should register with on start, comma separated +# "mdmIP" defines the MDM(s) the SDC's should register with on start +# This should be an list of MDM IP addresses or hostnames # You must set this to the MDM IPs for your VxFlex OS system. -mdmIP: 192.168.1.1 +mdmIP: + - 0.0.0.0 + - 0.0.0.0 -# "volumeNamePrefix" defines a string prepended to each volume created by the CSI driver. +# "volumeNamePrefix" defines a string prepended to each volume created. volumeNamePrefix: k8s # "controllerCount" defines the number of VxFlex controller nodes to deploy to # the Kubernetes release -controllerCount: 1 - -# Where is the drv_cfg binary on the host? -# By default, /bin/emc, but might be /var/vcap/packages/vxflexos_kernel on PKS -# IT IS RECOMMENDED YOU DO NOT CHANGE THIS. -vxflexosbinpath: /bin/emc +controllerCount: 2 -# Enable this to automatically delete all snapshots in a consistency group when a snap in the group is deleted +# Enable this to automatically delete all snapshots in a consistency group +# when a snap in the group is deleted enablesnapshotcgdelete: "false" -# Enable list volume operation to include snapshots (since creating a volume from a snap actually results in a new snap) -# It is recommend this be false unless Kubernetes needs this for some reason. +# Enable list volume operation to include snapshots (since creating a volume +# from a snap actually results in a new snap) +# It is recommend this be false unless instructed otherwise. enablelistvolumesnapshot: "false" -# The installation process will generate multiple storageclasses based on these parameters. -# Only the primary storageclass for the driver will be marked default if specified. +# Storage Class details +# storageclasses will be created based on these parameters. +# Only the primary storageclass will be marked default if specified. storageClass: - # "storageClass.name" defines the name of the storage class to be defined. + # "name" defines the name of the storage class to be defined. name: vxflexos - # "storageClass.isDefault" defines whether the primary storage class should be the # default. + # "isDefault" defines whether the storage class should be the default. isDefault: "true" - # "storageClass.reclaimPolicy" defines what will happen when a volume is + # "reclaimPolicy" defines what will happen when a volume is # removed from the Kubernetes API. Valid values are "Retain" and "Delete". reclaimPolicy: Delete + +# "controller" allows to configure controller specific parameters +controller: + + #"controller.nodeSelector" defines what nodes would be selected for pods of controller deployment + # Leave as blank to use all nodes + nodeSelector: + # node-role.kubernetes.io/master: "" + + # "controller.tolerations" defines tolerations that would be applied to controller deployment + # Leave as blank to install controller on worker nodes + tolerations: + # - key: "node-role.kubernetes.io/master" + # operator: "Exists" + # effect: "NoSchedule" +# monitoring pod details +# These options control the running of the monitoring container +# This container gather diagnostic information in case of failure +monitor: + # enabled allows the usage of te monitoring pod to be disabled + enabled: false + + # hostNetwork determines if the monitor pod should run on the host network or not + hostNetwork: true + + # hostPID determines if the monitor pod should run in the host namespace + hostPID: true + +# The below values apply to RedHat CoreOS only +# +# The VxFlex OS SDC may need to pull a new module that is known to work +# with newer Linux kernels. The default location of this mirror os at +# ftp.emc.com. The VxFlex OS documentation has instructions for methods +# to mirror this repository to a local location if necssary +sdcKernelMirror: + # URL of the ftp mirror containing sdc kernel modules. + # Only ftp locations are allowed. + # A blank string signifies the default mirror, which is "ftp://ftp.emc.com". + repoUrl: "" + +# use sdc-repo-secret.yaml to setup +# Userid for the kernel module mirror site +# Password for the kernel module mirror site + +# Podmon is an optional feature under development and tech preview. +# Enable this feature only after contact support for additional information +podmon: + enabled: false + image: + #controller: + # args: + # - "-csisock=unix:/var/run/csi/csi.sock" + # - "-labelvalue=csi-vxflexos" + # - "-mode=controller" + #node: + # args: + # - "-csisock=unix:/var/lib/kubelet/plugins/vxflexos.emc.dell.com/csi_sock" + # - "-labelvalue=csi-vxflexos" + # - "-mode=node" + # - "-leaderelection=false" diff --git a/helm/sdc-repo-secret.yaml b/helm/sdc-repo-secret.yaml new file mode 100644 index 00000000..9ba7f94b --- /dev/null +++ b/helm/sdc-repo-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: sdc-repo-creds + namespace: vxflexos +type: Opaque +data: + # set username to the base64 encoded username, sdc default is + username: UU56Z2R4WGl4 + # set password to the base64 encoded password, sdc default is + password: QXczd0ZBd0FxMw== diff --git a/k8sutils/k8sutils.go b/k8sutils/k8sutils.go new file mode 100644 index 00000000..43257fca --- /dev/null +++ b/k8sutils/k8sutils.go @@ -0,0 +1,41 @@ +package k8sutils + +import ( + "context" + "fmt" + "github.com/kubernetes-csi/csi-lib-utils/leaderelection" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "os" +) + +type leaderElection interface { + Run() error + WithNamespace(namespace string) +} + +// CreateKubeClientSet - Returns kubeclient set +func CreateKubeClientSet(kubeconfig string) (*kubernetes.Clientset, error) { + var clientset *kubernetes.Clientset + config, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + // creates the clientset + clientset, err = kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + + return clientset, nil +} + +// LeaderElection +func LeaderElection(clientset *kubernetes.Clientset, lockName string, namespace string, runFunc func(ctx context.Context)) { + le := leaderelection.NewLeaderElection(clientset, lockName, runFunc) + le.WithNamespace(namespace) + if err := le.Run(); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to initialize leader election: %v", err) + os.Exit(1) + } +} diff --git a/main.go b/main.go index f0e78f85..44e7a097 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,12 @@ package main import ( "context" + "flag" + "fmt" + "os" + "strings" + + "github.com/dell/csi-vxflexos/k8sutils" "github.com/dell/csi-vxflexos/provider" "github.com/dell/csi-vxflexos/service" "github.com/rexray/gocsi" @@ -11,12 +17,29 @@ import ( // main is ignored when this package is built as a go plug-in func main() { - gocsi.Run( - context.Background(), - service.Name, - "A VxFlex OS Container Storage Interface (CSI) Plugin", - usage, - provider.New()) + + enableLeaderElection := flag.Bool("leader-election", false, "boolean to enable leader election") + leaderElectionNamespace := flag.String("leader-election-namespace", "", "namespace where leader election lease will be created") + kubeconfig := flag.String("kubeconfig", "", "absolute path to the kubeconfig file") + flag.Parse() + run := func(ctx context.Context) { + gocsi.Run(ctx, service.Name, "A PowerFlex Container Storage Interface (CSI) Plugin", + usage, provider.New()) + } + if !*enableLeaderElection { + run(context.Background()) + } else { + driverName := strings.Replace(service.Name, ".", "-", -1) + lockName := fmt.Sprintf("driver-%s", driverName) + k8sclientset, err := k8sutils.CreateKubeClientSet(*kubeconfig) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to initialize leader election: %v", err) + os.Exit(1) + } + // Attempt to become leader and start the driver + k8sutils.LeaderElection(k8sclientset, lockName, *leaderElectionNamespace, run) + } + } const usage = ` X_CSI_VXFLEXOS_ENDPOINT diff --git a/overrides.mk b/overrides.mk index 1c3ed78f..1432b17a 100644 --- a/overrides.mk +++ b/overrides.mk @@ -8,6 +8,7 @@ DEFAULT_GOVERSION="1.13.12" DEFAULT_REGISTRY="sample_registry" DEFAULT_IMAGENAME="csi-vxflexos" DEFAULT_BUILDSTAGE="final" +DEFAULT_IMAGETAG="" # set the BASEIMAGE if needed ifeq ($(BASEIMAGE),) @@ -29,6 +30,11 @@ ifeq ($(IMAGENAME),) export IMAGENAME="$(DEFAULT_IMAGENAME)" endif +#set the IMAGETAG if needed +ifneq ($(DEFAULT_IMAGETAG), "") +export IMAGETAG="$(DEFAULT_IMAGETAG)" +endif + # set the BUILDSTAGE if needed ifeq ($(BUILDSTAGE),) export BUILDSTAGE="$(DEFAULT_BUILDSTAGE)" diff --git a/patch-notes.md b/patch-notes.md deleted file mode 100644 index a2b6b6a0..00000000 --- a/patch-notes.md +++ /dev/null @@ -1,24 +0,0 @@ -# CSI Driver for PowerFlex Patches - -## Patch 1.2.1 -##### Image: -dellemc/csi-vxflexos:v1.2.1 -##### Updates: -This patch updates the base image to UBI 8, fixing CVEs that were reported with UBI 7 base image. -##### Instructions to use: -###### Helm: -New driver image is already supplied in helm/csi-vxflexos/driver-image.yaml, run: -` ./csi-install.sh --namespace vxflexos --values --upgrade` -to upgrade your driver to the patched image. - -###### Operator: -Replace image dellemc/csi-vxflexos:v1.2.0.000R with dellemc/csi-vxflexos:v1.2.1 in the following yamls: - -Upstream Kubernetes: -samples/vxflex_120_k8s_117.yaml -samples/vxflex_120_k8s_118.yaml -samples/vxflex_120_k8s_119.yaml - -Openshift Environments: -samples/vxflex_120_ops_43.yaml -samples/vxflex_120_ops_44.yaml diff --git a/service/controller.go b/service/controller.go index 3578001e..a3f53057 100644 --- a/service/controller.go +++ b/service/controller.go @@ -83,6 +83,9 @@ func (s *service) CreateVolume( // validate AccessibleTopology accessibility := req.GetAccessibilityRequirements() + if accessibility == nil { + log.Printf("Received CreateVolume request without accessibility keys") + } requestedSystem := "" if accessibility != nil && len(accessibility.GetPreferred()) > 0 { @@ -96,7 +99,7 @@ func (s *service) CreateVolume( constraint = tokens[1] } log.Printf("Found topology constraint: VxFlex OS system: %s", constraint) - if constraint == s.system.System.ID { + if constraint == s.system.System.ID || constraint == s.system.System.Name { requestedSystem = s.system.System.ID } } @@ -134,15 +137,14 @@ func (s *service) CreateVolume( req.Name = name } - // Volume content source support Snapshots only contentSource := req.GetVolumeContentSource() - var snapshotSource *csi.VolumeContentSource_SnapshotSource if contentSource != nil { volumeSource := contentSource.GetVolume() if volumeSource != nil { - return nil, status.Error(codes.InvalidArgument, "Volume as a VolumeContentSource is not supported (i.e. clone)") + log.Printf("volume %s specified as volume content source", volumeSource.VolumeId) + return s.Clone(req, volumeSource, name, sizeInKiB, sp) } - snapshotSource = contentSource.GetSnapshot() + snapshotSource := contentSource.GetSnapshot() if snapshotSource != nil { log.Printf("snapshot %s specified as volume content source", snapshotSource.SnapshotId) return s.createVolumeFromSnapshot(req, snapshotSource, name, sizeInKiB, sp) @@ -1129,6 +1131,13 @@ func (s *service) ControllerGetCapabilities( }, }, }, + { + Type: &csi.ControllerServiceCapability_Rpc{ + Rpc: &csi.ControllerServiceCapability_RPC{ + Type: csi.ControllerServiceCapability_RPC_CLONE_VOLUME, + }, + }, + }, }, }, nil } @@ -1538,3 +1547,77 @@ func mergeStringMaps(base map[string]string, additional map[string]string) map[s return result } + +func (s *service) Clone(req *csi.CreateVolumeRequest, + volumeSource *csi.VolumeContentSource_VolumeSource, name string, sizeInKbytes int64, storagePool string) (*csi.CreateVolumeResponse, error) { + + // Look up the source volume + srcVol, err := s.getVolByID(volumeSource.VolumeId) + if err != nil { + return nil, status.Errorf(codes.NotFound, "Volume not found: %s", volumeSource.VolumeId) + } + + // Validate the size is the same + if int64(srcVol.SizeInKb) != sizeInKbytes { + return nil, status.Errorf(codes.InvalidArgument, + "Volume %s has incompatible size %d kbytes with requested %d kbytes", + volumeSource.VolumeId, srcVol.SizeInKb, sizeInKbytes) + } + + // Validate the storage pool is the same + volStoragePool := s.getStoragePoolNameFromID(srcVol.StoragePoolID) + if volStoragePool != storagePool { + return nil, status.Errorf(codes.InvalidArgument, + "Volume storage pool %s is different from the requested storage pool %s", volStoragePool, storagePool) + } + + // Check for idempotent request + existingVols, err := s.adminClient.GetVolume("", "", "", name, false) + for _, vol := range existingVols { + if vol.Name == name && vol.StoragePoolID == srcVol.StoragePoolID { + log.Printf("Requested volume %s already exists", name) + csiVolume := s.getCSIVolume(vol) + csiVolume.ContentSource = req.GetVolumeContentSource() + copyInterestingParameters(req.GetParameters(), csiVolume.VolumeContext) + log.Printf("Requested volume (from clone) already exists %s (%s) storage pool %s", + csiVolume.VolumeContext["Name"], csiVolume.VolumeId, csiVolume.VolumeContext["StoragePoolName"]) + return &csi.CreateVolumeResponse{Volume: csiVolume}, nil + + } + } + + // Snapshot the source volumes + snapshotDefs := make([]*siotypes.SnapshotDef, 0) + snapDef := &siotypes.SnapshotDef{VolumeID: volumeSource.VolumeId, SnapshotName: name} + snapshotDefs = append(snapshotDefs, snapDef) + snapParam := &siotypes.SnapshotVolumesParam{SnapshotDefs: snapshotDefs} + + // Create snapshot + snapResponse, err := s.system.CreateSnapshotConsistencyGroup(snapParam) + if err != nil { + return nil, status.Errorf(codes.Internal, "Failed to call CreateSnapshotConsistencyGroup to clone volume: %s", err.Error()) + } + + if len(snapResponse.VolumeIDList) != 1 { + return nil, status.Errorf(codes.Internal, "Expected volume ID to be returned but it was not") + } + + // Restrieve created destination volume + destID := snapResponse.VolumeIDList[0] + destVol, err := s.getVolByID(destID) + if err != nil { + return nil, status.Errorf(codes.Internal, "Could not retrieve created volume: %s", destID) + } + + // Create a volume response and return it + s.clearCache() + csiVolume := s.getCSIVolume(destVol) + csiVolume.ContentSource = req.GetVolumeContentSource() + copyInterestingParameters(req.GetParameters(), csiVolume.VolumeContext) + + log.Printf("Volume (from volume clone) %s (%s) storage pool %s", + csiVolume.VolumeContext["Name"], csiVolume.VolumeId, csiVolume.VolumeContext["storagePoolName"]) + + return &csi.CreateVolumeResponse{Volume: csiVolume}, nil + +} diff --git a/service/features/controller_publish_unpublish.feature b/service/features/controller_publish_unpublish.feature index 3fd4fab6..47ba7ef9 100644 --- a/service/features/controller_publish_unpublish.feature +++ b/service/features/controller_publish_unpublish.feature @@ -1,263 +1,263 @@ Feature: VxFlex OS CSI interface - As a consumer of the CSI interface - I want to test controller publish / unpublish interfaces - So that they are known to work + As a consumer of the CSI interface + I want to test controller publish / unpublish interfaces + So that they are known to work - Scenario: Publish volume with single writer - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "single-writer" - Then a valid PublishVolumeResponse is returned - And the number of SDC mappings is 1 + Scenario: Publish volume with single writer + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "single-writer" + Then a valid PublishVolumeResponse is returned + And the number of SDC mappings is 1 - Scenario Outline: Publish Volume with Wrong Access Types - Given a VxFlexOS service - And a valid volume - And I use AccessType Mount - When I call Probe - And I call PublishVolume with - Then the error contains + Scenario Outline: Publish Volume with Wrong Access Types + Given a VxFlexOS service + And a valid volume + And I use AccessType Mount + When I call Probe + And I call PublishVolume with + Then the error contains - Examples: + Examples: | access | msg | | "multiple-writer" | "Mount multinode multi-writer not allowed" | | "multi-single-writer" | "Multinode single writer not supported" | - - Scenario: Idempotent publish volume with single writer - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "single-writer" - And I call PublishVolume with "single-writer" - Then a valid PublishVolumeResponse is returned - And the number of SDC mappings is 1 - Scenario: Publish block volume with multiple writers to single writer volume - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "single-writer" - And then I use a different nodeID - And I call PublishVolume with "single-writer" - Then the error contains "volume already published" + Scenario: Idempotent publish volume with single writer + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "single-writer" + And I call PublishVolume with "single-writer" + Then a valid PublishVolumeResponse is returned + And the number of SDC mappings is 1 - Scenario: Publish block volume with multiple writers to multiple writer volume - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "multiple-writer" - And then I use a different nodeID - And I call PublishVolume with "multiple-writer" - Then a valid PublishVolumeResponse is returned - And the number of SDC mappings is 2 + Scenario: Publish block volume with multiple writers to single writer volume + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "single-writer" + And then I use a different nodeID + And I call PublishVolume with "single-writer" + Then the error contains "volume already published" - Scenario: Publish block volume with multiple writers to multiple reader volume - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "multiple-reader" - And then I use a different nodeID - And I call PublishVolume with "multiple-reader" - Then the error contains "not compatible with access type" + Scenario: Publish block volume with multiple writers to multiple writer volume + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "multiple-writer" + And then I use a different nodeID + And I call PublishVolume with "multiple-writer" + Then a valid PublishVolumeResponse is returned + And the number of SDC mappings is 2 - Scenario: Publish mount volume with multiple writers to single writer volume - Given a VxFlexOS service - And a valid volume - And I use AccessType Mount - When I call Probe - And I call PublishVolume with "single-writer" - And then I use a different nodeID - And I call PublishVolume with "single-writer" - Then the error contains "volume already published" + Scenario: Publish block volume with multiple writers to multiple reader volume + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "multiple-reader" + And then I use a different nodeID + And I call PublishVolume with "multiple-reader" + Then the error contains "not compatible with access type" - Scenario: Publish mount volume with multiple readers to multiple reader volume - Given a VxFlexOS service - And a valid volume - And I use AccessType Mount - When I call Probe - And I call PublishVolume with "multiple-reader" - And then I use a different nodeID - And I call PublishVolume with "multiple-reader" - Then a valid PublishVolumeResponse is returned + Scenario: Publish mount volume with multiple writers to single writer volume + Given a VxFlexOS service + And a valid volume + And I use AccessType Mount + When I call Probe + And I call PublishVolume with "single-writer" + And then I use a different nodeID + And I call PublishVolume with "single-writer" + Then the error contains "volume already published" - Scenario: Publish mount volume with multiple readers to multiple reader volume - Given a VxFlexOS service - And a valid volume - And I use AccessType Mount - When I call Probe - And I call PublishVolume with "multiple-reader" - And then I use a different nodeID - And I call PublishVolume with "multiple-reader" - Then a valid PublishVolumeResponse is returned - And the number of SDC mappings is 2 + Scenario: Publish mount volume with multiple readers to multiple reader volume + Given a VxFlexOS service + And a valid volume + And I use AccessType Mount + When I call Probe + And I call PublishVolume with "multiple-reader" + And then I use a different nodeID + And I call PublishVolume with "multiple-reader" + Then a valid PublishVolumeResponse is returned - Scenario: Publish volume with an invalid volumeID - Given a VxFlexOS service - When I call Probe - And I call PublishVolume with "single-writer" - Then the error contains "volume not found" + Scenario: Publish mount volume with multiple readers to multiple reader volume + Given a VxFlexOS service + And a valid volume + And I use AccessType Mount + When I call Probe + And I call PublishVolume with "multiple-reader" + And then I use a different nodeID + And I call PublishVolume with "multiple-reader" + Then a valid PublishVolumeResponse is returned + And the number of SDC mappings is 2 - Scenario: Publish volume no volumeID specified - Given a VxFlexOS service - And no volume - When I call Probe - And I call PublishVolume with "single-writer" - Then the error contains "volume ID is required" + Scenario: Publish volume with an invalid volumeID + Given a VxFlexOS service + When I call Probe + And I call PublishVolume with "single-writer" + Then the error contains "volume not found" - Scenario: Publish volume with no nodeID specified - Given a VxFlexOS service - And a valid volume - And no node - When I call Probe - And I call PublishVolume with "single-writer" - Then the error contains "node ID is required" + Scenario: Publish volume no volumeID specified + Given a VxFlexOS service + And no volume + When I call Probe + And I call PublishVolume with "single-writer" + Then the error contains "volume ID is required" - Scenario: Publish volume with no volume capability - Given a VxFlexOS service - And a valid volume - And no volume capability - When I call Probe - And I call PublishVolume with "single-writer" - Then the error contains "volume capability is required" + Scenario: Publish volume with no nodeID specified + Given a VxFlexOS service + And a valid volume + And no node + When I call Probe + And I call PublishVolume with "single-writer" + Then the error contains "node ID is required" - Scenario: Publish volume with no access mode - Given a VxFlexOS service - And a valid volume - And no access mode - When I call Probe - And I call PublishVolume with "single-writer" - Then the error contains "access mode is required" + Scenario: Publish volume with no volume capability + Given a VxFlexOS service + And a valid volume + And no volume capability + When I call Probe + And I call PublishVolume with "single-writer" + Then the error contains "volume capability is required" - Scenario: Publish volume with no previous probe - Given a VxFlexOS service - And a valid volume - When I invalidate the Probe cache - And I call PublishVolume with "single-writer" - Then the error contains "Controller Service has not been probed" + Scenario: Publish volume with no access mode + Given a VxFlexOS service + And a valid volume + And no access mode + When I call Probe + And I call PublishVolume with "single-writer" + Then the error contains "access mode is required" - Scenario: Publish volume with getSDCID error - Given a VxFlexOS service - And a valid volume - And I induce error "GetSdcInstancesError" - When I call Probe - And I call PublishVolume with "single-writer" - Then the error contains "error finding SDC from GUID" + Scenario: Publish volume with no previous probe + Given a VxFlexOS service + And a valid volume + When I invalidate the Probe cache + And I call PublishVolume with "single-writer" + Then the error contains "Controller Service has not been probed" - Scenario: Publish volume with bad vol ID - Given a VxFlexOS service - And a valid volume - And I induce error "BadVolIDError" - When I call Probe - And I call PublishVolume with "single-writer" - Then the error contains "volume not found" + Scenario: Publish volume with getSDCID error + Given a VxFlexOS service + And a valid volume + And I induce error "GetSdcInstancesError" + When I call Probe + And I call PublishVolume with "single-writer" + Then the error contains "error finding SDC from GUID" + Scenario: Publish volume with bad vol ID + Given a VxFlexOS service + And a valid volume + And I induce error "BadVolIDError" + When I call Probe + And I call PublishVolume with "single-writer" + Then the error contains "volume not found" - Scenario: Publish volume with a map SDC error - Given a VxFlexOS service - And a valid volume - And I induce error "MapSdcError" - When I call Probe - And I call PublishVolume with "single-writer" - Then the error contains "error mapping volume to node" - Scenario: Publish volume with AccessMode UNKNOWN - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "unknown" - Then the error contains "access mode cannot be UNKNOWN" + Scenario: Publish volume with a map SDC error + Given a VxFlexOS service + And a valid volume + And I induce error "MapSdcError" + When I call Probe + And I call PublishVolume with "single-writer" + Then the error contains "error mapping volume to node" - Scenario: Unpublish volume - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "single-writer" - And no error was received - And the number of SDC mappings is 1 - And I call UnpublishVolume - And no error was received - Then a valid UnpublishVolumeResponse is returned - And the number of SDC mappings is 0 + Scenario: Publish volume with AccessMode UNKNOWN + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "unknown" + Then the error contains "access mode cannot be UNKNOWN" - Scenario: Idempotent unpublish volume - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "single-writer" - And no error was received - And I call UnpublishVolume - And no error was received - And I call UnpublishVolume - And no error was received - Then a valid UnpublishVolumeResponse is returned + Scenario: Unpublish volume + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "single-writer" + And no error was received + And the number of SDC mappings is 1 + And I call UnpublishVolume + And no error was received + Then a valid UnpublishVolumeResponse is returned + And the number of SDC mappings is 0 - Scenario: Unpublish volume with no volume id - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "single-writer" - And no error was received - And no volume - And I call UnpublishVolume - Then the error contains "Volume ID is required" + Scenario: Idempotent unpublish volume + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "single-writer" + And no error was received + And I call UnpublishVolume + And no error was received + And I call UnpublishVolume + And no error was received + Then a valid UnpublishVolumeResponse is returned - Scenario: Unpublish volume with invalid probe cache - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "single-writer" - And no error was received - And the number of SDC mappings is 1 - And I invalidate the Probe cache - And I call UnpublishVolume - Then the error contains "Controller Service has not been probed" - - Scenario: Unpublish volume with invalid volume id - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "single-writer" - And no error was received - And an invalid volume - And I call UnpublishVolume - Then the error contains "volume not found" + Scenario: Unpublish volume with no volume id + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "single-writer" + And no error was received + And no volume + And I call UnpublishVolume + Then the error contains "Volume ID is required" - Scenario: Unpublish volume with no node id - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "single-writer" - And no error was received - And no node - And I call UnpublishVolume - Then the error contains "Node ID is required" + Scenario: Unpublish volume with invalid probe cache + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "single-writer" + And no error was received + And the number of SDC mappings is 1 + And I invalidate the Probe cache + And I call UnpublishVolume + Then the error contains "Controller Service has not been probed" - Scenario: Unpublish volume with RemoveMappedSdcError - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "single-writer" - And no error was received - And I induce error "RemoveMappedSdcError" - And I call UnpublishVolume - Then the error contains "Error unmapping volume from node" + Scenario: Unpublish volume with invalid volume id + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "single-writer" + And no error was received + And an invalid volume + And I call UnpublishVolume + Then the error contains "volume not found" - Scenario: Publish / unpublish mount volume with multiple writers to multiple writer volume - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call PublishVolume with "multiple-writer" - And a valid PublishVolumeResponse is returned - And the number of SDC mappings is 1 - And then I use a different nodeID - And I call PublishVolume with "multiple-writer" - And a valid PublishVolumeResponse is returned - And the number of SDC mappings is 2 - And I call UnpublishVolume - And no error was received - And the number of SDC mappings is 1 - And then I use a different nodeID - And I call UnpublishVolume - And no error was received - Then the number of SDC mappings is 0 + Scenario: Unpublish volume with no node id + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "single-writer" + And no error was received + And no node + And I call UnpublishVolume + Then the error contains "Node ID is required" + + Scenario: Unpublish volume with RemoveMappedSdcError + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "single-writer" + And no error was received + And I induce error "RemoveMappedSdcError" + And I call UnpublishVolume + Then the error contains "Error unmapping volume from node" + + Scenario: Publish / unpublish mount volume with multiple writers to multiple writer volume + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call PublishVolume with "multiple-writer" + And a valid PublishVolumeResponse is returned + And the number of SDC mappings is 1 + And then I use a different nodeID + And I call PublishVolume with "multiple-writer" + And a valid PublishVolumeResponse is returned + And the number of SDC mappings is 2 + And I call UnpublishVolume + And no error was received + And the number of SDC mappings is 1 + And then I use a different nodeID + And I call UnpublishVolume + And no error was received + Then the number of SDC mappings is 0 diff --git a/service/features/delete_volume.feature b/service/features/delete_volume.feature index 4bcfceab..d984c57c 100644 --- a/service/features/delete_volume.feature +++ b/service/features/delete_volume.feature @@ -1,72 +1,72 @@ Feature: VxFlex OS CSI interface - As a consumer of the CSI interface - I want to test delete service methods - So that they are known to work + As a consumer of the CSI interface + I want to test delete service methods + So that they are known to work - Scenario: Delete volume with valid CapacityRange capabilities BlockVolume, SINGLE_NODE_WRITER and null VolumeContentSource. - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call DeleteVolume with "single-writer" - Then a valid DeleteVolumeResponse is returned + Scenario: Delete volume with valid CapacityRange capabilities BlockVolume, SINGLE_NODE_WRITER and null VolumeContentSource. + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call DeleteVolume with "single-writer" + Then a valid DeleteVolumeResponse is returned - Scenario: Delete volume with valid CapacityRange capabilities BlockVolume, MULTI_NODE_READER_ONLY null VolumeContentSource. - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call DeleteVolume with "multiple-reader" - Then a valid DeleteVolumeResponse is returned + Scenario: Delete volume with valid CapacityRange capabilities BlockVolume, MULTI_NODE_READER_ONLY null VolumeContentSource. + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call DeleteVolume with "multiple-reader" + Then a valid DeleteVolumeResponse is returned - Scenario: Delete volume with valid CapacityRange capabilities BlockVolume, MULTI_NODE_WRITE null VolumeContentSource. - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call DeleteVolume with "multiple-writer" - Then a valid DeleteVolumeResponse is returned + Scenario: Delete volume with valid CapacityRange capabilities BlockVolume, MULTI_NODE_WRITE null VolumeContentSource. + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call DeleteVolume with "multiple-writer" + Then a valid DeleteVolumeResponse is returned - Scenario: Test idempotent deletion volume valid CapacityRange capabilities BlockVolume, SINGLE_NODE_WRITER and null VolumeContentSource (2nd attempt to delete same volume should be nop.) - Given a VxFlexOS service - And a valid volume - When I call Probe - And I call DeleteVolume with "single-writer" - And I call DeleteVolume with "single-writer" - Then a valid DeleteVolumeResponse is returned + Scenario: Test idempotent deletion volume valid CapacityRange capabilities BlockVolume, SINGLE_NODE_WRITER and null VolumeContentSource (2nd attempt to delete same volume should be nop.) + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call DeleteVolume with "single-writer" + And I call DeleteVolume with "single-writer" + Then a valid DeleteVolumeResponse is returned - Scenario: Delete volume with induced getVolByID error - Given a VxFlexOS service - And a valid volume - And I induce error "GetVolByIDError" - When I call Probe - And I call DeleteVolume with "single-writer" - Then the error contains "induced error" + Scenario: Delete volume with induced getVolByID error + Given a VxFlexOS service + And a valid volume + And I induce error "GetVolByIDError" + When I call Probe + And I call DeleteVolume with "single-writer" + Then the error contains "induced error" - Scenario: Delete a volume with induced SIOGatewayVolumeNotFound error - Given a VxFlexOS service - And a valid volume - And I induce error "SIOGatewayVolumeNotFound" - When I call Probe - And I call DeleteVolume with "single-writer" - Then a valid DeleteVolumeResponse is returned + Scenario: Delete a volume with induced SIOGatewayVolumeNotFound error + Given a VxFlexOS service + And a valid volume + And I induce error "SIOGatewayVolumeNotFound" + When I call Probe + And I call DeleteVolume with "single-writer" + Then a valid DeleteVolumeResponse is returned - Scenario: Delete volume with an invalid volume - Given a VxFlexOS service - And an invalid volume - When I call Probe - And I call DeleteVolume with "single-writer" - Then the error contains "volume not found" - - Scenario: Delete volume with an invalid volume id - Given a VxFlexOS service - And a valid volume - And I induce error "BadVolIDError" - When I call Probe - And I call DeleteVolume with "single-writer" - Then a valid DeleteVolumeResponse is returned - - Scenario: Delete Volume with invalid probe cache - Given a VxFlexOS service - And a valid volume - When I invalidate the Probe cache - And I call DeleteVolume with "single-writer" - Then the error contains "Controller Service has not been probed" + Scenario: Delete volume with an invalid volume + Given a VxFlexOS service + And an invalid volume + When I call Probe + And I call DeleteVolume with "single-writer" + Then the error contains "volume not found" + + Scenario: Delete volume with an invalid volume id + Given a VxFlexOS service + And a valid volume + And I induce error "BadVolIDError" + When I call Probe + And I call DeleteVolume with "single-writer" + Then a valid DeleteVolumeResponse is returned + + Scenario: Delete Volume with invalid probe cache + Given a VxFlexOS service + And a valid volume + When I invalidate the Probe cache + And I call DeleteVolume with "single-writer" + Then the error contains "Controller Service has not been probed" diff --git a/service/features/get_system_instances.json b/service/features/get_system_instances.json index 7035d3a6..f3e2791a 100644 --- a/service/features/get_system_instances.json +++ b/service/features/get_system_instances.json @@ -102,7 +102,7 @@ "id": "0873f5bd5c3dd9a1", "port": 9011 }, - "slaves": [ + "worker": [ { "versionInfo": "R2_6.0.0", "managementIPs": [ diff --git a/service/features/list_volumes.feature b/service/features/list_volumes.feature index 8592be1c..d979dcfc 100644 --- a/service/features/list_volumes.feature +++ b/service/features/list_volumes.feature @@ -1,169 +1,169 @@ Feature: VxFlex OS CSI interface - As a consumer of the CSI interface - I want to test list service methods - So that they are known to work - - Scenario: Test list volumes allowing an unlimited number of volumes - Given a VxFlex OS service - And there are 5 valid volumes - When I call Probe - And I call ListVolumes with - | max_entries | starting_token | - | 0 | none | - Then a valid ListVolumesResponse is returned - And 5 volumes are listed - - Scenario: Test list volumes, limiting the number of volumes to be less than the number present using max_entries. - Given a VxFlex OS service - And there are 5 valid volumes - When I call Probe - And I call ListVolumes with - | max_entries | starting_token | - | 1 | none | - Then a valid ListVolumesResponse is returned - And 1 volume is listed - - Scenario: Test list volumes starting at a different offset (using next_token) - Given a VxFlex OS service - And there are 5 valid volumes - When I call Probe - And I call ListVolumes with - | max_entries | starting_token | - | 2 | none | - And I call ListVolumes again with - | max_entries | starting_token | - | 3 | next | - Then a valid ListVolumesResponse is returned - And 3 volumes are listed - - Scenario: Test list volumes with an invalid starting token - Given a VxFlex OS service - And a valid volume - When I call Probe - And I call ListVolumes with - | max_entries | starting_token | - | 1 | invalid | - Then an invalid ListVolumesResponse is returned - - Scenario: Test list volumes with induced volume instances error - Given a VxFlex OS service - And a valid volume - And I induce error "VolumeInstancesError" - When I call Probe - And I call ListVolumes with - | max_entries | starting_token | - | 1 | none | - Then the error contains "Unable to list volumes" - - Scenario: Test list volumes with no probe - Given a VxFlex OS service - And a valid volume - And I invalidate the Probe cache - And I call ListVolumes with - | max_entries | starting_token | - | 1 | none | - Then the error contains "has not been probed" - - Scenario: Test list volumes with an starting token greater than volume count - Given a VxFlex OS service - And a valid volume - When I call Probe - And I call ListVolumes with - | max_entries | starting_token | - | 1 | larger | - Then an invalid ListVolumesResponse is returned - - Scenario: List snapshots - Given a VxFlexOS service - And there are 5 valid snapshots of "default" volume - When I call Probe - And I call ListSnapshots with max_entries "5" and starting_token "" - Then a valid ListSnapshotsResponse is returned with listed "5" and next_token "" - Scenario: List snapshots without calling probe - Given a VxFlexOS service - And there are 5 valid snapshots of "default" volume - When I invalidate the Probe cache - And I call ListSnapshots with max_entries "5" and starting_token "" - Then the error contains "has not been probed" - - Scenario: List snapshots with invalid starting token - Given a VxFlexOS service - And there are 5 valid snapshots of "default" volume - When I call Probe - And I call ListSnapshots with max_entries "5" and starting_token "abcd" - Then the error contains "Unable to parse StartingToken" - - Scenario: List snapshots with induced error reading snapshots - Given a VxFlexOS service - And there are 5 valid snapshots of "default" volume - When I call Probe - And I induce error "VolumeInstancesError" - And I call ListSnapshots with max_entries "5" and starting_token "" - Then the error contains "Unable to list snapshots" - - Scenario: List snapshots with induced error badVolID - Given a VxFlexOS service - And there are 1 valid snapshots of "default" volume - When I call Probe - And I induce error "BadVolIDError" - And I call ListSnapshots for volume "default" - Then the error contains "none" - - - Scenario: List snapshots two entries at times - Given a VxFlexOS service - And there are 5 valid snapshots of "default" volume - When I call Probe - Then I call ListSnapshots with max_entries "2" and starting_token "" - And a valid ListSnapshotsResponse is returned with listed "2" and next_token "2" - And I call ListSnapshots with max_entries "2" and starting_token "2" - And a valid ListSnapshotsResponse is returned with listed "2" and next_token "4" - And I call ListSnapshots with max_entries "2" and starting_token "4" - And a valid ListSnapshotsResponse is returned with listed "1" and next_token "" - - Scenario: List snapshots with 50000 entries - Given a VxFlexOS service - And there are 50000 valid snapshots of "default" volume - When I call Probe - Then I call ListSnapshots with max_entries "9999" and starting_token "" - And a valid ListSnapshotsResponse is returned with listed "9999" and next_token "9999" - And I call ListSnapshots with max_entries "9999" and starting_token "9999" - And a valid ListSnapshotsResponse is returned with listed "9999" and next_token "19998" - And I call ListSnapshots with max_entries "9999" and starting_token "19998" - And a valid ListSnapshotsResponse is returned with listed "9999" and next_token "29997" - And I call ListSnapshots with max_entries "9999" and starting_token "29997" - And a valid ListSnapshotsResponse is returned with listed "9999" and next_token "39996" - And I call ListSnapshots with max_entries "9999" and starting_token "39996" - And a valid ListSnapshotsResponse is returned with listed "9999" and next_token "49995" - And I call ListSnapshots with max_entries "9999" and starting_token "49995" - And a valid ListSnapshotsResponse is returned with listed "5" and next_token "" - And the total snapshots listed is "50000" - - Scenario: List snapshots for a given volume ancestor - Given a VxFlexOS service - And a valid volume - And there are 5 valid snapshots of "default" volume - And there are 10 valid snapshots of "alt" volume - When I call Probe - Then I call ListSnapshots for volume "default" - And a valid ListSnapshotsResponse is returned with listed "5" and next_token "" - And I call ListSnapshots for volume "alt" - And a valid ListSnapshotsResponse is returned with listed "10" and next_token "" - - Scenario: List a particular snapshot - Given a VxFlexOS service - And a valid volume - And there are 5 valid snapshots of "default" volume - When I call Probe - Then I call ListSnapshots for snapshot "0000-3" - And a valid ListSnapshotsResponse is returned with listed "1" and next_token "" - And the snapshot ID is "0000-3" - - Scenario: List a particular snapshot with induced error - Given a VxFlexOS service - And a valid volume - And there are 5 valid snapshots of "default" volume - When I call Probe - And I induce error "GetVolByIDError" - Then I call ListSnapshots for snapshot "0000-3" - And the error contains "Unable to list volumes" + As a consumer of the CSI interface + I want to test list service methods + So that they are known to work + + Scenario: Test list volumes allowing an unlimited number of volumes + Given a VxFlex OS service + And there are 5 valid volumes + When I call Probe + And I call ListVolumes with + | max_entries | starting_token | + | 0 | none | + Then a valid ListVolumesResponse is returned + And 5 volumes are listed + + Scenario: Test list volumes, limiting the number of volumes to be less than the number present using max_entries. + Given a VxFlex OS service + And there are 5 valid volumes + When I call Probe + And I call ListVolumes with + | max_entries | starting_token | + | 1 | none | + Then a valid ListVolumesResponse is returned + And 1 volume is listed + + Scenario: Test list volumes starting at a different offset (using next_token) + Given a VxFlex OS service + And there are 5 valid volumes + When I call Probe + And I call ListVolumes with + | max_entries | starting_token | + | 2 | none | + And I call ListVolumes again with + | max_entries | starting_token | + | 3 | next | + Then a valid ListVolumesResponse is returned + And 3 volumes are listed + + Scenario: Test list volumes with an invalid starting token + Given a VxFlex OS service + And a valid volume + When I call Probe + And I call ListVolumes with + | max_entries | starting_token | + | 1 | invalid | + Then an invalid ListVolumesResponse is returned + + Scenario: Test list volumes with induced volume instances error + Given a VxFlex OS service + And a valid volume + And I induce error "VolumeInstancesError" + When I call Probe + And I call ListVolumes with + | max_entries | starting_token | + | 1 | none | + Then the error contains "Unable to list volumes" + + Scenario: Test list volumes with no probe + Given a VxFlex OS service + And a valid volume + And I invalidate the Probe cache + And I call ListVolumes with + | max_entries | starting_token | + | 1 | none | + Then the error contains "has not been probed" + + Scenario: Test list volumes with an starting token greater than volume count + Given a VxFlex OS service + And a valid volume + When I call Probe + And I call ListVolumes with + | max_entries | starting_token | + | 1 | larger | + Then an invalid ListVolumesResponse is returned + + Scenario: List snapshots + Given a VxFlexOS service + And there are 5 valid snapshots of "default" volume + When I call Probe + And I call ListSnapshots with max_entries "5" and starting_token "" + Then a valid ListSnapshotsResponse is returned with listed "5" and next_token "" + Scenario: List snapshots without calling probe + Given a VxFlexOS service + And there are 5 valid snapshots of "default" volume + When I invalidate the Probe cache + And I call ListSnapshots with max_entries "5" and starting_token "" + Then the error contains "has not been probed" + + Scenario: List snapshots with invalid starting token + Given a VxFlexOS service + And there are 5 valid snapshots of "default" volume + When I call Probe + And I call ListSnapshots with max_entries "5" and starting_token "abcd" + Then the error contains "Unable to parse StartingToken" + + Scenario: List snapshots with induced error reading snapshots + Given a VxFlexOS service + And there are 5 valid snapshots of "default" volume + When I call Probe + And I induce error "VolumeInstancesError" + And I call ListSnapshots with max_entries "5" and starting_token "" + Then the error contains "Unable to list snapshots" + + Scenario: List snapshots with induced error badVolID + Given a VxFlexOS service + And there are 1 valid snapshots of "default" volume + When I call Probe + And I induce error "BadVolIDError" + And I call ListSnapshots for volume "default" + Then the error contains "none" + + + Scenario: List snapshots two entries at times + Given a VxFlexOS service + And there are 5 valid snapshots of "default" volume + When I call Probe + Then I call ListSnapshots with max_entries "2" and starting_token "" + And a valid ListSnapshotsResponse is returned with listed "2" and next_token "2" + And I call ListSnapshots with max_entries "2" and starting_token "2" + And a valid ListSnapshotsResponse is returned with listed "2" and next_token "4" + And I call ListSnapshots with max_entries "2" and starting_token "4" + And a valid ListSnapshotsResponse is returned with listed "1" and next_token "" + + Scenario: List snapshots with 50000 entries + Given a VxFlexOS service + And there are 50000 valid snapshots of "default" volume + When I call Probe + Then I call ListSnapshots with max_entries "9999" and starting_token "" + And a valid ListSnapshotsResponse is returned with listed "9999" and next_token "9999" + And I call ListSnapshots with max_entries "9999" and starting_token "9999" + And a valid ListSnapshotsResponse is returned with listed "9999" and next_token "19998" + And I call ListSnapshots with max_entries "9999" and starting_token "19998" + And a valid ListSnapshotsResponse is returned with listed "9999" and next_token "29997" + And I call ListSnapshots with max_entries "9999" and starting_token "29997" + And a valid ListSnapshotsResponse is returned with listed "9999" and next_token "39996" + And I call ListSnapshots with max_entries "9999" and starting_token "39996" + And a valid ListSnapshotsResponse is returned with listed "9999" and next_token "49995" + And I call ListSnapshots with max_entries "9999" and starting_token "49995" + And a valid ListSnapshotsResponse is returned with listed "5" and next_token "" + And the total snapshots listed is "50000" + + Scenario: List snapshots for a given volume ancestor + Given a VxFlexOS service + And a valid volume + And there are 5 valid snapshots of "default" volume + And there are 10 valid snapshots of "alt" volume + When I call Probe + Then I call ListSnapshots for volume "default" + And a valid ListSnapshotsResponse is returned with listed "5" and next_token "" + And I call ListSnapshots for volume "alt" + And a valid ListSnapshotsResponse is returned with listed "10" and next_token "" + + Scenario: List a particular snapshot + Given a VxFlexOS service + And a valid volume + And there are 5 valid snapshots of "default" volume + When I call Probe + Then I call ListSnapshots for snapshot "0000-3" + And a valid ListSnapshotsResponse is returned with listed "1" and next_token "" + And the snapshot ID is "0000-3" + + Scenario: List a particular snapshot with induced error + Given a VxFlexOS service + And a valid volume + And there are 5 valid snapshots of "default" volume + When I call Probe + And I induce error "GetVolByIDError" + Then I call ListSnapshots for snapshot "0000-3" + And the error contains "Unable to list volumes" diff --git a/service/features/node_publish_unpublish.feature b/service/features/node_publish_unpublish.feature index 51381a02..c4876d31 100644 --- a/service/features/node_publish_unpublish.feature +++ b/service/features/node_publish_unpublish.feature @@ -1,7 +1,7 @@ Feature: VxFlex OS CSI interface - As a consumer of the CSI interface - I want to test list service methods - So that they are known to work + As a consumer of the CSI interface + I want to test list service methods + So that they are known to work Scenario Outline: Node publish various use cases from examples @@ -13,13 +13,13 @@ Feature: VxFlex OS CSI interface Then the error contains Examples: - | voltype | access | fstype | errormsg | - | "mount" | "single-writer" | "xfs" | "none" | - | "mount" | "single-writer" | "ext4" | "none" | - | "mount" | "multiple-writer" | "ext4" | "Mount volumes do not support AccessMode MULTI_NODE_MULTI_WRITER" | - | "block" | "single-writer" | "none" | "none" | - | "block" | "multiple-writer" | "none" | "none" | - + | voltype | access | fstype | errormsg | + | "mount" | "single-writer" | "xfs" | "none" | + | "mount" | "single-writer" | "ext4" | "none" | + | "mount" | "multiple-writer" | "ext4" | "Mount volumes do not support AccessMode MULTI_NODE_MULTI_WRITER" | + | "block" | "single-writer" | "none" | "none" | + | "block" | "multiple-writer" | "none" | "none" | + Scenario Outline: Node publish block volumes various induced error use cases from examples Given a VxFlexOS service And a controller published volume @@ -31,23 +31,23 @@ Feature: VxFlex OS CSI interface Then the error contains Examples: - | error | errormsg | - | "NodePublishBlockTargetNotFile" | "existing path is a directory" | - | "GOFSMockBindMountError" | "none" | - | "GOFSMockMountError" | "error bind mounting to target path" | - | "GOFSMockGetMountsError" | "Could not getDevMounts" | - | "NoSymlinkForNodePublish" | "not published to node" | - # may be different for Windows vs. Linux - | "NoBlockDevForNodePublish" | "is not a block device@@not published to node" | - | "TargetNotCreatedForNodePublish" | "none" | - # may be different for Windows vs. Linux - | "PrivateDirectoryNotExistForNodePublish"| "cannot find the path specified@@no such file or directory"| - | "BlockMkfilePrivateDirectoryNodePublish"| "existing path is not a directory" | - | "NodePublishNoTargetPath" | "target path required" | - | "NodePublishNoVolumeCapability" | "volume capability required" | - | "NodePublishNoAccessMode" | "Volume Access Mode is required" | - | "NodePublishNoAccessType" | "Volume Access Type is required" | - | "NodePublishBadTargetPath" | "cannot find the path specified@@no such file or directory"| + | error | errormsg | + | "NodePublishBlockTargetNotFile" | "existing path is a directory" | + | "GOFSMockBindMountError" | "none" | + | "GOFSMockMountError" | "error bind mounting to target path" | + | "GOFSMockGetMountsError" | "Could not getDevMounts" | + | "NoSymlinkForNodePublish" | "not published to node" | + # may be different for Windows vs. Linux + | "NoBlockDevForNodePublish" | "is not a block device@@not published to node" | + | "TargetNotCreatedForNodePublish" | "none" | + # may be different for Windows vs. Linux + | "PrivateDirectoryNotExistForNodePublish" | "cannot find the path specified@@no such file or directory" | + | "BlockMkfilePrivateDirectoryNodePublish" | "existing path is not a directory" | + | "NodePublishNoTargetPath" | "target path required" | + | "NodePublishNoVolumeCapability" | "volume capability required" | + | "NodePublishNoAccessMode" | "Volume Access Mode is required" | + | "NodePublishNoAccessType" | "Volume Access Type is required" | + | "NodePublishBadTargetPath" | "cannot find the path specified@@no such file or directory" | Scenario Outline: Node publish mount volumes various induced error use cases from examples Given a VxFlexOS service @@ -61,26 +61,26 @@ Feature: VxFlex OS CSI interface Then the error contains Examples: - | error | errorb | errormsg | - | "GOFSMockDevMountsError" | "none" | "none" | - | "GOFSMockMountError" | "none" | "mount induced error" | - | "GOFSMockGetMountsError" | "none" | "could not reliably determine existing mount status" | - | "NoSymlinkForNodePublish" | "none" | "not published to node" | - # may be different for Windows vs. Linux - | "NoBlockDevForNodePublish" | "none" | "is not a block device@@not published to node" | - | "TargetNotCreatedForNodePublish" | "none" | "none" | - # may be different for Windows vs. Linux - | "PrivateDirectoryNotExistForNodePublish"| "none" | "cannot find the path specified@@no such file or directory" | - | "BlockMkfilePrivateDirectoryNodePublish"| "none" | "existing path is not a directory" | - | "NodePublishNoTargetPath" | "none" | "target path required" | - | "NodePublishNoVolumeCapability" | "none" | "volume capability required" | - | "NodePublishNoAccessMode" | "none" | "Volume Access Mode is required" | - | "NodePublishNoAccessType" | "none" | "Volume Access Type is required" | - | "NodePublishFileTargetNotDir" | "none" | "existing path is not a directory" | - | "NodePublishPrivateTargetAlreadyCreated"| "none" | "not published to node" | - | "NodePublishPrivateTargetAlreadyMounted"| "none" | "Mount point already in use by device@@none" | - | "NodePublishPrivateTargetAlreadyMounted"| "GOFSMockGetMountsError" | "could not reliably determine existing mount status" | - | "NodePublishBadTargetPath" | "none" | "cannot find the path specified@@no such file or directory"| + | error | errorb | errormsg | + | "GOFSMockDevMountsError" | "none" | "none" | + | "GOFSMockMountError" | "none" | "mount induced error" | + | "GOFSMockGetMountsError" | "none" | "could not reliably determine existing mount status" | + | "NoSymlinkForNodePublish" | "none" | "not published to node" | + # may be different for Windows vs. Linux + | "NoBlockDevForNodePublish" | "none" | "is not a block device@@not published to node" | + | "TargetNotCreatedForNodePublish" | "none" | "none" | + # may be different for Windows vs. Linux + | "PrivateDirectoryNotExistForNodePublish" | "none" | "cannot find the path specified@@no such file or directory" | + | "BlockMkfilePrivateDirectoryNodePublish" | "none" | "existing path is not a directory" | + | "NodePublishNoTargetPath" | "none" | "target path required" | + | "NodePublishNoVolumeCapability" | "none" | "volume capability required" | + | "NodePublishNoAccessMode" | "none" | "Volume Access Mode is required" | + | "NodePublishNoAccessType" | "none" | "Volume Access Type is required" | + | "NodePublishFileTargetNotDir" | "none" | "existing path is not a directory" | + | "NodePublishPrivateTargetAlreadyCreated" | "none" | "not published to node" | + | "NodePublishPrivateTargetAlreadyMounted" | "none" | "Mount point already in use by device@@none" | + | "NodePublishPrivateTargetAlreadyMounted" | "GOFSMockGetMountsError" | "could not reliably determine existing mount status" | + | "NodePublishBadTargetPath" | "none" | "cannot find the path specified@@no such file or directory" | Scenario Outline: Node publish various use cases from examples when volume already published Given a VxFlexOS service @@ -93,13 +93,13 @@ Feature: VxFlex OS CSI interface Then the error contains Examples: - | voltype | access | fstype | errormsg | - | "block" | "single-writer" | "none" | "Access mode conflicts with existing mounts" | - | "block" | "multiple-writer" | "none" | "none" | - | "mount" | "single-writer" | "xfs" | "Access mode conflicts with existing mounts" | - | "mount" | "single-writer" | "ext4" | "Access mode conflicts with existing mounts" | - | "mount" | "multiple-writer" | "ext4" | "Mount volumes do not support AccessMode MULTI_NODE_MULTI_WRITER" | - | "block" | "multiple-reader" | "none" | "none" | + | voltype | access | fstype | errormsg | + | "block" | "single-writer" | "none" | "Access mode conflicts with existing mounts" | + | "block" | "multiple-writer" | "none" | "none" | + | "mount" | "single-writer" | "xfs" | "Access mode conflicts with existing mounts" | + | "mount" | "single-writer" | "ext4" | "Access mode conflicts with existing mounts" | + | "mount" | "multiple-writer" | "ext4" | "Mount volumes do not support AccessMode MULTI_NODE_MULTI_WRITER" | + | "block" | "multiple-reader" | "none" | "none" | Scenario Outline: Node publish various use cases from examples when read-only mount volume already published Given a VxFlexOS service @@ -113,13 +113,13 @@ Feature: VxFlex OS CSI interface Then the error contains Examples: - | voltype | access | fstype | errormsg | - | "block" | "multiple-reader" | "none" | "read only not supported for Block Volume" | - | "mount" | "single-reader" | "none" | "none" | - | "mount" | "single-reader" | "xfs" | "none" | - | "mount" | "multiple-reader" | "ext4" | "none" | - | "mount" | "single-writer" | "ext4" | "Access mode conflicts with existing mounts" | - | "mount" | "multiple-writer" | "ext4" | "do not support AccessMode MULTI_NODE_MULTI_WRITER" | + | voltype | access | fstype | errormsg | + | "block" | "multiple-reader" | "none" | "read only not supported for Block Volume" | + | "mount" | "single-reader" | "none" | "none" | + | "mount" | "single-reader" | "xfs" | "none" | + | "mount" | "multiple-reader" | "ext4" | "none" | + | "mount" | "single-writer" | "ext4" | "Access mode conflicts with existing mounts" | + | "mount" | "multiple-writer" | "ext4" | "do not support AccessMode MULTI_NODE_MULTI_WRITER" | Scenario Outline: Node publish various use cases from examples when read-only mount volume already published and I change the target path Given a VxFlexOS service @@ -134,13 +134,13 @@ Feature: VxFlex OS CSI interface Then the error contains Examples: - | voltype | access | fstype | errormsg | - | "mount" | "single-reader" | "none" | "none" | - | "mount" | "single-reader" | "xfs" | "none" | - | "block" | "multiple-reader" | "none" | "read only not supported for Block Volume" | - | "mount" | "multiple-reader" | "ext4" | "none" | - | "mount" | "single-writer" | "ext4" | "Access mode conflicts with existing mounts" | - | "mount" | "multiple-writer" | "ext4" | "do not support AccessMode MULTI_NODE_MULTI_WRITER" | + | voltype | access | fstype | errormsg | + | "mount" | "single-reader" | "none" | "none" | + | "mount" | "single-reader" | "xfs" | "none" | + | "block" | "multiple-reader" | "none" | "read only not supported for Block Volume" | + | "mount" | "multiple-reader" | "ext4" | "none" | + | "mount" | "single-writer" | "ext4" | "Access mode conflicts with existing mounts" | + | "mount" | "multiple-writer" | "ext4" | "do not support AccessMode MULTI_NODE_MULTI_WRITER" | Scenario: Node publish volume with volume context Given a VxFlexOS service @@ -150,7 +150,7 @@ Feature: VxFlex OS CSI interface And I give request volume context When I call Probe And I call NodePublishVolume "SDC_GUID" - Then the error contains "none" + Then the error contains "none" Scenario Outline: Node Unpublish various use cases from examples Given a VxFlexOS service @@ -163,10 +163,10 @@ Feature: VxFlex OS CSI interface Then the error contains Examples: - | voltype | access | fstype | errormsg | - | "block" | "single-writer" | "none" | "none" | - | "block" | "multiple-writer" | "none" | "none" | - | "mount" | "single-writer" | "xfs" | "none" | + | voltype | access | fstype | errormsg | + | "block" | "single-writer" | "none" | "none" | + | "block" | "multiple-writer" | "none" | "none" | + | "mount" | "single-writer" | "xfs" | "none" | Scenario Outline: Node Unpublish mount volumes various induced error use cases from examples Given a VxFlexOS service @@ -180,12 +180,12 @@ Feature: VxFlex OS CSI interface Then the error contains Examples: - | error | errormsg | - | "NodeUnpublishBadVolume" | "none" | - | "GOFSMockGetMountsError" | "could not reliably determine existing mount status" | - | "NodeUnpublishNoTargetPath" | "target path argument is required" | - | "GOFSMockUnmountError" | "Error unmounting target" | - | "PrivateDirectoryNotExistForNodePublish"| "none" | + | error | errormsg | + | "NodeUnpublishBadVolume" | "none" | + | "GOFSMockGetMountsError" | "could not reliably determine existing mount status" | + | "NodeUnpublishNoTargetPath" | "target path argument is required" | + | "GOFSMockUnmountError" | "Error unmounting target" | + | "PrivateDirectoryNotExistForNodePublish" | "none" | Scenario: Get device given invalid path Given a VxFlexOS service diff --git a/service/features/service.feature b/service/features/service.feature index f2f235b6..3bab46d1 100644 --- a/service/features/service.feature +++ b/service/features/service.feature @@ -1,629 +1,661 @@ Feature: VxFlex OS CSI interface - As a consumer of the CSI interface - I want to test service methods - So that they are known to work - - Scenario: Identity GetPluginInfo good call - Given a VxFlexOS service - When I call GetPluginInfo - Then a valid GetPlugInfoResponse is returned - Scenario: Identity GetPluginCapabilitiles good call - Given a VxFlexOS service - When I call GetPluginCapabilities - Then a valid GetPluginCapabilitiesResponse is returned - - Scenario: Identity Probe good call - Given a VxFlexOS service - When I call Probe - Then a valid ProbeResponse is returned - - Scenario: Identity Probe call no controller connection - Given a VxFlexOS service - And the Controller has no connection - When I invalidate the Probe cache - And I call Probe - Then the error contains "unable to login to VxFlexOS Gateway" - - - Scenario Outline: Probe Call with various errors - Given a VxFlexOS service - And I induce error - When I invalidate the Probe cache - And I call Probe - Then the error contains - -Examples: -| error | msg | -| "NoEndpointError" | "missing VxFlexOS Gateway endpoint" | -| "NoUserError" | "missing VxFlexOS MDM user" | -| "NoPasswordError" | "missing VxFlexOS MDM password" | -| "NoSysNameError" | "missing VxFlexOS system name" | -| "WrongSysNameError" | "unable to find matching VxFlexOS system name" | - - -# This injected error fails on Windows with no SDC but passes on Linux with SDC - Scenario: Identity Probe call node probe Lsmod error - Given a VxFlexOS service - And there is a Node Probe Lsmod error - When I invalidate the Probe cache - And I call Probe - Then the possible error contains "scini kernel module not loaded" - -# This injected error fails on Windows with no SDC but passes on Linux with SDC - Scenario: Identity Probe call node probe SdcGUID error - Given a VxFlexOS service - And there is a Node Probe SdcGUID error - When I call Probe - Then the possible error contains "unable to get SDC GUID" - - Scenario: Identity Probe call node probe drvCfg error - Given a VxFlexOS service - And there is a Node Probe drvCfg error - When I call Probe - Then the possible error contains "unable to get System Name via config or drv_cfg binary" - - Scenario Outline: Create volume good scenario - Given a VxFlexOS service - When I call Probe - And I call CreateVolume - Then a valid CreateVolumeResponse is returned - - Examples: - | name | - | "volume1" | - | "thisnameiswaytoolongtopossiblybeunder31characters" | - - - Scenario: Create volume with admin error - Given a VxFlexOS service - When I call Probe - And I induce error "NoAdminError" - And I call CreateVolume "volume1" - Then a valid CreateVolumeResponse is returned - - Scenario: Create Volume with invalid probe cache, no endpoint, and no admin - Given a VxFlexOS service - When I induce error "NoAdminError" - And I induce error "NoEndpointError" - And I invalidate the Probe cache - And I call CreateVolume "volume1" - Then the error contains "failed to probe/init plugin:" - - - Scenario: Idempotent create volume with duplicate volume name - Given a VxFlexOS service - When I call Probe - And I call CreateVolume "volume2" - And I call CreateVolume "volume2" - Then a valid CreateVolumeResponse is returned - - Scenario: Idempotent create volume with different sizes - Given a VxFlexOS service - When I call Probe - And I call CreateVolumeSize "volume3" "8" - And I call CreateVolumeSize "volume3" "16" - Then the error contains "different size than requested" - - Scenario: Idempotent create volume with different sizes and induced error in handleQueryVolumeIDByKey - Given a VxFlexOS service - When I call Probe - And I call CreateVolumeSize "volume3" "8" - And I induce error "FindVolumeIDError" - And I call CreateVolumeSize "volume3" "16" - Then the error contains "induced error" - - Scenario: Idempotent create volume with different sizes and induced error in handleInstances - Given a VxFlexOS service - When I call Probe - And I call CreateVolumeSize "volume3" "8" - And I induce error "GetVolByIDError" - And I call CreateVolumeSize "volume3" "16" - Then the error contains "induced error" - - Scenario: Idempotent create volume with different sizes and induced error in handleStoragePoolInstances - Given a VxFlexOS service - When I call Probe - And I call CreateVolumeSize "volume3" "8" - And I induce error "GetStoragePoolsError" - And I call CreateVolumeSize "volume3" "16" - Then the error contains "induced error" - - Scenario: Idempotent create volume with different storage pool - Given a VxFlexOS service - When I call Probe - And I call CreateVolume "volume4" - And I change the StoragePool "other_storage_pool" - And I call CreateVolume "volume4" - Then the error contains "different storage pool" - - Scenario: Idempotent create volume with bad storage pool - Given a VxFlexOS service - When I call Probe - And I call CreateVolume "volume4" - And I change the StoragePool "no_storage_pool" - And I call CreateVolume "volume4" - Then the error contains "Couldn't find storage pool" - - Scenario Outline: Create volume with Accessibility Requirements - Given a VxFlexOS service - When I call Probe - And I specify AccessibilityRequirements with a SystemID of - And I call CreateVolume "accessibility" - Then the error contains - - Examples: - | sysID | errormsg | - | "f.service.opt.SystemName" | "none" | - | "" | "unknown to this controller" | - | "Unknown" | "unknown to this controller" | - | "badSystem" | "unknown to this controller" | - - Scenario: Create volume with VolumeContentSource - Given a VxFlexOS service - When I call Probe - And I specify VolumeContentSource - And I call CreateVolume "volumecontentsource" - Then the error contains "Volume as a VolumeContentSource is not supported" - - Scenario: Create volume with AccessMode_MULTINODE_WRITER - Given a VxFlexOS service - When I call Probe - And I specify MULTINODE_WRITER - And I call CreateVolume "multi-writer" - Then a valid CreateVolumeResponse is returned - - Scenario: Attempt create volume with no name - Given a VxFlexOS service - When I call Probe - And I call CreateVolume "" - Then the error contains "Name cannot be empty" - - Scenario: Create volume with bad capacity - Given a VxFlexOS service - When I call Probe - And I specify a BadCapacity - And I call CreateVolume "bad capacity" - Then the error contains "bad capacity" - - Scenario: Create volume with no storage pool - Given a VxFlexOS service - When I call Probe - And I specify NoStoragePool - And I call CreateVolume "no storage pool" - Then the error contains "storagepool is a required parameter" - - Scenario: Create mount volume good scenario - Given a VxFlexOS service - When I call Probe - When I specify CreateVolumeMountRequest "xfs" - And I call CreateVolume "volume1" - Then a valid CreateVolumeResponse is returned - - Scenario: Create mount volume idempotent test - Given a VxFlexOS service - When I call Probe - When I specify CreateVolumeMountRequest "xfs" - And I call CreateVolume "volume2" - And I call CreateVolume "volume2" - Then a valid CreateVolumeResponse is returned - - Scenario: Call NodeGetInfo and validate NodeId - Given a VxFlexOS service - When I call NodeGetInfo - Then a valid NodeGetInfoResponse is returned - - Scenario: Call NodeGetInfo which requires probe and returns error - Given a VxFlexOS service - And I induce error "require-probe" - When I call NodeGetInfo - Then the error contains "Node Service has not been probed" - - Scenario: Call GetCapacity without specifying Storage Pool Name (this returns overall capacity) - Given a VxFlexOS service - When I call Probe - And I call GetCapacity with storage pool "" - Then a valid GetCapacityResponse is returned - - Scenario: Call GetCapacity with valid Storage Pool Name - Given a VxFlexOS service - When I call Probe - And I call GetCapacity with storage pool "viki_pool_HDD_20181031" - Then a valid GetCapacityResponse is returned - - Scenario: Call GetCapacity without specifying Storage Pool and without probe - Given a VxFlexOS service - When I invalidate the Probe cache - And I call GetCapacity with storage pool "" - Then the error contains "Controller Service has not been probed" - - Scenario: Call GetCapacity with invalid Storage Pool name - Given a VxFlexOS service - When I call Probe - And I call GetCapacity with storage pool "xxx" - Then the error contains "unable to look up storage pool" - - Scenario: Call GetCapacity with induced error retrieving statistics - Given a VxFlexOS service - When I call Probe - And I induce error "GetStatisticsError" - And I call GetCapacity with storage pool "viki_pool_HDD_20181031" - Then the error contains "unable to get system stats" - - Scenario: Call ControllerGetCapabilities - Given a VxFlexOS service - When I call ControllerGetCapabilities - Then a valid ControllerGetCapabilitiesResponse is returned - - Scenario Outline: Calls to validate volume capabilities - Given a VxFlexOS service - When I call Probe - And I call CreateVolume "volume1" - And a valid CreateVolumeResponse is returned - And I call ValidateVolumeCapabilities with voltype access fstype - Then the error contains - - Examples: - | voltype | access | fstype | errormsg | - | "block" | "single-writer" | "none" | "none" | - | "block" | "multi-reader" | "none" | "none" | - | "mount" | "multi-writer" | "ext4" | "multi-node with writer(s) only supported for block access type" | - | "mount" | "multi-node-single-writer" | "ext4" | "multi-node with writer(s) only supported for block access type" | - | "mount" | "unknown" | "ext4" | "access mode cannot be UNKNOWN" | - | "none " | "unknown" | "ext4" | "unknown access type is not Block or Mount" | - - Scenario Outline: Call validate volume capabilities with non-existent volume - Given a VxFlexOS service - When I call Probe - And an invalid volume - And I call ValidateVolumeCapabilities with voltype access fstype - Then the error contains - - Examples: - | voltype | access | fstype | errormsg | - | "block" | "single-writer" | "none" | "volume not found" | - - Scenario Outline: Call with no probe volume to validate volume capabilities - Given a VxFlexOS service - When I invalidate the Probe cache - And I call ValidateVolumeCapabilities with voltype access fstype - Then the error contains - - Examples: - | voltype | access | fstype | errormsg | - | "block" | "single-writer" | "none" | "Service has not been probed" | - - Scenario: Call with ValidateVolumeCapabilities with bad vol ID - Given a VxFlexOS service - When I call Probe - And I call CreateVolume "volume1" - And a valid CreateVolumeResponse is returned - And I induce error "BadVolIDError" - And I call ValidateVolumeCapabilities with voltype "block" access "single-writer" fstype "none" - Then the error contains "volume not found" - - Scenario: Call NodeStageVolume, should get unimplemented - Given a VxFlexOS service - And I call Probe - When I call NodeStageVolume - Then the error contains "Unimplemented" - - Scenario: Call NodeUnstageVolume, should get unimplemented - Given a VxFlexOS service - And I call Probe - When I call NodeUnstageVolume - Then the error contains "Unimplemented" - - - Scenario: Call NodeGetCapabilities should return a valid response - Given a VxFlexOS service - And I call Probe - When I call NodeGetCapabilities - Then a valid NodeGetCapabilitiesResponse is returned - - Scenario: Snapshot a single block volume - Given a VxFlexOS service - When I call Probe - And I call CreateVolume "vol1" - And a valid CreateVolumeResponse is returned - And I call CreateSnapshot "snap1" - Then a valid CreateSnapshotResponse is returned - - Scenario: Idempotent test of snapshot a single block volume - Given a VxFlexOS service - When I call Probe - And I call CreateVolume "vol1" - And a valid CreateVolumeResponse is returned - And I call CreateSnapshot "snapshot-a5d67905-14e9-11e9-ab1c-005056264ad3" - And no error was received - And I call CreateSnapshot "snapshot-a5d67905-14e9-11e9-ab1c-005056264ad3" - Then a valid CreateSnapshotResponse is returned - And no error was received - - Scenario: Request to create Snapshot with same name and different SourceVolumeID - Given a VxFlexOS service - When I call Probe - And I call CreateVolume "vol1" - And a valid CreateVolumeResponse is returned - And I call CreateSnapshot "snap1" - And no error was received - And I call CreateVolume "A Different Volume" - And a valid CreateVolumeResponse is returned - And I induce error "WrongVolIDError" - And I call CreateSnapshot "snap1" - Then the error contains "Failed to create snapshot" - - Scenario: Snapshot a single block volume but receive error - Given a VxFlexOS service - When I call Probe - And I induce error "CreateSnapshotError" - And I call CreateVolume "vol1" - And a valid CreateVolumeResponse is returned - And I call CreateSnapshot "" - Then the error contains "snapshot name cannot be Nil" - - Scenario: Call snapshot create with invalid volume - Given a VxFlexOS service - And an invalid volume - When I call Probe - And I call CreateSnapshot "snap1" - Then the error contains "volume not found" - - Scenario: Call snapshot create with no volume - Given a VxFlexOS service - And no volume - When I call Probe - And I call CreateSnapshot "snap1" - Then the error contains "volume ID to be snapped is required" - - Scenario: Call snapshot with no probe - Given a VxFlexOS service - And an invalid volume - When I invalidate the Probe cache - And I call CreateSnapshot "snap1" - Then the error contains "Controller Service has not been probed" - - Scenario: Snapshot a block volume consistency group - Given a VxFlexOS service - When I call Probe - And I call CreateVolume "vol1" - And a valid CreateVolumeResponse is returned - And I call CreateVolume "vol2" - And a valid CreateVolumeResponse is returned - And I call CreateVolume "vol3" - And a valid CreateVolumeResponse is returned - And I call CreateSnapshot "snap1" - Then a valid CreateSnapshotResponse is returned - - Scenario: Delete a snapshot - Given a VxFlexOS service - And a valid snapshot - When I call Probe - And I call DeleteSnapshot - Then no error was received - - Scenario: Idempotent delete a snapshot - Given a VxFlexOS service - And a valid snapshot - When I call Probe - And I call DeleteSnapshot - Then no error was received - And I call DeleteSnapshot - Then no error was received - - Scenario: Delete a snapshot with bad Vol ID - Given a VxFlexOS service - And a valid snapshot - When I call Probe - And I induce error "BadVolIDError" - And I call DeleteSnapshot - Then no error was received - - Scenario: Delete a snapshot with no probe - Given a VxFlexOS service - And a valid snapshot - When I invalidate the Probe cache - And I call DeleteSnapshot - Then the error contains "Controller Service has not been probed" - - Scenario: Delete a snapshot with invalid volume - Given a VxFlexOS service - And an invalid volume - When I call Probe - And I call DeleteSnapshot - Then the error contains "volume not found" - - Scenario: Delete a snapshot with no volume - Given a VxFlexOS service - And no volume - When I call Probe - And I call DeleteSnapshot - Then the error contains "snapshot ID to be deleted is required" - - Scenario: Delete snapshot that is mapped to an SDC - Given a VxFlexOS service - And a valid snapshot - And the volume is already mapped to an SDC - When I call Probe - And I call DeleteSnapshot - Then the error contains "snapshot is in use by the following SDC" - - Scenario: Delete snapshot with induced remove volume error - Given a VxFlexOS service - And a valid snapshot - And I induce error "RemoveVolumeError" - When I call Probe - And I call DeleteSnapshot - Then the error contains "error removing snapshot" - - Scenario: Delete snapshot consistency group - Given a VxFlexOS service - And a valid snapshot consistency group - When I call Probe - And I call DeleteSnapshot - Then no error was received - And I call DeleteSnapshot - Then no error was received - - Scenario: Delete snapshot consistency group with mapped volumes - Given a VxFlexOS service - And a valid snapshot consistency group - When I call Probe - And I call PublishVolume with "single-writer" - And a valid PublishVolumeResponse is returned - And I call DeleteSnapshot - Then the error contains "One or more consistency group volumes are exposed and may be in use" - - Scenario: Delete snapshot consistency with induced remove volume error - Given a VxFlexOS service - And a valid snapshot consistency group - And I induce error "RemoveVolumeError" - When I call Probe - And I call DeleteSnapshot - Then the error contains "error removing snapshot" - - Scenario: Create a volume from a snapshot - Given a VxFlexOS service - And a valid snapshot - When I call Probe - And I call Create Volume from Snapshot - Then a valid CreateVolumeResponse is returned - And no error was received - - Scenario: Create a volume from a snapshot with wrong capacity - Given a VxFlexOS service - And a valid snapshot - And the wrong capacity - When I call Probe - And I call Create Volume from Snapshot - Then the error contains "incompatible size" - - Scenario: Create a volume from a snapshot with wrong storage pool - Given a VxFlexOS service - And a valid snapshot - And the wrong storage pool - When I call Probe - And I call Create Volume from Snapshot - Then the error contains "different than the requested storage pool" - - Scenario: Create a volume from a snapshot with induced volume not found - Given a VxFlexOS service - And a valid snapshot - And I induce error "GetVolByIDError" - When I call Probe - And I call Create Volume from Snapshot - Then the error contains "Snapshot not found" - - Scenario: Create a volume from a snapshot with induced create snapshot error - Given a VxFlexOS service - And a valid snapshot - And I induce error "CreateSnapshotError" - When I call Probe - And I call Create Volume from Snapshot - Then the error contains "Failed to create snapshot" - - - Scenario: Idempotent create a volume from a snapshot - Given a VxFlexOS service - And a valid snapshot - When I call Probe - And I call Create Volume from Snapshot - And a valid CreateVolumeResponse is returned - And no error was received - Then I call Create Volume from Snapshot - And no error was received - And a valid CreateVolumeResponse is returned - - - Scenario Outline: Call ControllerExpandVolume - Given a VxFlexOS service - And I call Probe - And I call CreateVolumeSize "volume10" "32" - And a valid CreateVolumeResponse is returned - And I induce error - Then I call ControllerExpandVolume set to - And the error contains - And I call ControllerExpandVolume set to - Then the error contains - - Examples: - | error | GB | errmsg | - | "none" | 32 | "none" | - | "SetVolumeSizeError" | 64 | "induced error" | - | "none" | 16 | "none" | - | "NoVolumeIDError" | 64 | "Volume ID is required" | - | "none" | 64 | "none" | - | "GetVolByIDError" | 64 | "induced error" | - - Scenario Outline: Call NodeExpandVolume - Given a VxFlexOS service - And I call Probe - And I call CreateVolumeSize "volume4" "32" - And a controller published volume - And a capability with voltype "mount" access "single-writer" fstype "xfs" - And get Node Publish Volume Request - And I call NodePublishVolume "SDC_GUID" - And no error was received - And I induce error - When I call NodeExpandVolume with volumePath as - Then the error contains - - Examples: - | error | volPath | errormsg | - | "none" | "" | "Volume path required" | - | "none" | "test/tmp/datadir" | "none" | - | "GOFSInduceFSTypeError" | "test/tmp/datadir" | "Failed to fetch filesystem" | - | "GOFSInduceResizeFSError" | "test/tmp/datadir" | "Failed to resize device" | - | "NoVolumeIDError" | "test/tmp/datadir" | "Volume ID is required" | - | "none" | "not/a/path/1234" | "Could not stat volume path" | - | "none" | "test/tmp/datafile" | "none" | - - Scenario: Call NodeGetVolumeStats, should get unimplemented - Given a VxFlexOS service - When I call NodeGetVolumeStats - Then the error contains "Unimplemented" - - Scenario: Call New in service, a new service should return - Given a VxFlexOS service - When I call NewService - Then a new service is returned - - Scenario: Call getVolProvisionType with bad params - Given a VxFlexOS service - When I call getVolProvisionType with bad params - Then the error contains "getVolProvisionType - invalid boolean received" - - Scenario: Call getstoragepool with wrong ID - Given a VxFlexOS service - When i Call getStoragePoolnameByID "123" - Then the error contains "cannot find storage pool" - - Scenario: Test BeforeServe - Given a VxFlexOS service - And I invalidate the Probe cache - When I call BeforeServe - # Get different error message on Windows vs. Linux - Then the error contains "Unable to initialize cert pool from system@@unable to login to VxFlexOS Gateway@@unable to get SDC GUID" - - Scenario: Call Node getAllSystems - Given a VxFlexOS service - And I do not have a gateway connection - When I Call nodeGetAllSystems - - Scenario: Call Node getAllSystems - Given a VxFlexOS service - And I do not have a gateway connection - And I do not have a valid gateway endpoint - When I Call nodeGetAllSystems - Then the error contains "Unable to create ScaleIO client" - - Scenario: Call Node getAllSystems - Given a VxFlexOS service - And I do not have a gateway connection - And I do not have a valid gateway password - When I Call nodeGetAllSystems - Then the error contains "Unable to create ScaleIO client" - - Scenario: Call evalsymlinks - Given a VxFlexOS service - When I call evalsymlink "invalidpath" - Then the error contains "Could not evaluate symlinks for path" + As a consumer of the CSI interface + I want to test service methods + So that they are known to work + + Scenario: Identity GetPluginInfo good call + Given a VxFlexOS service + When I call GetPluginInfo + Then a valid GetPlugInfoResponse is returned + Scenario: Identity GetPluginCapabilitiles good call + Given a VxFlexOS service + When I call GetPluginCapabilities + Then a valid GetPluginCapabilitiesResponse is returned + + Scenario: Identity Probe good call + Given a VxFlexOS service + When I call Probe + Then a valid ProbeResponse is returned + + Scenario: Identity Probe call no controller connection + Given a VxFlexOS service + And the Controller has no connection + When I invalidate the Probe cache + And I call Probe + Then the error contains "unable to login to VxFlexOS Gateway" + + + Scenario Outline: Probe Call with various errors + Given a VxFlexOS service + And I induce error + When I invalidate the Probe cache + And I call Probe + Then the error contains + + Examples: + | error | msg | + | "NoEndpointError" | "missing VxFlexOS Gateway endpoint" | + | "NoUserError" | "missing VxFlexOS MDM user" | + | "NoPasswordError" | "missing VxFlexOS MDM password" | + | "NoSysNameError" | "missing VxFlexOS system name" | + | "WrongSysNameError" | "unable to find matching VxFlexOS system name" | + + + # This injected error fails on Windows with no SDC but passes on Linux with SDC + Scenario: Identity Probe call node probe Lsmod error + Given a VxFlexOS service + And there is a Node Probe Lsmod error + When I invalidate the Probe cache + And I call Probe + Then the possible error contains "scini kernel module not loaded" + + # This injected error fails on Windows with no SDC but passes on Linux with SDC + Scenario: Identity Probe call node probe SdcGUID error + Given a VxFlexOS service + And there is a Node Probe SdcGUID error + When I call Probe + Then the possible error contains "unable to get SDC GUID" + + Scenario: Identity Probe call node probe drvCfg error + Given a VxFlexOS service + And there is a Node Probe drvCfg error + When I call Probe + Then the possible error contains "unable to get System Name via config or drv_cfg binary" + + Scenario Outline: Create volume good scenario + Given a VxFlexOS service + When I call Probe + And I call CreateVolume + Then a valid CreateVolumeResponse is returned + + Examples: + | name | + | "volume1" | + | "thisnameiswaytoolongtopossiblybeunder31characters" | + + + Scenario: Create volume with admin error + Given a VxFlexOS service + When I call Probe + And I induce error "NoAdminError" + And I call CreateVolume "volume1" + Then a valid CreateVolumeResponse is returned + + Scenario: Create Volume with invalid probe cache, no endpoint, and no admin + Given a VxFlexOS service + When I induce error "NoAdminError" + And I induce error "NoEndpointError" + And I invalidate the Probe cache + And I call CreateVolume "volume1" + Then the error contains "failed to probe/init plugin:" + + + Scenario: Idempotent create volume with duplicate volume name + Given a VxFlexOS service + When I call Probe + And I call CreateVolume "volume2" + And I call CreateVolume "volume2" + Then a valid CreateVolumeResponse is returned + + Scenario: Idempotent create volume with different sizes + Given a VxFlexOS service + When I call Probe + And I call CreateVolumeSize "volume3" "8" + And I call CreateVolumeSize "volume3" "16" + Then the error contains "different size than requested" + + Scenario: Idempotent create volume with different sizes and induced error in handleQueryVolumeIDByKey + Given a VxFlexOS service + When I call Probe + And I call CreateVolumeSize "volume3" "8" + And I induce error "FindVolumeIDError" + And I call CreateVolumeSize "volume3" "16" + Then the error contains "induced error" + + Scenario: Idempotent create volume with different sizes and induced error in handleInstances + Given a VxFlexOS service + When I call Probe + And I call CreateVolumeSize "volume3" "8" + And I induce error "GetVolByIDError" + And I call CreateVolumeSize "volume3" "16" + Then the error contains "induced error" + + Scenario: Idempotent create volume with different sizes and induced error in handleStoragePoolInstances + Given a VxFlexOS service + When I call Probe + And I call CreateVolumeSize "volume3" "8" + And I induce error "GetStoragePoolsError" + And I call CreateVolumeSize "volume3" "16" + Then the error contains "induced error" + + Scenario: Idempotent create volume with different storage pool + Given a VxFlexOS service + When I call Probe + And I call CreateVolume "volume4" + And I change the StoragePool "other_storage_pool" + And I call CreateVolume "volume4" + Then the error contains "different storage pool" + + Scenario: Idempotent create volume with bad storage pool + Given a VxFlexOS service + When I call Probe + And I call CreateVolume "volume4" + And I change the StoragePool "no_storage_pool" + And I call CreateVolume "volume4" + Then the error contains "Couldn't find storage pool" + + Scenario Outline: Create volume with Accessibility Requirements + Given a VxFlexOS service + When I call Probe + And I specify AccessibilityRequirements with a SystemID of + And I call CreateVolume "accessibility" + Then the error contains + + Examples: + | sysID | errormsg | + | "f.service.opt.SystemName" | "none" | + | "" | "unknown to this controller" | + | "Unknown" | "unknown to this controller" | + | "badSystem" | "unknown to this controller" | + + Scenario: Create volume with AccessMode_MULTINODE_WRITER + Given a VxFlexOS service + When I call Probe + And I specify MULTINODE_WRITER + And I call CreateVolume "multi-writer" + Then a valid CreateVolumeResponse is returned + + Scenario: Attempt create volume with no name + Given a VxFlexOS service + When I call Probe + And I call CreateVolume "" + Then the error contains "Name cannot be empty" + + Scenario: Create volume with bad capacity + Given a VxFlexOS service + When I call Probe + And I specify a BadCapacity + And I call CreateVolume "bad capacity" + Then the error contains "bad capacity" + + Scenario: Create volume with no storage pool + Given a VxFlexOS service + When I call Probe + And I specify NoStoragePool + And I call CreateVolume "no storage pool" + Then the error contains "storagepool is a required parameter" + + Scenario: Create mount volume good scenario + Given a VxFlexOS service + When I call Probe + When I specify CreateVolumeMountRequest "xfs" + And I call CreateVolume "volume1" + Then a valid CreateVolumeResponse is returned + + Scenario: Create mount volume idempotent test + Given a VxFlexOS service + When I call Probe + When I specify CreateVolumeMountRequest "xfs" + And I call CreateVolume "volume2" + And I call CreateVolume "volume2" + Then a valid CreateVolumeResponse is returned + + Scenario: Call NodeGetInfo and validate NodeId + Given a VxFlexOS service + When I call NodeGetInfo + Then a valid NodeGetInfoResponse is returned + + Scenario: Call NodeGetInfo which requires probe and returns error + Given a VxFlexOS service + And I induce error "require-probe" + When I call NodeGetInfo + Then the error contains "Node Service has not been probed" + + Scenario: Call GetCapacity without specifying Storage Pool Name (this returns overall capacity) + Given a VxFlexOS service + When I call Probe + And I call GetCapacity with storage pool "" + Then a valid GetCapacityResponse is returned + + Scenario: Call GetCapacity with valid Storage Pool Name + Given a VxFlexOS service + When I call Probe + And I call GetCapacity with storage pool "viki_pool_HDD_20181031" + Then a valid GetCapacityResponse is returned + + Scenario: Call GetCapacity without specifying Storage Pool and without probe + Given a VxFlexOS service + When I invalidate the Probe cache + And I call GetCapacity with storage pool "" + Then the error contains "Controller Service has not been probed" + + Scenario: Call GetCapacity with invalid Storage Pool name + Given a VxFlexOS service + When I call Probe + And I call GetCapacity with storage pool "xxx" + Then the error contains "unable to look up storage pool" + + Scenario: Call GetCapacity with induced error retrieving statistics + Given a VxFlexOS service + When I call Probe + And I induce error "GetStatisticsError" + And I call GetCapacity with storage pool "viki_pool_HDD_20181031" + Then the error contains "unable to get system stats" + + Scenario: Call ControllerGetCapabilities + Given a VxFlexOS service + When I call ControllerGetCapabilities + Then a valid ControllerGetCapabilitiesResponse is returned + + Scenario Outline: Calls to validate volume capabilities + Given a VxFlexOS service + When I call Probe + And I call CreateVolume "volume1" + And a valid CreateVolumeResponse is returned + And I call ValidateVolumeCapabilities with voltype access fstype + Then the error contains + + Examples: + | voltype | access | fstype | errormsg | + | "block" | "single-writer" | "none" | "none" | + | "block" | "multi-reader" | "none" | "none" | + | "mount" | "multi-writer" | "ext4" | "multi-node with writer(s) only supported for block access type" | + | "mount" | "multi-node-single-writer" | "ext4" | "multi-node with writer(s) only supported for block access type" | + | "mount" | "unknown" | "ext4" | "access mode cannot be UNKNOWN" | + | "none " | "unknown" | "ext4" | "unknown access type is not Block or Mount" | + + Scenario Outline: Call validate volume capabilities with non-existent volume + Given a VxFlexOS service + When I call Probe + And an invalid volume + And I call ValidateVolumeCapabilities with voltype access fstype + Then the error contains + + Examples: + | voltype | access | fstype | errormsg | + | "block" | "single-writer" | "none" | "volume not found" | + + Scenario Outline: Call with no probe volume to validate volume capabilities + Given a VxFlexOS service + When I invalidate the Probe cache + And I call ValidateVolumeCapabilities with voltype access fstype + Then the error contains + + Examples: + | voltype | access | fstype | errormsg | + | "block" | "single-writer" | "none" | "Service has not been probed" | + + Scenario: Call with ValidateVolumeCapabilities with bad vol ID + Given a VxFlexOS service + When I call Probe + And I call CreateVolume "volume1" + And a valid CreateVolumeResponse is returned + And I induce error "BadVolIDError" + And I call ValidateVolumeCapabilities with voltype "block" access "single-writer" fstype "none" + Then the error contains "volume not found" + + Scenario: Call NodeStageVolume, should get unimplemented + Given a VxFlexOS service + And I call Probe + When I call NodeStageVolume + Then the error contains "Unimplemented" + + Scenario: Call NodeUnstageVolume, should get unimplemented + Given a VxFlexOS service + And I call Probe + When I call NodeUnstageVolume + Then the error contains "Unimplemented" + + + Scenario: Call NodeGetCapabilities should return a valid response + Given a VxFlexOS service + And I call Probe + When I call NodeGetCapabilities + Then a valid NodeGetCapabilitiesResponse is returned + + Scenario: Snapshot a single block volume + Given a VxFlexOS service + When I call Probe + And I call CreateVolume "vol1" + And a valid CreateVolumeResponse is returned + And I call CreateSnapshot "snap1" + Then a valid CreateSnapshotResponse is returned + + Scenario: Idempotent test of snapshot a single block volume + Given a VxFlexOS service + When I call Probe + And I call CreateVolume "vol1" + And a valid CreateVolumeResponse is returned + And I call CreateSnapshot "snapshot-a5d67905-14e9-11e9-ab1c-005056264ad3" + And no error was received + And I call CreateSnapshot "snapshot-a5d67905-14e9-11e9-ab1c-005056264ad3" + Then a valid CreateSnapshotResponse is returned + And no error was received + + Scenario: Request to create Snapshot with same name and different SourceVolumeID + Given a VxFlexOS service + When I call Probe + And I call CreateVolume "vol1" + And a valid CreateVolumeResponse is returned + And I call CreateSnapshot "snap1" + And no error was received + And I call CreateVolume "A Different Volume" + And a valid CreateVolumeResponse is returned + And I induce error "WrongVolIDError" + And I call CreateSnapshot "snap1" + Then the error contains "Failed to create snapshot" + + Scenario: Snapshot a single block volume but receive error + Given a VxFlexOS service + When I call Probe + And I induce error "CreateSnapshotError" + And I call CreateVolume "vol1" + And a valid CreateVolumeResponse is returned + And I call CreateSnapshot "" + Then the error contains "snapshot name cannot be Nil" + + Scenario: Call snapshot create with invalid volume + Given a VxFlexOS service + And an invalid volume + When I call Probe + And I call CreateSnapshot "snap1" + Then the error contains "volume not found" + + Scenario: Call snapshot create with no volume + Given a VxFlexOS service + And no volume + When I call Probe + And I call CreateSnapshot "snap1" + Then the error contains "volume ID to be snapped is required" + + Scenario: Call snapshot with no probe + Given a VxFlexOS service + And an invalid volume + When I invalidate the Probe cache + And I call CreateSnapshot "snap1" + Then the error contains "Controller Service has not been probed" + + Scenario: Snapshot a block volume consistency group + Given a VxFlexOS service + When I call Probe + And I call CreateVolume "vol1" + And a valid CreateVolumeResponse is returned + And I call CreateVolume "vol2" + And a valid CreateVolumeResponse is returned + And I call CreateVolume "vol3" + And a valid CreateVolumeResponse is returned + And I call CreateSnapshot "snap1" + Then a valid CreateSnapshotResponse is returned + + Scenario: Delete a snapshot + Given a VxFlexOS service + And a valid snapshot + When I call Probe + And I call DeleteSnapshot + Then no error was received + + Scenario: Idempotent delete a snapshot + Given a VxFlexOS service + And a valid snapshot + When I call Probe + And I call DeleteSnapshot + Then no error was received + And I call DeleteSnapshot + Then no error was received + + Scenario: Delete a snapshot with bad Vol ID + Given a VxFlexOS service + And a valid snapshot + When I call Probe + And I induce error "BadVolIDError" + And I call DeleteSnapshot + Then no error was received + + Scenario: Delete a snapshot with no probe + Given a VxFlexOS service + And a valid snapshot + When I invalidate the Probe cache + And I call DeleteSnapshot + Then the error contains "Controller Service has not been probed" + + Scenario: Delete a snapshot with invalid volume + Given a VxFlexOS service + And an invalid volume + When I call Probe + And I call DeleteSnapshot + Then the error contains "volume not found" + + Scenario: Delete a snapshot with no volume + Given a VxFlexOS service + And no volume + When I call Probe + And I call DeleteSnapshot + Then the error contains "snapshot ID to be deleted is required" + + Scenario: Delete snapshot that is mapped to an SDC + Given a VxFlexOS service + And a valid snapshot + And the volume is already mapped to an SDC + When I call Probe + And I call DeleteSnapshot + Then the error contains "snapshot is in use by the following SDC" + + Scenario: Delete snapshot with induced remove volume error + Given a VxFlexOS service + And a valid snapshot + And I induce error "RemoveVolumeError" + When I call Probe + And I call DeleteSnapshot + Then the error contains "error removing snapshot" + + Scenario: Delete snapshot consistency group + Given a VxFlexOS service + And a valid snapshot consistency group + When I call Probe + And I call DeleteSnapshot + Then no error was received + And I call DeleteSnapshot + Then no error was received + + Scenario: Delete snapshot consistency group with mapped volumes + Given a VxFlexOS service + And a valid snapshot consistency group + When I call Probe + And I call PublishVolume with "single-writer" + And a valid PublishVolumeResponse is returned + And I call DeleteSnapshot + Then the error contains "One or more consistency group volumes are exposed and may be in use" + + Scenario: Delete snapshot consistency with induced remove volume error + Given a VxFlexOS service + And a valid snapshot consistency group + And I induce error "RemoveVolumeError" + When I call Probe + And I call DeleteSnapshot + Then the error contains "error removing snapshot" + + Scenario: Create a volume from a snapshot + Given a VxFlexOS service + And a valid snapshot + When I call Probe + And I call Create Volume from Snapshot + Then a valid CreateVolumeResponse is returned + And no error was received + + Scenario: Create a volume from a snapshot with wrong capacity + Given a VxFlexOS service + And a valid snapshot + And the wrong capacity + When I call Probe + And I call Create Volume from Snapshot + Then the error contains "incompatible size" + + Scenario: Create a volume from a snapshot with wrong storage pool + Given a VxFlexOS service + And a valid snapshot + And the wrong storage pool + When I call Probe + And I call Create Volume from Snapshot + Then the error contains "different than the requested storage pool" + + Scenario: Create a volume from a snapshot with induced volume not found + Given a VxFlexOS service + And a valid snapshot + And I induce error "GetVolByIDError" + When I call Probe + And I call Create Volume from Snapshot + Then the error contains "Snapshot not found" + + Scenario: Create a volume from a snapshot with induced create snapshot error + Given a VxFlexOS service + And a valid snapshot + And I induce error "CreateSnapshotError" + When I call Probe + And I call Create Volume from Snapshot + Then the error contains "Failed to create snapshot" + + + Scenario: Idempotent create a volume from a snapshot + Given a VxFlexOS service + And a valid snapshot + When I call Probe + And I call Create Volume from Snapshot + And a valid CreateVolumeResponse is returned + And no error was received + Then I call Create Volume from Snapshot + And no error was received + And a valid CreateVolumeResponse is returned + + + Scenario Outline: Call ControllerExpandVolume + Given a VxFlexOS service + And I call Probe + And I call CreateVolumeSize "volume10" "32" + And a valid CreateVolumeResponse is returned + And I induce error + Then I call ControllerExpandVolume set to + And the error contains + And I call ControllerExpandVolume set to + Then the error contains + + Examples: + | error | GB | errmsg | + | "none" | 32 | "none" | + | "SetVolumeSizeError" | 64 | "induced error" | + | "none" | 16 | "none" | + | "NoVolumeIDError" | 64 | "Volume ID is required" | + | "none" | 64 | "none" | + | "GetVolByIDError" | 64 | "induced error" | + + Scenario Outline: Call NodeExpandVolume + Given a VxFlexOS service + And I call Probe + And I call CreateVolumeSize "volume4" "32" + And a controller published volume + And a capability with voltype "mount" access "single-writer" fstype "xfs" + And get Node Publish Volume Request + And I call NodePublishVolume "SDC_GUID" + And no error was received + And I induce error + When I call NodeExpandVolume with volumePath as + Then the error contains + + Examples: + | error | volPath | errormsg | + | "none" | "" | "Volume path required" | + | "none" | "test/tmp/datadir" | "none" | + | "GOFSInduceFSTypeError" | "test/tmp/datadir" | "Failed to fetch filesystem" | + | "GOFSInduceResizeFSError" | "test/tmp/datadir" | "Failed to resize device" | + | "NoVolumeIDError" | "test/tmp/datadir" | "Volume ID is required" | + | "none" | "not/a/path/1234" | "Could not stat volume path" | + | "none" | "test/tmp/datafile" | "none" | + + Scenario: Call NodeGetVolumeStats, should get unimplemented + Given a VxFlexOS service + When I call NodeGetVolumeStats + Then the error contains "Unimplemented" + + Scenario: Call New in service, a new service should return + Given a VxFlexOS service + When I call NewService + Then a new service is returned + + Scenario: Call getVolProvisionType with bad params + Given a VxFlexOS service + When I call getVolProvisionType with bad params + Then the error contains "getVolProvisionType - invalid boolean received" + + Scenario: Call getstoragepool with wrong ID + Given a VxFlexOS service + When i Call getStoragePoolnameByID "123" + Then the error contains "cannot find storage pool" + + Scenario: Test BeforeServe + Given a VxFlexOS service + And I invalidate the Probe cache + When I call BeforeServe + # Get different error message on Windows vs. Linux + Then the error contains "Unable to initialize cert pool from system@@unable to login to VxFlexOS Gateway@@unable to get SDC GUID" + + Scenario: Call Node getAllSystems + Given a VxFlexOS service + And I do not have a gateway connection + When I Call nodeGetAllSystems + + Scenario: Call Node getAllSystems + Given a VxFlexOS service + And I do not have a gateway connection + And I do not have a valid gateway endpoint + When I Call nodeGetAllSystems + Then the error contains "Unable to create ScaleIO client" + + Scenario: Call Node getAllSystems + Given a VxFlexOS service + And I do not have a gateway connection + And I do not have a valid gateway password + When I Call nodeGetAllSystems + Then the error contains "Unable to create ScaleIO client" + + Scenario: Call evalsymlinks + Given a VxFlexOS service + When I call evalsymlink "invalidpath" + Then the error contains "Could not evaluate symlinks for path" + + Scenario: Clone a volume + Given a VxFlexOS service + And a valid volume + When I call Probe + And I call Clone volume + Then a valid CreateVolumeResponse is returned + And no error was received + + Scenario: Clone a volume with wrong capacity + Given a VxFlexOS service + And a valid volume + And the wrong capacity + When I call Probe + And I call Clone volume + Then the error contains "incompatible size" + + Scenario: Clone a volume with wrong storage pool + Given a VxFlexOS service + And a valid volume + And the wrong storage pool + When I call Probe + And I call Clone volume + Then the error contains "different from the requested storage pool" + + Scenario: Clone a volume with invalid volume + Given a VxFlexOS service + And an invalid volume + When I call Probe + And I call Clone volume + Then the error contains "Volume not found" + + Scenario: Clone a volume with induced volume not found + Given a VxFlexOS service + And a valid volume + And I induce error "CreateSnapshotError" + When I call Probe + And I call Clone volume + Then the error contains "Failed to call CreateSnapshotConsistencyGroup to clone volume" diff --git a/service/identity.go b/service/identity.go index f97613a8..431651e3 100644 --- a/service/identity.go +++ b/service/identity.go @@ -40,6 +40,13 @@ func (s *service) GetPluginCapabilities( }, }, }, + { + Type: &csi.PluginCapability_Service_{ + Service: &csi.PluginCapability_Service{ + Type: csi.PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS, + }, + }, + }, { Type: &csi.PluginCapability_VolumeExpansion_{ VolumeExpansion: &csi.PluginCapability_VolumeExpansion{ diff --git a/service/service.go b/service/service.go index 9960cbf4..f8eae8e4 100644 --- a/service/service.go +++ b/service/service.go @@ -323,6 +323,7 @@ func (s *service) getCSISnapshot(vol *siotypes.Volume) *csi.Snapshot { SizeBytes: int64(vol.SizeInKb) * bytesInKiB, SnapshotId: vol.ID, SourceVolumeId: vol.AncestorVolumeID, + ReadyToUse: true, } // Convert array timestamp to CSI timestamp and add csiTimestamp, err := ptypes.TimestampProto(time.Unix(int64(vol.CreationTime), 0)) diff --git a/service/service_test.go b/service/service_test.go index b020a961..5080e424 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -21,7 +21,7 @@ func TestMain(m *testing.M) { }, godog.Options{ Format: "pretty", Paths: []string{"features"}, - //Tags: "wip", + // Tags: "wip", }) fmt.Printf("godog finished\n") diff --git a/service/step_defs_test.go b/service/step_defs_test.go index d14faf80..50535cf0 100644 --- a/service/step_defs_test.go +++ b/service/step_defs_test.go @@ -1100,11 +1100,13 @@ func (f *feature) aValidControllerGetCapabilitiesResponseIsReturned() error { count = count + 1 case csi.ControllerServiceCapability_RPC_EXPAND_VOLUME: count = count + 1 + case csi.ControllerServiceCapability_RPC_CLONE_VOLUME: + count = count + 1 default: return fmt.Errorf("received unexpected capability: %v", typex) } } - if count != 7 { + if count != 8 { return errors.New("Did not retrieve all the expected capabilities") } return nil @@ -1114,6 +1116,30 @@ func (f *feature) aValidControllerGetCapabilitiesResponseIsReturned() error { } +func (f *feature) iCallCloneVolume() error { + ctx := new(context.Context) + req := getTypicalCreateVolumeRequest() + req.Name = "clone" + + if f.wrongCapacity { + req.CapacityRange.RequiredBytes = 64 * 1024 * 1024 * 1024 + } + + if f.wrongStoragePool { + req.Parameters["storagepool"] = "bad storage pool" + } + + source := &csi.VolumeContentSource_VolumeSource{VolumeId: goodVolumeID} + req.VolumeContentSource = new(csi.VolumeContentSource) + req.VolumeContentSource.Type = &csi.VolumeContentSource_Volume{Volume: source} + f.createVolumeResponse, f.err = f.service.CreateVolume(*ctx, req) + if f.err != nil { + fmt.Printf("Error on CreateVolume from volume: %s\n", f.err.Error()) + } + + return nil +} + func (f *feature) iCallValidateVolumeCapabilitiesWithVoltypeAccessFstype(voltype, access, fstype string) error { ctx := new(context.Context) req := new(csi.ValidateVolumeCapabilitiesRequest) @@ -1957,4 +1983,5 @@ func FeatureContext(s *godog.Suite) { s.Step(`^I do not have a gateway connection$`, f.iDoNotHaveAGatewayConnection) s.Step(`^I do not have a valid gateway endpoint$`, f.iDoNotHaveAValidGatewayEndpoint) s.Step(`^I do not have a valid gateway password$`, f.iDoNotHaveAValidGatewayPassword) + s.Step(`^I call Clone volume$`, f.iCallCloneVolume) } diff --git a/test/helm/2vols+clone/Chart.yaml b/test/helm/2vols+clone/Chart.yaml new file mode 100644 index 00000000..7eb9b0b5 --- /dev/null +++ b/test/helm/2vols+clone/Chart.yaml @@ -0,0 +1,9 @@ +name: 2vols+clone +version: 1.0.0 +apiVersion: v1 +description: | + Tests VxFlexOS CSI deployments. +keywords: +- vxflexos-csi +- storage +engine: gotpl diff --git a/test/helm/2vols+clone/templates/createFromVolume.yaml b/test/helm/2vols+clone/templates/createFromVolume.yaml new file mode 100644 index 00000000..37025933 --- /dev/null +++ b/test/helm/2vols+clone/templates/createFromVolume.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: clonedpvc + namespace: helmtest-vxflexos +spec: + storageClassName: vxflexos + dataSource: + name: pvol0 + kind: PersistentVolumeClaim + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi diff --git a/test/helm/2vols+clone/templates/pvc0.yaml b/test/helm/2vols+clone/templates/pvc0.yaml new file mode 100644 index 00000000..4e7bdc58 --- /dev/null +++ b/test/helm/2vols+clone/templates/pvc0.yaml @@ -0,0 +1,13 @@ +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: pvol0 + namespace: helmtest-vxflexos +spec: + accessModes: + - ReadWriteOnce + volumeMode: Filesystem + resources: + requests: + storage: 8Gi + storageClassName: vxflexos diff --git a/test/helm/2vols+clone/templates/pvc1.yaml b/test/helm/2vols+clone/templates/pvc1.yaml new file mode 100644 index 00000000..8c2daa2f --- /dev/null +++ b/test/helm/2vols+clone/templates/pvc1.yaml @@ -0,0 +1,13 @@ +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: pvol1 + namespace: helmtest-vxflexos +spec: + accessModes: + - ReadWriteOnce + volumeMode: Filesystem + resources: + requests: + storage: 12Gi + storageClassName: vxflexos-xfs diff --git a/test/helm/2vols+clone/templates/test.yaml b/test/helm/2vols+clone/templates/test.yaml new file mode 100644 index 00000000..bc630239 --- /dev/null +++ b/test/helm/2vols+clone/templates/test.yaml @@ -0,0 +1,43 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: vxflextest + namespace: helmtest-vxflexos +--- +kind: StatefulSet +apiVersion: apps/v1 +metadata: + name: vxflextest + namespace: helmtest-vxflexos +spec: + selector: + matchLabels: + app: vxflextest + serviceName: 2vols + template: + metadata: + labels: + app: vxflextest + spec: + serviceAccount: vxflextest + containers: + - name: test + image: docker.io/centos:latest + command: [ "/bin/sleep", "3600" ] + volumeMounts: + - mountPath: "/data0" + name: pvol0 + - mountPath: "/data1" + name: pvol1 + - mountPath: "/data2" + name: pvol2 + volumes: + - name: pvol0 + persistentVolumeClaim: + claimName: pvol0 + - name: pvol1 + persistentVolumeClaim: + claimName: pvol1 + - name: pvol2 + persistentVolumeClaim: + claimName: clonedpvc diff --git a/test/helm/2vols/Chart.yaml b/test/helm/2vols/Chart.yaml index 018cbf64..ad1528c7 100644 --- a/test/helm/2vols/Chart.yaml +++ b/test/helm/2vols/Chart.yaml @@ -1,7 +1,6 @@ name: 2vols version: 1.0.0 apiVersion: v1 -appVersion: 1.0.0 description: | Tests VxFlexOS CSI deployments. icon: https://avatars1.githubusercontent.com/u/20958494?s=200&v=4 diff --git a/test/helm/postgres.sh b/test/helm/postgres.sh deleted file mode 100755 index c3b4fd17..00000000 --- a/test/helm/postgres.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -DBSQL=/root/dbsamples-0.1/world/world.sql -helm install -n postgres postgres -echo "Waiting for pods to come up..." -up=0 -while [ $up -lt 2 ]; -do - sleep 10 - kubectl get pods - up=`kubectl get pods | grep '1/1 Running' | wc -l` -done -kubectl describe svc postgres-postgresql - -sleep 20 -#echo "Logging into container..." -#kubectl exec -it postgres-postgresql-master-0 /bin/sh - -echo "Set up port forwarding..." -kubectl port-forward --namespace default svc/postgres-postgresql 5432:5432 & -forwardpid=$! -echo $forwardpid -# Don't remove this sleep; setting up port-forwarding is async. and takes time to complete -sleep 5 - -echo "Initializing database..." -PGPASSWORD="dangerous" psql --host 127.0.0.1 -U postgres < $DBSQL - -echo "Querying database..." -PGPASSWORD="dangerous" psql --host 127.0.0.1 -U postgres < **Tip**: List all releases using `helm list` - -## Uninstalling the Chart - -To uninstall/delete the `my-release` deployment: - -```console -$ helm delete my-release -``` - -The command removes all the Kubernetes components associated with the chart and deletes the release. - -## Configuration - -The following tables lists the configurable parameters of the PostgreSQL chart and their default values. - -| Parameter | Description | Default | -|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| -| `global.imageRegistry` | Global Docker Image registry | `nil` | -| `image.registry` | PostgreSQL Image registry | `docker.io` | -| `image.repository` | PostgreSQL Image name | `bitnami/postgresql` | -| `image.tag` | PostgreSQL Image tag | `{VERSION}` | -| `image.pullPolicy` | PostgreSQL Image pull policy | `Always` | -| `image.pullSecrets` | Specify Image pull secrets | `nil` (does not add image pull secrets to deployed pods) | -| `image.debug` | Specify if debug values should be set | `false` | -| `volumePermissions.image.registry` | Init container volume-permissions image registry | `docker.io` | -| `volumePermissions.image.repository` | Init container volume-permissions image name | `bitnami/minideb` | -| `volumePermissions.image.tag` | Init container volume-permissions image tag | `latest` | -| `volumePermissions.image.pullPolicy` | Init container volume-permissions image pull policy | `Always` | -| `volumePermissions.securityContext.runAsUser` | User ID for the init container | `0` | -| `usePasswordFile` | Have the secrets mounted as a file instead of env vars | `false` | -| `replication.enabled` | Would you like to enable replication | `false` | -| `replication.user` | Replication user | `repl_user` | -| `replication.password` | Replication user password | `repl_password` | -| `replication.slaveReplicas` | Number of slaves replicas | `1` | -| `replication.synchronousCommit` | Set synchronous commit mode. Allowed values: `on`, `remote_apply`, `remote_write`, `local` and `off` | `off` | -| `replication.numSynchronousReplicas` | Number of replicas that will have synchronous replication. Note: Cannot be greater than `replication.slaveReplicas`. | `0` | -| `replication.applicationName` | Cluster application name. Useful for advanced replication settings | `my_application` | -| `existingSecret` | Name of existing secret to use for PostgreSQL passwords | `nil` | -| `postgresqlUsername` | PostgreSQL admin user | `postgres` | -| `postgresqlPassword` | PostgreSQL admin password | _random 10 character alphanumeric string_ | -| `postgresqlDatabase` | PostgreSQL database | `nil` | -| `postgresqlConfiguration` | Runtime Config Parameters | `nil` | -| `postgresqlExtendedConf` | Extended Runtime Config Parameters (appended to main or default configuration) | `nil` | -| `pgHbaConfiguration` | Content of pg\_hba.conf | `nil (do not create pg_hba.conf)` | -| `configurationConfigMap` | ConfigMap with the PostgreSQL configuration files (Note: Overrides `postgresqlConfiguration` and `pgHbaConfiguration`) | `nil` | -| `extendedConfConfigMap` | ConfigMap with the extended PostgreSQL configuration files | `nil` | -| `initdbScripts` | List of initdb scripts | `nil` | -| `initdbScriptsConfigMap` | ConfigMap with the initdb scripts (Note: Overrides `initdbScripts`) | `nil` | -| `service.type` | Kubernetes Service type | `ClusterIP` | -| `service.port` | PostgreSQL port | `5432` | -| `service.nodePort` | Kubernetes Service nodePort | `nil` | -| `service.annotations` | Annotations for PostgreSQL service | {} | -| `service.loadBalancerIP` | loadBalancerIP if service type is `LoadBalancer` | `nil` | -| `persistence.enabled` | Enable persistence using PVC | `true` | -| `persistence.existingClaim` | Provide an existing `PersistentVolumeClaim` | `nil` | -| `persistence.mountPath` | Path to mount the volume at | `/bitnami/postgresql` | -| `persistence.storageClass` | PVC Storage Class for PostgreSQL volume | `nil` | -| `persistence.accessMode` | PVC Access Mode for PostgreSQL volume | `ReadWriteOnce` | -| `persistence.size` | PVC Storage Request for PostgreSQL volume | `8Gi` | -| `persistence.annotations` | Annotations for the PVC | `{}` | -| `master.nodeSelector` | Node labels for pod assignment (postgresql master) | `{}` | -| `master.affinity` | Affinity labels for pod assignment (postgresql master) | `{}` | -| `master.tolerations` | Toleration labels for pod assignment (postgresql master) | `[]` | -| `slave.nodeSelector` | Node labels for pod assignment (postgresql slave) | `{}` | -| `slave.affinity` | Affinity labels for pod assignment (postgresql slave) | `{}` | -| `slave.tolerations` | Toleration labels for pod assignment (postgresql slave) | `[]` | -| `terminationGracePeriodSeconds` | Seconds the pod needs to terminate gracefully | `nil` | -| `resources` | CPU/Memory resource requests/limits | Memory: `256Mi`, CPU: `250m` | -| `securityContext.enabled` | Enable security context | `true` | -| `securityContext.fsGroup` | Group ID for the container | `1001` | -| `securityContext.runAsUser` | User ID for the container | `1001` | -| `livenessProbe.enabled` | Would you like a livessProbed to be enabled | `true` | -| `networkPolicy.enabled` | Enable NetworkPolicy | `false` | -| `networkPolicy.allowExternal` | Don't require client label for connections | `true` | -| `livenessProbe.initialDelaySeconds` | Delay before liveness probe is initiated | 30 | -| `livenessProbe.periodSeconds` | How often to perform the probe | 10 | -| `livenessProbe.timeoutSeconds` | When the probe times out | 5 | -| `livenessProbe.failureThreshold` | Minimum consecutive failures for the probe to be considered failed after having succeeded. | 6 | -| `livenessProbe.successThreshold` | Minimum consecutive successes for the probe to be considered successful after having failed | 1 | -| `readinessProbe.enabled` | would you like a readinessProbe to be enabled | `true` | -| `readinessProbe.initialDelaySeconds` | Delay before liveness probe is initiated | 5 | -| `readinessProbe.periodSeconds` | How often to perform the probe | 10 | -| `readinessProbe.timeoutSeconds` | When the probe times out | 5 | -| `readinessProbe.failureThreshold` | Minimum consecutive failures for the probe to be considered failed after having succeeded. | 6 | -| `readinessProbe.successThreshold` | Minimum consecutive successes for the probe to be considered successful after having failed | 1 | -| `metrics.enabled` | Start a prometheus exporter | `false` | -| `metrics.service.type` | Kubernetes Service type | `ClusterIP` | -| `service.clusterIP` | Static clusterIP or None for headless services | `nil` | -| `metrics.service.annotations` | Additional annotations for metrics exporter pod | `{}` | -| `metrics.service.loadBalancerIP` | loadBalancerIP if redis metrics service type is `LoadBalancer` | `nil` | -| `metrics.image.registry` | PostgreSQL Image registry | `docker.io` | -| `metrics.image.repository` | PostgreSQL Image name | `wrouesnel/postgres_exporter` | -| `metrics.image.tag` | PostgreSQL Image tag | `{VERSION}` | -| `metrics.image.pullPolicy` | PostgreSQL Image pull policy | `IfNotPresent` | -| `metrics.image.pullSecrets` | Specify Image pull secrets | `nil` (does not add image pull secrets to deployed pods) | -| `extraEnv` | Any extra environment variables you would like to pass on to the pod | `{}` | -| `updateStrategy` | Update strategy policy | `{type: "onDelete"}` | - -Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, - -```console -$ helm install --name my-release \ - --set postgresqlPassword=secretpassword,postgresqlDatabase=my-database \ - stable/postgresql -``` - -The above command sets the PostgreSQL `postgres` account password to `secretpassword`. Additionally it creates a database named `my-database`. - -Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, - -```console -$ helm install --name my-release -f values.yaml stable/postgresql -``` - -> **Tip**: You can use the default [values.yaml](values.yaml) - -### postgresql.conf / pg_hba.conf files as configMap - -This helm chart also supports to customize the whole configuration file. - -Add your custom file to "files/postgresql.conf" in your working directory. This file will be mounted as configMap to the containers and it will be used for configuring the PostgreSQL server. - -Alternatively, you can specify PostgreSQL configuration parameters using the `postgresqlConfiguration` parameter as a dict, using camelCase, e.g. {"sharedBuffers": "500MB"}. - -In addition to these options, you can also set an external ConfigMap with all the configuration files. This is done by setting the `configurationConfigMap` parameter. Note that this will override the two previous options. - -### Allow settings to be loaded from files other than the default `postgresql.conf` - -If you don't want to provide the whole PostgreSQL configuration file and only specify certain parameters, you can add your extended `.conf` files to "files/conf.d/" in your working directory. -Those files will be mounted as configMap to the containers adding/overwriting the default configuration using the `include_dir` directive that allows settings to be loaded from files other than the default `postgresql.conf`. - -Alternatively, you can also set an external ConfigMap with all the extra configuration files. This is done by setting the `extendedConfConfigMap` parameter. Note that this will override the previous option. - -## Initialize a fresh instance - -The [Bitnami PostgreSQL](https://github.com/bitnami/bitnami-docker-postgresql) image allows you to use your custom scripts to initialize a fresh instance. In order to execute the scripts, they must be located inside the chart folder `files/docker-entrypoint-initdb.d` so they can be consumed as a ConfigMap. - -Alternatively, you can specify custom scripts using the `initdbScripts` parameter as dict. - -In addition to these options, you can also set an external ConfigMap with all the initialization scripts. This is done by setting the `initdbScriptsConfigMap` parameter. Note that this will override the two previous options. - -The allowed extensions are `.sh`, `.sql` and `.sql.gz`. - -## Production and horizontal scaling - -The following repo contains the recommended production settings for PostgreSQL server in an alternative [values file](values-production.yaml). Please read carefully the comments in the values-production.yaml file to set up your environment - -To horizontally scale this chart, first download the [values-production.yaml](values-production.yaml) file to your local folder, then: - -```console -$ helm install --name my-release -f ./values-production.yaml stable/postgresql -$ kubectl scale statefulset my-postgresql-slave --replicas=3 -``` - -## Persistence - -The [Bitnami PostgreSQL](https://github.com/bitnami/bitnami-docker-postgresql) image stores the PostgreSQL data and configurations at the `/bitnami/postgresql` path of the container. - -Persistent Volume Claims are used to keep the data across deployments. This is known to work in GCE, AWS, and minikube. -See the [Configuration](#configuration) section to configure the PVC or to disable persistence. - -## Metrics - -The chart optionally can start a metrics exporter for [prometheus](https://prometheus.io). The metrics endpoint (port 9187) is not exposed and it is expected that the metrics are collected from inside the k8s cluster using something similar as the described in the [example Prometheus scrape configuration](https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml). - -The exporter allows to create custom metrics from additional SQL queries. See the Chart's `values.yaml` for an example and consult the [exporters documentation](https://github.com/wrouesnel/postgres_exporter#adding-new-metrics-via-a-config-file) for more details. - -## NetworkPolicy - -To enable network policy for PostgreSQL, install [a networking plugin that implements the Kubernetes NetworkPolicy spec](https://kubernetes.io/docs/tasks/administer-cluster/declare-network-policy#before-you-begin), and set `networkPolicy.enabled` to `true`. - -For Kubernetes v1.5 & v1.6, you must also turn on NetworkPolicy by setting the DefaultDeny namespace annotation. Note: this will enforce policy for _all_ pods in the namespace: - -```console -$ kubectl annotate namespace default "net.beta.kubernetes.io/network-policy={\"ingress\":{\"isolation\":\"DefaultDeny\"}}" -``` - -With NetworkPolicy enabled, traffic will be limited to just port 5432. - -For more precise policy, set `networkPolicy.allowExternal=false`. This will only allow pods with the generated client label to connect to PostgreSQL. -This label will be displayed in the output of a successful install. - -## Upgrade - -### 3.0.0 - -This releases make it possible to specify different nodeSelector, affinity and tolerations for master and slave pods. -It also fixes an issue with `postgresql.master.fullname` helper template not obeying fullnameOverride. - -#### Breaking changes - -- `affinty` has been renamed to `master.affinity` and `slave.affinity`. -- `tolerations` has been renamed to `master.tolerations` and `slave.tolerations`. -- `nodeSelector` has been renamed to `master.nodeSelector` and `slave.nodeSelector`. - -### 2.0.0 - -In order to upgrade from the `0.X.X` branch to `1.X.X`, you should follow the below steps: - - - Obtain the service name (`SERVICE_NAME`) and password (`OLD_PASSWORD`) of the existing postgresql chart. You can find the instructions to obtain the password in the NOTES.txt, the service name can be obtained by running - - ```console -$ kubectl get svc - ``` - -- Install (not upgrade) the new version - -```console -$ helm repo update -$ helm install --name my-release stable/postgresql -``` - -- Connect to the new pod (you can obtain the name by running `kubectl get pods`): - -```console -$ kubectl exec -it NAME bash -``` - -- Once logged in, create a dump file from the previous database using `pg_dump`, for that we should connect to the previous postgresql chart: - -```console -$ pg_dump -h SERVICE_NAME -U postgres DATABASE_NAME > /tmp/backup.sql -``` - -After run above command you should be prompted for a password, this password is the previous chart password (`OLD_PASSWORD`). -This operation could take some time depending on the database size. - -- Once you have the backup file, you can restore it with a command like the one below: - -```console -$ psql -U postgres DATABASE_NAME < /tmp/backup.sql -``` - -In this case, you are accessing to the local postgresql, so the password should be the new one (you can find it in NOTES.txt). - -If you want to restore the database and the database schema does not exist, it is necessary to first follow the steps described below. - -```console -$ psql -U postgres -postgres=# drop database DATABASE_NAME; -postgres=# create database DATABASE_NAME; -postgres=# create user USER_NAME; -postgres=# alter role USER_NAME with password 'BITNAMI_USER_PASSWORD'; -postgres=# grant all privileges on database DATABASE_NAME to USER_NAME; -postgres=# alter database DATABASE_NAME owner to USER_NAME; -``` diff --git a/test/helm/postgres/files/README.md b/test/helm/postgres/files/README.md deleted file mode 100644 index 1813a2fe..00000000 --- a/test/helm/postgres/files/README.md +++ /dev/null @@ -1 +0,0 @@ -Copy here your postgresql.conf and/or pg_hba.conf files to use it as a config map. diff --git a/test/helm/postgres/files/conf.d/README.md b/test/helm/postgres/files/conf.d/README.md deleted file mode 100644 index 184c1875..00000000 --- a/test/helm/postgres/files/conf.d/README.md +++ /dev/null @@ -1,4 +0,0 @@ -If you don't want to provide the whole configuration file and only specify certain parameters, you can copy here your extended `.conf` files. -These files will be injected as a config maps and add/overwrite the default configuration using the `include_dir` directive that allows settings to be loaded from files other than the default `postgresql.conf`. - -More info in the [bitnami-docker-postgresql README](https://github.com/bitnami/bitnami-docker-postgresql#configuration-file). diff --git a/test/helm/postgres/files/docker-entrypoint-initdb.d/README.md b/test/helm/postgres/files/docker-entrypoint-initdb.d/README.md deleted file mode 100644 index cba38091..00000000 --- a/test/helm/postgres/files/docker-entrypoint-initdb.d/README.md +++ /dev/null @@ -1,3 +0,0 @@ -You can copy here your custom `.sh`, `.sql` or `.sql.gz` file so they are executed during the first boot of the image. - -More info in the [bitnami-docker-postgresql](https://github.com/bitnami/bitnami-docker-postgresql#initializing-a-new-instance) repository. \ No newline at end of file diff --git a/test/helm/postgres/templates/NOTES.txt b/test/helm/postgres/templates/NOTES.txt deleted file mode 100644 index 41c22104..00000000 --- a/test/helm/postgres/templates/NOTES.txt +++ /dev/null @@ -1,60 +0,0 @@ -{{- if contains .Values.service.type "LoadBalancer" }} -{{- if not .Values.postgresqlPassword }} -------------------------------------------------------------------------------- - WARNING - - By specifying "serviceType=LoadBalancer" and not specifying "postgresqlPassword" - you have most likely exposed the PostgreSQL service externally without any - authentication mechanism. - - For security reasons, we strongly suggest that you switch to "ClusterIP" or - "NodePort". As an alternative, you can also specify a valid password on the - "postgresqlPassword" parameter. - -------------------------------------------------------------------------------- -{{- end }} -{{- end }} - -** Please be patient while the chart is being deployed ** - -PostgreSQL can be accessed via port 5432 on the following DNS name from within your cluster: - - {{ template "postgresql.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local - Read/Write connection -{{- if .Values.replication.enabled }} - {{ template "postgresql.fullname" . }}-read.{{ .Release.Namespace }}.svc.cluster.local - Read only connection -{{- end }} -To get the password for "{{ .Values.postgresqlUsername }}" run: - - export POSTGRESQL_PASSWORD=$(kubectl get secret --namespace {{ .Release.Namespace }} {{ if .Values.existingSecret }}{{ .Values.existingSecret }}{{ else }}{{ template "postgresql.fullname" . }}{{ end }} -o jsonpath="{.data.postgresql-password}" | base64 --decode) - -To connect to your database run the following command: - - kubectl run {{ template "postgresql.fullname" . }}-client --rm --tty -i --restart='Never' --namespace {{ .Release.Namespace }} --image bitnami/postgresql --env="PGPASSWORD=$POSTGRESQL_PASSWORD" {{- if and (.Values.networkPolicy.enabled) (not .Values.networkPolicy.allowExternal) }} - --labels="{{ template "postgresql.fullname" . }}-client=true" {{- end }} --command -- psql --host {{ template "postgresql.fullname" . }} -U {{ .Values.postgresqlUsername }} - -{{ if and (.Values.networkPolicy.enabled) (not .Values.networkPolicy.allowExternal) }} -Note: Since NetworkPolicy is enabled, only pods with label {{ template "postgresql.fullname" . }}-client=true" will be able to connect to this PostgreSQL cluster. -{{- end }} - -To connect to your database from outside the cluster execute the following commands: - -{{- if contains "NodePort" .Values.service.type }} - - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "postgresql.fullname" . }}) - {{ if .Values.postgresqlPassword }}PGPASSWORD="{{ .Values.postgresqlPassword}}" {{ end }}psql --host $NODE_IP --port $NODE_PORT -U {{ .Values.postgresqlUsername }} - -{{- else if contains "LoadBalancer" .Values.service.type }} - - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - Watch the status with: 'kubectl get svc --namespace {{ .Release.Namespace }} -w {{ template "postgresql.fullname" . }}' - - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "postgresql.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - {{ if .Values.postgresqlPassword }}PGPASSWORD="{{ .Values.postgresqlPassword}}" {{ end }}psql --host $SERVICE_IP --port {{ .Values.service.port }} -U {{ .Values.postgresqlUsername }} - -{{- else if contains "ClusterIP" .Values.service.type }} - - kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ template "postgresql.fullname" . }} 5432:5432 & - {{ if .Values.postgresqlPassword }}PGPASSWORD="{{ .Values.postgresqlPassword}}" {{ end }}psql --host 127.0.0.1 -U {{ .Values.postgresqlUsername }} - -{{- end }} diff --git a/test/helm/postgres/templates/_helpers.tpl b/test/helm/postgres/templates/_helpers.tpl deleted file mode 100644 index d1797796..00000000 --- a/test/helm/postgres/templates/_helpers.tpl +++ /dev/null @@ -1,152 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "postgresql.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -*/}} -{{- define "postgresql.fullname" -}} -{{- if .Values.fullnameOverride -}} -{{- printf .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -*/}} -{{- define "postgresql.master.fullname" -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- $fullname := default (printf "%s-%s" .Release.Name $name) .Values.fullnameOverride -}} -{{- if .Values.replication.enabled -}} -{{- printf "%s-%s" $fullname "master" | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- printf "%s" $fullname | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} - -{{/* -Return the appropriate apiVersion for networkpolicy. -*/}} -{{- define "postgresql.networkPolicy.apiVersion" -}} -{{- if semverCompare ">=1.4-0, <1.7-0" .Capabilities.KubeVersion.GitVersion -}} -"extensions/v1beta1" -{{- else if semverCompare "^1.7-0" .Capabilities.KubeVersion.GitVersion -}} -"networking.k8s.io/v1" -{{- end -}} -{{- end -}} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "postgresql.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Return the proper PostgreSQL image name -*/}} -{{- define "postgresql.image" -}} -{{- $registryName := .Values.image.registry -}} -{{- $repositoryName := .Values.image.repository -}} -{{- $tag := .Values.image.tag | toString -}} -{{/* -Helm 2.11 supports the assignment of a value to a variable defined in a different scope, -but Helm 2.9 and 2.10 doesn't support it, so we need to implement this if-else logic. -Also, we can't use a single if because lazy evaluation is not an option -*/}} -{{- if .Values.global }} - {{- if .Values.global.imageRegistry }} - {{- printf "%s/%s:%s" .Values.global.imageRegistry $repositoryName $tag -}} - {{- else -}} - {{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} - {{- end -}} -{{- else -}} - {{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} -{{- end -}} -{{- end -}} - -{{/* -Return the proper image name to change the volume permissions -*/}} -{{- define "postgresql.volumePermissions.image" -}} -{{- $registryName := .Values.volumePermissions.image.registry -}} -{{- $repositoryName := .Values.volumePermissions.image.repository -}} -{{- $tag := .Values.volumePermissions.image.tag | toString -}} -{{/* -Helm 2.11 supports the assignment of a value to a variable defined in a different scope, -but Helm 2.9 and 2.10 doesn't support it, so we need to implement this if-else logic. -Also, we can't use a single if because lazy evaluation is not an option -*/}} -{{- if .Values.global }} - {{- if .Values.global.imageRegistry }} - {{- printf "%s/%s:%s" .Values.global.imageRegistry $repositoryName $tag -}} - {{- else -}} - {{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} - {{- end -}} -{{- else -}} - {{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} -{{- end -}} -{{- end -}} - - -{{/* -Return the proper PostgreSQL metrics image name -*/}} -{{- define "metrics.image" -}} -{{- $registryName := default "docker.io" .Values.metrics.image.registry -}} -{{- $tag := default "latest" .Values.metrics.image.tag | toString -}} -{{- printf "%s/%s:%s" $registryName .Values.metrics.image.repository $tag -}} -{{- end -}} - -{{/* -Get the password secret. -*/}} -{{- define "postgresql.secretName" -}} -{{- if .Values.existingSecret -}} -{{- printf "%s" .Values.existingSecret -}} -{{- else -}} -{{- printf "%s" (include "postgresql.fullname" .) -}} -{{- end -}} -{{- end -}} - -{{/* -Get the configuration ConfigMap name. -*/}} -{{- define "postgresql.configurationCM" -}} -{{- if .Values.configurationConfigMap -}} -{{- printf "%s" .Values.configurationConfigMap -}} -{{- else -}} -{{- printf "%s-configuration" (include "postgresql.fullname" .) -}} -{{- end -}} -{{- end -}} - -{{/* -Get the extended configuration ConfigMap name. -*/}} -{{- define "postgresql.extendedConfigurationCM" -}} -{{- if .Values.extendedConfConfigMap -}} -{{- printf "%s" .Values.extendedConfConfigMap -}} -{{- else -}} -{{- printf "%s-extended-configuration" (include "postgresql.fullname" .) -}} -{{- end -}} -{{- end -}} - -{{/* -Get the initialization scripts ConfigMap name. -*/}} -{{- define "postgresql.initdbScriptsCM" -}} -{{- if .Values.initdbScriptsConfigMap -}} -{{- printf "%s" .Values.initdbScriptsConfigMap -}} -{{- else -}} -{{- printf "%s-init-scripts" (include "postgresql.fullname" .) -}} -{{- end -}} -{{- end -}} diff --git a/test/helm/postgres/templates/configmap.yaml b/test/helm/postgres/templates/configmap.yaml deleted file mode 100644 index d2178c07..00000000 --- a/test/helm/postgres/templates/configmap.yaml +++ /dev/null @@ -1,26 +0,0 @@ -{{ if and (or (.Files.Glob "files/postgresql.conf") (.Files.Glob "files/pg_hba.conf") .Values.postgresqlConfiguration .Values.pgHbaConfiguration) (not .Values.configurationConfigMap) }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ template "postgresql.fullname" . }}-configuration - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} -data: -{{- if (.Files.Glob "files/postgresql.conf") }} -{{ (.Files.Glob "files/postgresql.conf").AsConfig | indent 2 }} -{{- else if .Values.postgresqlConfiguration }} - postgresql.conf: | -{{- range $key, $value := default dict .Values.postgresqlConfiguration }} - {{ $key | snakecase }}={{ $value }} -{{- end }} -{{- end }} -{{- if (.Files.Glob "files/pg_hba.conf") }} -{{ (.Files.Glob "files/pg_hba.conf").AsConfig | indent 2 }} -{{- else if .Values.pgHbaConfiguration }} - pg_hba.conf: | -{{ .Values.pgHbaConfiguration | indent 4 }} -{{- end }} -{{ end }} diff --git a/test/helm/postgres/templates/extended-config-configmap.yaml b/test/helm/postgres/templates/extended-config-configmap.yaml deleted file mode 100644 index 8a411957..00000000 --- a/test/helm/postgres/templates/extended-config-configmap.yaml +++ /dev/null @@ -1,21 +0,0 @@ -{{- if and (or (.Files.Glob "files/conf.d/*.conf") .Values.postgresqlExtendedConf) (not .Values.extendedConfConfigMap)}} -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ template "postgresql.fullname" . }}-extended-configuration - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} -data: -{{- with .Files.Glob "files/conf.d/*.conf" }} -{{ .AsConfig | indent 2 }} -{{- end }} -{{ with .Values.postgresqlExtendedConf }} - override.conf: | -{{- range $key, $value := . }} - {{ $key | snakecase }}={{ $value }} -{{- end }} -{{- end }} -{{- end }} diff --git a/test/helm/postgres/templates/initialization-configmap.yaml b/test/helm/postgres/templates/initialization-configmap.yaml deleted file mode 100644 index 8eb5e058..00000000 --- a/test/helm/postgres/templates/initialization-configmap.yaml +++ /dev/null @@ -1,24 +0,0 @@ -{{- if and (or (.Files.Glob "files/docker-entrypoint-initdb.d/*.{sh,sql,sql.gz}") .Values.initdbScripts) (not .Values.initdbScriptsConfigMap) }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ template "postgresql.fullname" . }}-init-scripts - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} -{{- with .Files.Glob "files/docker-entrypoint-initdb.d/*.sql.gz" }} -binaryData: -{{- range $path, $bytes := . }} - {{ base $path }}: {{ $.Files.Get $path | b64enc | quote }} -{{- end }} -{{- end }} -data: -{{- with .Files.Glob "files/docker-entrypoint-initdb.d/*.{sh,sql}" }} -{{ .AsConfig | indent 2 }} -{{- end }} -{{- with .Values.initdbScripts }} -{{ toYaml . | indent 2 }} -{{- end }} -{{- end }} diff --git a/test/helm/postgres/templates/metrics-svc.yaml b/test/helm/postgres/templates/metrics-svc.yaml deleted file mode 100644 index 2e210e34..00000000 --- a/test/helm/postgres/templates/metrics-svc.yaml +++ /dev/null @@ -1,26 +0,0 @@ -{{- if .Values.metrics.enabled }} -apiVersion: v1 -kind: Service -metadata: - name: {{ template "postgresql.fullname" . }}-metrics - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} - annotations: -{{ toYaml .Values.metrics.service.annotations | indent 4 }} -spec: - type: {{ .Values.metrics.service.type }} - {{- if and (eq .Values.metrics.service.type "LoadBalancer") .Values.metrics.service.loadBalancerIP }} - loadBalancerIP: {{ .Values.metrics.service.loadBalancerIP }} - {{- end }} - ports: - - name: metrics - port: 9187 - targetPort: metrics - selector: - app: {{ template "postgresql.name" . }} - release: {{ .Release.Name }} - role: master -{{- end }} diff --git a/test/helm/postgres/templates/networkpolicy.yaml b/test/helm/postgres/templates/networkpolicy.yaml deleted file mode 100644 index 40496a76..00000000 --- a/test/helm/postgres/templates/networkpolicy.yaml +++ /dev/null @@ -1,29 +0,0 @@ -{{- if .Values.networkPolicy.enabled }} -kind: NetworkPolicy -apiVersion: {{ template "postgresql.networkPolicy.apiVersion" . }} -metadata: - name: {{ template "postgresql.fullname" . }} - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} -spec: - podSelector: - matchLabels: - app: {{ template "postgresql.name" . }} - release: {{ .Release.Name | quote }} - ingress: - # Allow inbound connections - - ports: - - port: 5432 - {{- if not .Values.networkPolicy.allowExternal }} - from: - - podSelector: - matchLabels: - {{ template "postgresql.fullname" . }}-client: "true" - {{- end }} - # Allow prometheus scrapes - - ports: - - port: 9187 -{{- end }} diff --git a/test/helm/postgres/templates/secrets.yaml b/test/helm/postgres/templates/secrets.yaml deleted file mode 100644 index acc16814..00000000 --- a/test/helm/postgres/templates/secrets.yaml +++ /dev/null @@ -1,25 +0,0 @@ -{{- if not .Values.existingSecret }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ template "postgresql.fullname" . }} - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} -type: Opaque -data: - {{- if .Values.postgresqlPassword }} - postgresql-password: {{ .Values.postgresqlPassword | b64enc | quote }} - {{- else }} - postgresql-password: {{ randAlphaNum 10 | b64enc | quote }} - {{- end }} - {{- if .Values.replication.enabled }} - {{- if .Values.replication.password }} - postgresql-replication-password: {{ .Values.replication.password | b64enc | quote }} - {{- else }} - postgresql-replication-password: {{ randAlphaNum 10 | b64enc | quote }} - {{- end }} - {{- end }} -{{- end -}} diff --git a/test/helm/postgres/templates/statefulset-slaves.yaml b/test/helm/postgres/templates/statefulset-slaves.yaml deleted file mode 100644 index 057ed664..00000000 --- a/test/helm/postgres/templates/statefulset-slaves.yaml +++ /dev/null @@ -1,211 +0,0 @@ -{{- if .Values.replication.enabled }} -apiVersion: apps/v1beta2 -kind: StatefulSet -metadata: - name: "{{ template "postgresql.fullname" . }}-slave" - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} -spec: - serviceName: {{ template "postgresql.fullname" . }}-headless - replicas: {{ .Values.replication.slaveReplicas }} - selector: - matchLabels: - app: {{ template "postgresql.name" . }} - release: {{ .Release.Name | quote }} - role: slave - template: - metadata: - name: {{ template "postgresql.fullname" . }} - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} - role: slave - spec: - {{- if .Values.securityContext.enabled }} - securityContext: - fsGroup: {{ .Values.securityContext.fsGroup }} - runAsUser: {{ .Values.securityContext.runAsUser }} - {{- end }} - {{- if .Values.image.pullSecrets }} - imagePullSecrets: - {{- range .Values.image.pullSecrets }} - - name: {{ . }} - {{- end}} - {{- end }} - {{- if .Values.slave.nodeSelector }} - nodeSelector: -{{ toYaml .Values.slave.nodeSelector | indent 8 }} - {{- end }} - {{- if .Values.slave.affinity }} - affinity: -{{ toYaml .Values.slave.affinity | indent 8 }} - {{- end }} - {{- if .Values.slave.tolerations }} - tolerations: -{{ toYaml .Values.slave.tolerations | indent 8 }} - {{- end }} - {{- if .Values.terminationGracePeriodSeconds }} - terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} - {{- end }} - {{- if and .Values.volumePermissions.enabled .Values.persistence.enabled }} - initContainers: - - name: init-chmod-data - image: {{ template "postgresql.volumePermissions.image" . }} - imagePullPolicy: "{{ .Values.volumePermissions.image.pullPolicy }}" - resources: -{{ toYaml .Values.resources | indent 10 }} - command: - - sh - - -c - - | - chown -R {{ .Values.securityContext.runAsUser }}:{{ .Values.securityContext.fsGroup }} /bitnami - if [ -d /bitnami/postgresql/data ]; then - chmod 0700 /bitnami/postgresql/data; - fi - securityContext: - runAsUser: {{ .Values.volumePermissions.securityContext.runAsUser }} - volumeMounts: - - name: data - mountPath: /bitnami/postgresql - {{- end }} - containers: - - name: {{ template "postgresql.fullname" . }} - image: {{ template "postgresql.image" . }} - imagePullPolicy: "{{ .Values.image.pullPolicy }}" - resources: -{{ toYaml .Values.resources | indent 10 }} - env: - {{- if .Values.image.debug}} - - name: BASH_DEBUG - value: "1" - - name: NAMI_DEBUG - value: "1" - {{- end }} - - name: POSTGRESQL_REPLICATION_MODE - value: "slave" - - name: POSTGRESQL_REPLICATION_USER - value: {{ .Values.replication.user | quote }} - {{- if .Values.usePasswordFile }} - - name: POSTGRESQL_REPLICATION_PASSWORD_FILE - value: "/opt/bitnami/postgresql/secrets/postgresql-replication-password" - {{- else }} - - name: POSTGRESQL_REPLICATION_PASSWORD - valueFrom: - secretKeyRef: - name: {{ template "postgresql.secretName" . }} - key: postgresql-replication-password - {{- end }} - - name: POSTGRESQL_CLUSTER_APP_NAME - value: {{ .Values.replication.applicationName }} - - name: POSTGRESQL_MASTER_HOST - value: {{ template "postgresql.fullname" . }} - - name: POSTGRESQL_MASTER_PORT_NUMBER - value: {{ .Values.service.port | quote }} - ports: - - name: postgresql - containerPort: {{ .Values.service.port }} - {{- if .Values.livenessProbe.enabled }} - livenessProbe: - exec: - command: - - sh - - -c - {{- if .Values.postgresqlDatabase }} - - exec pg_isready -U {{ .Values.postgresqlUsername | quote }} -d {{ .Values.postgresqlDatabase | quote }} -h localhost - {{- else }} - - exec pg_isready -U {{ .Values.postgresqlUsername | quote }} -h localhost - {{- end }} - initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} - periodSeconds: {{ .Values.livenessProbe.periodSeconds }} - timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} - successThreshold: {{ .Values.livenessProbe.successThreshold }} - failureThreshold: {{ .Values.livenessProbe.failureThreshold }} - {{- end }} - {{- if .Values.readinessProbe.enabled }} - readinessProbe: - exec: - command: - - sh - - -c - {{- if .Values.postgresqlDatabase }} - - exec pg_isready -U {{ .Values.postgresqlUsername | quote }} -d {{ .Values.postgresqlDatabase | quote }} -h localhost - {{- else }} - - exec pg_isready -U {{ .Values.postgresqlUsername | quote }} -h localhost - {{- end }} - initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} - periodSeconds: {{ .Values.readinessProbe.periodSeconds }} - timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} - successThreshold: {{ .Values.readinessProbe.successThreshold }} - failureThreshold: {{ .Values.readinessProbe.failureThreshold }} - {{- end }} - volumeMounts: - {{- if .Values.usePasswordFile }} - - name: postgresql-password - mountPath: /opt/bitnami/postgresql/secrets - {{ end }} - {{- if .Values.persistence.enabled }} - - name: data - mountPath: {{ .Values.persistence.mountPath }} - {{ end }} - {{- if or (.Files.Glob "files/conf.d/*.conf") .Values.extendedConfConfigMap }} - - name: postgresql-extended-config - mountPath: /bitnami/postgresql/conf/conf.d/ - {{- end }} - {{- if or (.Files.Glob "files/postgresql.conf") (.Files.Glob "files/pg_hba.conf") .Values.postgresqlConfiguration .Values.pgHbaConfiguration .Values.configurationConfigMap }} - - name: postgresql-config - mountPath: /bitnami/postgresql/conf - {{- end }} - volumes: - {{- if .Values.usePasswordFile }} - - name: postgresql-password - secret: - secretName: {{ template "postgresql.secretName" . }} - {{ end }} - {{- if or (.Files.Glob "files/postgresql.conf") (.Files.Glob "files/pg_hba.conf") .Values.postgresqlConfiguration .Values.pgHbaConfiguration .Values.configurationConfigMap}} - - name: postgresql-config - configMap: - name: {{ template "postgresql.configurationCM" . }} - {{- end }} - {{- if or (.Files.Glob "files/conf.d/*.conf") .Values.extendedConfConfigMap }} - - name: postgresql-extended-config - configMap: - name: {{ template "postgresql.extendedConfigurationCM" . }} - {{- end }} - {{- if not .Values.persistence.enabled }} - - name: data - emptyDir: {} - {{- end }} - updateStrategy: - type: {{ .Values.updateStrategy.type }} -{{- if .Values.persistence.enabled }} - volumeClaimTemplates: - - metadata: - name: data - {{- with .Values.persistence.annotations }} - annotations: - {{- range $key, $value := . }} - {{ $key }}: {{ $value }} - {{- end }} - {{- end }} - spec: - accessModes: - {{- range .Values.persistence.accessModes }} - - {{ . | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.size | quote }} - {{- if .Values.persistence.storageClass }} - {{- if (eq "-" .Values.persistence.storageClass) }} - storageClassName: "" - {{- else }} - storageClassName: "{{ .Values.persistence.storageClass }}" - {{- end }} - {{- end }} -{{- end }} -{{- end }} diff --git a/test/helm/postgres/templates/statefulset.yaml b/test/helm/postgres/templates/statefulset.yaml deleted file mode 100644 index d85826fc..00000000 --- a/test/helm/postgres/templates/statefulset.yaml +++ /dev/null @@ -1,300 +0,0 @@ -apiVersion: apps/v1beta2 -kind: StatefulSet -metadata: - name: {{ template "postgresql.master.fullname" . }} - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} -spec: - serviceName: {{ template "postgresql.fullname" . }}-headless - replicas: 1 - updateStrategy: - type: {{ .Values.updateStrategy.type }} - selector: - matchLabels: - app: {{ template "postgresql.name" . }} - release: {{ .Release.Name | quote }} - role: master - template: - metadata: - name: {{ template "postgresql.fullname" . }} - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} - role: master - spec: - {{- if .Values.securityContext.enabled }} - securityContext: - fsGroup: {{ .Values.securityContext.fsGroup }} - runAsUser: {{ .Values.securityContext.runAsUser }} - {{- end }} - {{- if or .Values.image.pullSecrets .Values.metrics.image.pullSecrets }} - imagePullSecrets: - {{- range .Values.image.pullSecrets }} - - name: {{ . }} - {{- end}} - {{- range .Values.metrics.image.pullSecrets }} - - name: {{ . }} - {{- end}} - {{- end }} - {{- if .Values.master.nodeSelector }} - nodeSelector: -{{ toYaml .Values.master.nodeSelector | indent 8 }} - {{- end }} - {{- if .Values.master.affinity }} - affinity: -{{ toYaml .Values.master.affinity | indent 8 }} - {{- end }} - {{- if .Values.master.tolerations }} - tolerations: -{{ toYaml .Values.master.tolerations | indent 8 }} - {{- end }} - {{- if .Values.terminationGracePeriodSeconds }} - terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} - {{- end }} - {{- if and .Values.volumePermissions.enabled .Values.persistence.enabled }} - initContainers: - - name: init-chmod-data - image: {{ template "postgresql.volumePermissions.image" . }} - imagePullPolicy: "{{ .Values.volumePermissions.image.pullPolicy }}" - resources: -{{ toYaml .Values.resources | indent 10 }} - command: - - sh - - -c - - | - chown -R {{ .Values.securityContext.runAsUser }}:{{ .Values.securityContext.fsGroup }} /bitnami - if [ -d /bitnami/postgresql/data ]; then - chmod 0700 /bitnami/postgresql/data; - fi - securityContext: - runAsUser: {{ .Values.volumePermissions.securityContext.runAsUser }} - volumeMounts: - - name: data - mountPath: /bitnami/postgresql - {{- end }} - containers: - - name: {{ template "postgresql.fullname" . }} - image: {{ template "postgresql.image" . }} - imagePullPolicy: "{{ .Values.image.pullPolicy }}" - resources: -{{ toYaml .Values.resources | indent 10 }} - env: - {{- if .Values.image.debug}} - - name: BASH_DEBUG - value: "1" - - name: NAMI_DEBUG - value: "1" - {{- end }} - {{- if .Values.replication.enabled }} - - name: POSTGRESQL_REPLICATION_MODE - value: "master" - - name: POSTGRESQL_REPLICATION_USER - value: {{ .Values.replication.user | quote }} - {{- if .Values.usePasswordFile }} - - name: POSTGRESQL_REPLICATION_PASSWORD_FILE - value: "/opt/bitnami/postgresql/secrets/postgresql-replication-password" - {{- else }} - - name: POSTGRESQL_REPLICATION_PASSWORD - valueFrom: - secretKeyRef: - name: {{ template "postgresql.secretName" . }} - key: postgresql-replication-password - {{- end }} - {{- if not (eq .Values.replication.synchronousCommit "off")}} - - name: POSTGRESQL_SYNCHRONOUS_COMMIT_MODE - value: {{ .Values.replication.synchronousCommit | quote }} - - name: POSTGRESQL_NUM_SYNCHRONOUS_REPLICAS - value: {{ .Values.replication.numSynchronousReplicas | quote }} - {{- end }} - - name: POSTGRESQL_CLUSTER_APP_NAME - value: {{ .Values.replication.applicationName }} - {{- end }} - - name: POSTGRESQL_USERNAME - value: {{ .Values.postgresqlUsername | quote }} - {{- if .Values.usePasswordFile }} - - name: POSTGRESQL_PASSWORD_FILE - value: "/opt/bitnami/postgresql/secrets/postgresql-password" - {{- else }} - - name: POSTGRESQL_PASSWORD - valueFrom: - secretKeyRef: - name: {{ template "postgresql.secretName" . }} - key: postgresql-password - {{- end }} - {{- if .Values.postgresqlDatabase }} - - name: POSTGRESQL_DATABASE - value: {{ .Values.postgresqlDatabase | quote }} - {{- end }} -{{- if .Values.extraEnv }} -{{ toYaml .Values.extraEnv | indent 8 }} -{{- end }} - ports: - - name: postgresql - containerPort: {{ .Values.service.port }} - {{- if .Values.livenessProbe.enabled }} - livenessProbe: - exec: - command: - - sh - - -c - {{- if .Values.postgresqlDatabase }} - - exec pg_isready -U {{ .Values.postgresqlUsername | quote }} -d {{ .Values.postgresqlDatabase | quote }} -h localhost - {{- else }} - - exec pg_isready -U {{ .Values.postgresqlUsername | quote }} -h localhost - {{- end }} - initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} - periodSeconds: {{ .Values.livenessProbe.periodSeconds }} - timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} - successThreshold: {{ .Values.livenessProbe.successThreshold }} - failureThreshold: {{ .Values.livenessProbe.failureThreshold }} - {{- end }} - {{- if .Values.readinessProbe.enabled }} - readinessProbe: - exec: - command: - - sh - - -c - {{- if .Values.postgresqlDatabase }} - - exec pg_isready -U {{ .Values.postgresqlUsername | quote }} -d {{ .Values.postgresqlDatabase | quote }} -h localhost - {{- else }} - - exec pg_isready -U {{ .Values.postgresqlUsername | quote }} -h localhost - {{- end }} - initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} - periodSeconds: {{ .Values.readinessProbe.periodSeconds }} - timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} - successThreshold: {{ .Values.readinessProbe.successThreshold }} - failureThreshold: {{ .Values.readinessProbe.failureThreshold }} - {{- end }} - volumeMounts: - {{- if or (.Files.Glob "files/docker-entrypoint-initdb.d/*.{sh,sql,sql.gz}") .Values.initdbScriptsConfigMap .Values.initdbScripts }} - - name: custom-init-scripts - mountPath: /docker-entrypoint-initdb.d - {{- end }} - {{- if or (.Files.Glob "files/conf.d/*.conf") .Values.postgresqlExtendedConf .Values.extendedConfConfigMap }} - - name: postgresql-extended-config - mountPath: /bitnami/postgresql/conf/conf.d/ - {{- end }} - {{- if .Values.usePasswordFile }} - - name: postgresql-password - mountPath: /opt/bitnami/postgresql/secrets/ - {{- end }} - {{- if .Values.persistence.enabled }} - - name: data - mountPath: {{ .Values.persistence.mountPath }} - {{- end }} - {{- if or (.Files.Glob "files/postgresql.conf") (.Files.Glob "files/pg_hba.conf") .Values.postgresqlConfiguration .Values.pgHbaConfiguration .Values.configurationConfigMap }} - - name: postgresql-config - mountPath: /bitnami/postgresql/conf - {{- end }} -{{- if .Values.metrics.enabled }} - - name: metrics - image: {{ template "metrics.image" . }} - imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }} - env: - {{- $database := required "In order to enable metrics you need to specify a database (.Values.postgresqlDatabase)" .Values.postgresqlDatabase }} - - name: DATA_SOURCE_URI - value: {{ printf "localhost:%d/%s?sslmode=disable" (int .Values.service.port) $database | quote }} - {{- if .Values.usePasswordFile }} - - name: DATA_SOURCE_PASS_FILE - value: "/opt/bitnami/postgresql/secrets/postgresql-password" - {{- else }} - - name: DATA_SOURCE_PASS - valueFrom: - secretKeyRef: - name: {{ template "postgresql.secretName" . }} - key: postgresql-password - {{- end }} - - name: DATA_SOURCE_USER - value: {{ .Values.postgresqlUsername }} - {{- if .Values.livenessProbe.enabled }} - livenessProbe: - httpGet: - path: / - port: metrics - initialDelaySeconds: {{ .Values.metrics.livenessProbe.initialDelaySeconds }} - periodSeconds: {{ .Values.metrics.livenessProbe.periodSeconds }} - timeoutSeconds: {{ .Values.metrics.livenessProbe.timeoutSeconds }} - successThreshold: {{ .Values.metrics.livenessProbe.successThreshold }} - failureThreshold: {{ .Values.metrics.livenessProbe.failureThreshold }} - {{- end }} - {{- if .Values.readinessProbe.enabled }} - readinessProbe: - httpGet: - path: / - port: metrics - initialDelaySeconds: {{ .Values.metrics.readinessProbe.initialDelaySeconds }} - periodSeconds: {{ .Values.metrics.readinessProbe.periodSeconds }} - timeoutSeconds: {{ .Values.metrics.readinessProbe.timeoutSeconds }} - successThreshold: {{ .Values.metrics.readinessProbe.successThreshold }} - failureThreshold: {{ .Values.metrics.readinessProbe.failureThreshold }} - {{- end }} - volumeMounts: - {{- if .Values.usePasswordFile }} - - name: postgresql-password - mountPath: /opt/bitnami/postgresql/secrets/ - {{- end }} - ports: - - name: metrics - containerPort: 9187 - resources: -{{ toYaml .Values.metrics.resources | indent 10 }} -{{- end }} - volumes: - {{- if or (.Files.Glob "files/postgresql.conf") (.Files.Glob "files/pg_hba.conf") .Values.postgresqlConfiguration .Values.pgHbaConfiguration .Values.configurationConfigMap}} - - name: postgresql-config - configMap: - name: {{ template "postgresql.configurationCM" . }} - {{- end }} - {{- if or (.Files.Glob "files/conf.d/*.conf") .Values.postgresqlExtendedConf .Values.extendedConfConfigMap }} - - name: postgresql-extended-config - configMap: - name: {{ template "postgresql.extendedConfigurationCM" . }} - {{- end }} - {{- if .Values.usePasswordFile }} - - name: postgresql-password - secret: - secretName: {{ template "postgresql.secretName" . }} - {{- end }} - {{- if or (.Files.Glob "files/docker-entrypoint-initdb.d/*.{sh,sql,sql.gz}") .Values.initdbScriptsConfigMap .Values.initdbScripts }} - - name: custom-init-scripts - configMap: - name: {{ template "postgresql.initdbScriptsCM" . }} - {{- end }} -{{- if and .Values.persistence.enabled .Values.persistence.existingClaim }} - - name: data - persistentVolumeClaim: - claimName: {{ .Values.persistence.existingClaim }} -{{- else if not .Values.persistence.enabled }} - - name: data - emptyDir: {} -{{- else if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} - volumeClaimTemplates: - - metadata: - name: data - {{- with .Values.persistence.annotations }} - annotations: - {{- range $key, $value := . }} - {{ $key }}: {{ $value }} - {{- end }} - {{- end }} - spec: - accessModes: - {{- range .Values.persistence.accessModes }} - - {{ . | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.size | quote }} - {{- if .Values.persistence.storageClass }} - {{- if (eq "-" .Values.persistence.storageClass) }} - storageClassName: "" - {{- else }} - storageClassName: "{{ .Values.persistence.storageClass }}" - {{- end }} - {{- end }} -{{- end }} diff --git a/test/helm/postgres/templates/svc-headless.yaml b/test/helm/postgres/templates/svc-headless.yaml deleted file mode 100644 index 9414d609..00000000 --- a/test/helm/postgres/templates/svc-headless.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ template "postgresql.fullname" . }}-headless - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} -spec: - type: ClusterIP - clusterIP: None - ports: - - name: postgresql - port: 5432 - targetPort: postgresql - selector: - app: {{ template "postgresql.name" . }} - release: {{ .Release.Name | quote }} diff --git a/test/helm/postgres/templates/svc-read.yaml b/test/helm/postgres/templates/svc-read.yaml deleted file mode 100644 index 6b2de778..00000000 --- a/test/helm/postgres/templates/svc-read.yaml +++ /dev/null @@ -1,31 +0,0 @@ -{{- if .Values.replication.enabled }} -apiVersion: v1 -kind: Service -metadata: - name: {{ template "postgresql.fullname" . }}-read - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} -{{- with .Values.service.annotations }} - annotations: -{{ toYaml . | indent 4 }} -{{- end }} -spec: - type: {{ .Values.service.type }} - {{- if and .Values.service.loadBalancerIP (eq .Values.service.type "LoadBalancer") }} - loadBalancerIP: {{ .Values.service.loadBalancerIP }} - {{- end }} - ports: - - name: postgresql - port: {{ .Values.service.port }} - targetPort: postgresql - {{- if .Values.service.nodePort }} - nodePort: {{ .Values.service.nodePort }} - {{- end }} - selector: - app: {{ template "postgresql.name" . }} - release: {{ .Release.Name | quote }} - role: slave -{{- end }} diff --git a/test/helm/postgres/templates/svc.yaml b/test/helm/postgres/templates/svc.yaml deleted file mode 100644 index 31b9b08d..00000000 --- a/test/helm/postgres/templates/svc.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ template "postgresql.fullname" . }} - labels: - app: {{ template "postgresql.name" . }} - chart: {{ template "postgresql.chart" . }} - release: {{ .Release.Name | quote }} - heritage: {{ .Release.Service | quote }} -{{- with .Values.service.annotations }} - annotations: -{{ toYaml . | indent 4 }} -{{- end }} -spec: - type: {{ .Values.service.type }} - {{- if and .Values.service.loadBalancerIP (eq .Values.service.type "LoadBalancer") }} - loadBalancerIP: {{ .Values.service.loadBalancerIP }} - {{- end }} - {{- if and (eq .Values.service.type "ClusterIP") .Values.service.clusterIP }} - clusterIP: {{ .Values.service.clusterIP }} - {{- end }} - ports: - - name: postgresql - port: {{ .Values.service.port }} - targetPort: postgresql - {{- if .Values.service.nodePort }} - nodePort: {{ .Values.service.nodePort }} - {{- end }} - selector: - app: {{ template "postgresql.name" . }} - release: {{ .Release.Name | quote }} - role: master diff --git a/test/helm/postgres/values-production.yaml b/test/helm/postgres/values-production.yaml deleted file mode 100644 index f53542fb..00000000 --- a/test/helm/postgres/values-production.yaml +++ /dev/null @@ -1,283 +0,0 @@ -## Global Docker image registry -### Please, note that this will override the image registry for all the images, including dependencies, configured to use the global value -### -## global: -## imageRegistry: - -## Bitnami PostgreSQL image version -## ref: https://hub.docker.com/r/bitnami/postgresql/tags/ -## -image: - registry: docker.io - repository: bitnami/postgresql - tag: 10.6.0 - ## Specify a imagePullPolicy - ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' - ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images - ## - pullPolicy: Always - - ## Optionally specify an array of imagePullSecrets. - ## Secrets must be manually created in the namespace. - ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ - ## - # pullSecrets: - # - myRegistrKeySecretName - - ## Set to true if you would like to see extra information on logs - ## It turns BASH and NAMI debugging in minideb - ## ref: https://github.com/bitnami/minideb-extras/#turn-on-bash-debugging - debug: false - -## -## Init containers parameters: -## volumePermissions: Change the owner of the persist volume mountpoint to RunAsUser:fsGroup -## -volumePermissions: - enabled: true - image: - registry: docker.io - repository: bitnami/minideb - tag: latest - ## Specify a imagePullPolicy - ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' - ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images - ## - pullPolicy: Always - ## Init container Security Context - securityContext: - runAsUser: 0 - -## Pod Security Context -## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ -## -securityContext: - enabled: true - fsGroup: 1001 - runAsUser: 1001 - -replication: - enabled: true - user: repl_user - password: repl_password - slaveReplicas: 2 - ## Set synchronous commit mode: on, off, remote_apply, remote_write and local - ## ref: https://www.postgresql.org/docs/9.6/runtime-config-wal.html#GUC-WAL-LEVEL - synchronousCommit: "on" - ## From the number of `slaveReplicas` defined above, set the number of those that will have synchronous replication - ## NOTE: It cannot be > slaveReplicas - numSynchronousReplicas: 1 - ## Replication Cluster application name. Useful for defining multiple replication policies - applicationName: my_application - -## PostgreSQL admin user -## ref: https://github.com/bitnami/bitnami-docker-postgresql/blob/master/README.md#setting-the-root-password-on-first-run -postgresqlUsername: postgres - -## PostgreSQL password -## ref: https://github.com/bitnami/bitnami-docker-postgresql/blob/master/README.md#setting-the-root-password-on-first-run -## -# postgresqlPassword: - -## Create a database -## ref: https://github.com/bitnami/bitnami-docker-postgresql/blob/master/README.md#creating-a-database-on-first-run -## -# postgresqlDatabase: - -## PostgreSQL password using existing secret -## existingSecret: secret - -## Mount PostgreSQL secret as a file instead of passing environment variable -# usePasswordFile: false - -## PostgreSQL configuration -## Specify runtime configuration parameters as a dict, using camelCase, e.g. -## {"sharedBuffers": "500MB"} -## Alternatively, you can put your postgresql.conf under the files/ directory -## ref: https://www.postgresql.org/docs/current/static/runtime-config.html -## -# postgresqlConfiguration: - -## PostgreSQL extended configuration -## As above, but _appended_ to the main configuration -## Alternatively, you can put your *.conf under the files/conf.d/ directory -## https://github.com/bitnami/bitnami-docker-postgresql#allow-settings-to-be-loaded-from-files-other-than-the-default-postgresqlconf -## -# postgresqlExtendedConf: - -## PostgreSQL client authentication configuration -## Specify content for pg_hba.conf -## Default: do not create pg_hba.conf -## Alternatively, you can put your pg_hba.conf under the files/ directory -# pgHbaConfiguration: |- -# local all all trust -# host all all localhost trust -# host mydatabase mysuser 192.168.0.0/24 md5 - -## ConfigMap with PostgreSQL configuration -## NOTE: This will override postgresqlConfiguration and pgHbaConfiguration -# configurationConfigMap: - -## ConfigMap with PostgreSQL extended configuration -# extendedConfConfigMap: - -## initdb scripts -## Specify dictionnary of scripts to be run at first boot -## Alternatively, you can put your scripts under the files/docker-entrypoint-initdb.d directory -## -# initdbScripts: -# my_init_script.sh:| -# #!/bin/sh -# echo "Do something." - -## ConfigMap with scripts to be run at first boot -## NOTE: This will override initdbScripts -# initdbScriptsConfigMap: - -## PostgreSQL service configuration -service: - ## PosgresSQL service type - type: ClusterIP - port: 5432 - - ## Specify the nodePort value for the LoadBalancer and NodePort service types. - ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport - ## - # nodePort: - - ## Provide any additional annotations which may be required. This can be used to - annotations: {} - ## Set the LoadBalancer service type to internal only. - ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer - ## - # loadBalancerIP: - -## PostgreSQL data Persistent Volume Storage Class -## If defined, storageClassName: -## If set to "-", storageClassName: "", which disables dynamic provisioning -## If undefined (the default) or set to null, no storageClassName spec is -## set, choosing the default provisioner. (gp2 on AWS, standard on -## GKE, AWS & OpenStack) -## -persistence: - enabled: true - ## A manually managed Persistent Volume and Claim - ## If defined, PVC must be created manually before volume will be bound - # existingClaim: - mountPath: /bitnami/postgresql - # storageClass: "-" - accessModes: - - ReadWriteOnce - size: 8Gi - annotations: {} - -## updateStrategy for PostgreSQL StatefulSet and its slaves StatefulSets -## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies -updateStrategy: - type: RollingUpdate - -## -## PostgreSQL Master parameters -## -master: - ## Node, affinity and tolerations labels for pod assignment - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#taints-and-tolerations-beta-feature - nodeSelector: {} - affinity: {} - tolerations: [] - -## -## PostgreSQL Slave parameters -## -slave: - ## Node, affinity and tolerations labels for pod assignment - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#taints-and-tolerations-beta-feature - nodeSelector: {} - affinity: {} - tolerations: [] - -## Configure resource requests and limits -## ref: http://kubernetes.io/docs/user-guide/compute-resources/ -## -resources: - requests: - memory: 256Mi - cpu: 250m - -networkPolicy: - ## Enable creation of NetworkPolicy resources. - ## - enabled: false - - ## The Policy model to apply. When set to false, only pods with the correct - ## client label will have network access to the port PostgreSQL is listening - ## on. When true, PostgreSQL will accept connections from any source - ## (with the correct destination port). - ## - allowExternal: true - -## Configure extra options for liveness and readiness probes -## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes) -livenessProbe: - enabled: true - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 6 - successThreshold: 1 - -readinessProbe: - enabled: true - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 6 - successThreshold: 1 - -## Configure metrics exporter -## -metrics: - enabled: true - # resources: {} - service: - type: ClusterIP - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "9187" - loadBalancerIP: - image: - registry: docker.io - repository: wrouesnel/postgres_exporter - tag: v0.4.6 - pullPolicy: IfNotPresent - ## Optionally specify an array of imagePullSecrets. - ## Secrets must be manually created in the namespace. - ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ - ## - # pullSecrets: - # - myRegistrKeySecretName - - ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes) - ## Configure extra options for liveness and readiness probes - livenessProbe: - enabled: true - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 6 - successThreshold: 1 - - readinessProbe: - enabled: true - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 6 - successThreshold: 1 - -# Define custom environment variables to pass to the image here -extraEnv: {} diff --git a/test/helm/postgres/values.yaml b/test/helm/postgres/values.yaml deleted file mode 100644 index cfbe5560..00000000 --- a/test/helm/postgres/values.yaml +++ /dev/null @@ -1,289 +0,0 @@ -## Global Docker image registry -### Please, note that this will override the image registry for all the images, including dependencies, configured to use the global value -### -## global: -## imageRegistry: - -## Bitnami PostgreSQL image version -## ref: https://hub.docker.com/r/bitnami/postgresql/tags/ -## -image: - registry: docker.io - repository: bitnami/postgresql - tag: 10.6.0 - ## Specify a imagePullPolicy - ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' - ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images - ## - pullPolicy: Always - - ## Optionally specify an array of imagePullSecrets. - ## Secrets must be manually created in the namespace. - ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ - ## - # pullSecrets: - # - myRegistrKeySecretName - - ## Set to true if you would like to see extra information on logs - ## It turns BASH and NAMI debugging in minideb - ## ref: https://github.com/bitnami/minideb-extras/#turn-on-bash-debugging - debug: false - -## -## Init containers parameters: -## volumePermissions: Change the owner of the persist volume mountpoint to RunAsUser:fsGroup -## -volumePermissions: - enabled: true - image: - registry: docker.io - repository: bitnami/minideb - tag: latest - ## Specify a imagePullPolicy - ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' - ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images - ## - pullPolicy: Always - ## Init container Security Context - securityContext: - runAsUser: 0 - -## Pod Security Context -## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ -## -securityContext: - enabled: true - fsGroup: 1001 - runAsUser: 1001 - -replication: - enabled: true - user: repl_user - password: repl_password - slaveReplicas: 1 - ## Set synchronous commit mode: on, off, remote_apply, remote_write and local - ## ref: https://www.postgresql.org/docs/9.6/runtime-config-wal.html#GUC-WAL-LEVEL - synchronousCommit: "off" - ## From the number of `slaveReplicas` defined above, set the number of those that will have synchronous replication - ## NOTE: It cannot be > slaveReplicas - numSynchronousReplicas: 0 - ## Replication Cluster application name. Useful for defining multiple replication policies - applicationName: my_application - -## PostgreSQL admin user -## ref: https://github.com/bitnami/bitnami-docker-postgresql/blob/master/README.md#setting-the-root-password-on-first-run -postgresqlUsername: postgres - -## PostgreSQL password -## ref: https://github.com/bitnami/bitnami-docker-postgresql/blob/master/README.md#setting-the-root-password-on-first-run -## -postgresqlPassword: dangerous - -## PostgreSQL password using existing secret -## existingSecret: secret - -## Mount PostgreSQL secret as a file instead of passing environment variable -# usePasswordFile: false - -## Create a database -## ref: https://github.com/bitnami/bitnami-docker-postgresql/blob/master/README.md#creating-a-database-on-first-run -## -# postgresqlDatabase: - -## PostgreSQL configuration -## Specify runtime configuration parameters as a dict, using camelCase, e.g. -## {"sharedBuffers": "500MB"} -## Alternatively, you can put your postgresql.conf under the files/ directory -## ref: https://www.postgresql.org/docs/current/static/runtime-config.html -## -# postgresqlConfiguration: - -## PostgreSQL extended configuration -## As above, but _appended_ to the main configuration -## Alternatively, you can put your *.conf under the files/conf.d/ directory -## https://github.com/bitnami/bitnami-docker-postgresql#allow-settings-to-be-loaded-from-files-other-than-the-default-postgresqlconf -## -# postgresqlExtendedConf: - -## PostgreSQL client authentication configuration -## Specify content for pg_hba.conf -## Default: do not create pg_hba.conf -## Alternatively, you can put your pg_hba.conf under the files/ directory -# pgHbaConfiguration: |- -# local all all trust -# host all all localhost trust -# host mydatabase mysuser 192.168.0.0/24 md5 - -## ConfigMap with PostgreSQL configuration -## NOTE: This will override postgresqlConfiguration and pgHbaConfiguration -# configurationConfigMap: - -## ConfigMap with PostgreSQL extended configuration -# extendedConfConfigMap: - -## initdb scripts -## Specify dictionnary of scripts to be run at first boot -## Alternatively, you can put your scripts under the files/docker-entrypoint-initdb.d directory -## -# initdbScripts: -# my_init_script.sh:| -# #!/bin/sh -# echo "Do something." -# -## ConfigMap with scripts to be run at first boot -## NOTE: This will override initdbScripts -# initdbScriptsConfigMap: - -## Optional duration in seconds the pod needs to terminate gracefully. -## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods -## -# terminationGracePeriodSeconds: 30 - -## PostgreSQL service configuration -service: - ## PosgresSQL service type - type: ClusterIP - # clusterIP: None - port: 5432 - - ## Specify the nodePort value for the LoadBalancer and NodePort service types. - ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport - ## - # nodePort: - - ## Provide any additional annotations which may be required. This can be used to - annotations: {} - ## Set the LoadBalancer service type to internal only. - ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer - ## - # loadBalancerIP: - -## PostgreSQL data Persistent Volume Storage Class -## If defined, storageClassName: -## If set to "-", storageClassName: "", which disables dynamic provisioning -## If undefined (the default) or set to null, no storageClassName spec is -## set, choosing the default provisioner. (gp2 on AWS, standard on -## GKE, AWS & OpenStack) -## -persistence: - enabled: true - ## A manually managed Persistent Volume and Claim - ## If defined, PVC must be created manually before volume will be bound - # existingClaim: - mountPath: /bitnami/postgresql - storageClass: "vxflexos" - accessModes: - - ReadWriteOnce - size: 8Gi - annotations: {} - -## updateStrategy for PostgreSQL StatefulSet and its slaves StatefulSets -## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies -updateStrategy: - type: RollingUpdate - -## -## PostgreSQL Master parameters -## -master: - ## Node, affinity and tolerations labels for pod assignment - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#taints-and-tolerations-beta-feature - nodeSelector: {} - affinity: {} - tolerations: [] - -## -## PostgreSQL Slave parameters -## -slave: - ## Node, affinity and tolerations labels for pod assignment - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity - ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#taints-and-tolerations-beta-feature - nodeSelector: {} - affinity: {} - tolerations: [] - -## Configure resource requests and limits -## ref: http://kubernetes.io/docs/user-guide/compute-resources/ -## -resources: - requests: - memory: 256Mi - cpu: 250m - -networkPolicy: - ## Enable creation of NetworkPolicy resources. - ## - enabled: false - - ## The Policy model to apply. When set to false, only pods with the correct - ## client label will have network access to the port PostgreSQL is listening - ## on. When true, PostgreSQL will accept connections from any source - ## (with the correct destination port). - ## - allowExternal: true - -## Configure extra options for liveness and readiness probes -## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes) -livenessProbe: - enabled: true - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 6 - successThreshold: 1 - -readinessProbe: - enabled: true - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 6 - successThreshold: 1 - -## Configure metrics exporter -## -metrics: - enabled: false - # resources: {} - service: - type: ClusterIP - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "9187" - loadBalancerIP: - image: - registry: docker.io - repository: wrouesnel/postgres_exporter - tag: v0.4.6 - pullPolicy: IfNotPresent - ## Optionally specify an array of imagePullSecrets. - ## Secrets must be manually created in the namespace. - ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ - ## - # pullSecrets: - # - myRegistrKeySecretName - - ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes) - ## Configure extra options for liveness and readiness probes - livenessProbe: - enabled: true - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 6 - successThreshold: 1 - - readinessProbe: - enabled: true - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 6 - successThreshold: 1 - -# Define custom environment variables to pass to the image here -extraEnv: {} diff --git a/test/helm/snaprestoretest.sh b/test/helm/snaprestoretest.sh index 244f67f1..977833ab 100755 --- a/test/helm/snaprestoretest.sh +++ b/test/helm/snaprestoretest.sh @@ -3,7 +3,7 @@ NS=helmtest-vxflexos source ./common.bash echo "installing a 2 volume container" -sh starttest.sh 2vols +bash starttest.sh 2vols echo "done installing a 2 volume container" echo "marking volume" kubectl exec -n ${NS} vxflextest-0 -- touch /data0/orig @@ -20,8 +20,8 @@ echo "waiting for container to upgrade/stabalize" sleep 20 waitOnRunning kubectl describe pods -n ${NS} -kubectl exec -n ${NS} vxflextest-0 -it df | grep data -kubectl exec -n ${NS} vxflextest-0 -it mount | grep data +kubectl exec -n ${NS} vxflextest-0 -- df | grep data +kubectl exec -n ${NS} vxflextest-0 -- mount | grep data echo "updating container finished" echo "marking volume" kubectl exec -n ${NS} vxflextest-0 -- touch /data2/new @@ -31,7 +31,7 @@ echo "listing /data2" kubectl exec -n ${NS} vxflextest-0 -- ls -l /data2 sleep 20 echo "deleting container" -sh stoptest.sh 2vols +bash stoptest.sh 2vols sleep 5 echo "deleting snap" kubectl delete volumesnapshot pvol0-snap1 -n ${NS} diff --git a/test/helm/starttest.sh b/test/helm/starttest.sh index 91401084..9239acb3 100755 --- a/test/helm/starttest.sh +++ b/test/helm/starttest.sh @@ -8,11 +8,11 @@ source ./common.bash RELEASE=`basename "${1}"` -helm install -n ${NS} "${RELEASE}" $1 +helm install -n ${NS} "${RELEASE}" $1 sleep 30 kubectl describe pods -n ${NS} waitOnRunning kubectl describe pods -n ${NS} -kubectl exec -n ${NS} vxflextest-0 -it df | grep data -kubectl exec -n ${NS} vxflextest-0 -it mount | grep data +kubectl exec -n ${NS} vxflextest-0 -- df | grep data +kubectl exec -n ${NS} vxflextest-0 -- mount | grep data diff --git a/test/helm/verify_leader_election.sh b/test/helm/verify_leader_election.sh new file mode 100755 index 00000000..88f8d604 --- /dev/null +++ b/test/helm/verify_leader_election.sh @@ -0,0 +1,240 @@ +#!/bin/bash + +# To run this test, you must: +# Not have any other tests currently running on cluster(This test will bring down and revive worker nodes) +# Have driver installed with more than 1 controller pod +# This test will perform the following in order: +# Ensure that the pod with lease is accepting requests, while pod without lease isn't +# Kill driver container on pod with lease and check that a leader election is triggered +# Ssh into the lease holder pod's node and reboot it, triggering a leader election +# Ssh into the lease holder pod's node and bring down the network interface (specified by -i flag), forcing a lease transfer + + + +function print_help(){ + echo '''This script tests controller-HA support and leader election for a driver + Arguments: + --namespace namespace driver is deployed in, for example: --namespace vxflexos + --interface network interface to bring down during network fail test, for example: --interface ens192 + --time-down time to keep network interface down for during network fail test, in seconds. For example: --time-down 60 + --user user usually either "root" or "core". For example: --user root + --skip-docker set this flag if docker is not the container engine being used. Docker tests need to be skipped for now + --safe-mode set this flag to skip reboot step + + for upstream k8s: + ./verify_leader_election.sh --namespace vxflexos --interface ens192 --time-down 120 --user root + will test the vxflexos driver leader election, and for the network fail test, interface ens192, while using root user + + for openshift: + ./verify_leader_election.sh --namespace test-vxflexos --interface ens192 --time-down 120 --user core --skip-docker + will test the driver leader election using core user, and will skip the docker tests since podman is specified + will be brought down for 60 seconds + ''' + +} + + +# This method gets the current holder of the lease, the node the holder of the lease is on, and the number of times Leader Election has +# been called. These values change frequently throughout the tests. +function get_holder() { + echo + holder=$(kubectl get lease -n $NAMESPACE | awk '{print $2}' | sed -n '2 p') + node=$(kubectl get pods -n $NAMESPACE -o wide | grep $holder | awk '{print $7}') + old_count=$(kubectl describe lease -n $NAMESPACE csi-vxflexos-dellemc-com | grep LeaderElection | wc -l) + echo Holder of lease is: $holder on node: $node +} + +# This method checks the driver log for a specified string, and fails/passes depending if the string +# was supposed to be found or not +# $1-pod to check, $2-expression to grep for, $3-0 if grep should fail, 1 if grep should pass +function check_driver_logs() { + holder_lease=$(kubectl logs -n $NAMESPACE $1 driver | grep "${2}" | wc -l) + if [ $holder_lease -ge 1 ] && [ $3 -eq 1 ]; then + echo \""${2}"\" found in driver logs for pod $1 + echo + echo kubectl logs -n $NAMESPACE $1 driver: + kubectl logs -n $NAMESPACE $1 driver | grep "${2}" + echo + elif [ $holder_lease -ge 1 ] && [ $3 -eq 0 ]; then + echo ERROR: \""${2}"\" should not have been in driver logs for pod $1, but was found + echo + echo kubectl logs -n $NAMESPACE $1 driver: + kubectl logs -n $NAMESPACE $1 driver + echo + exit 1 + elif [ $holder_lease -eq 0 ] && [ $3 -eq 0 ]; then + echo \""${2}"\" not found in driver logs for pod $1 + echo + echo kubectl logs -n $NAMESPACE $1 driver: + kubectl logs -n $NAMESPACE $1 driver + echo + else + echo ERROR: \""${2}"\" should have been in driver logs for pod $1, but was not found + echo + echo kubectl logs -n $NAMESPACE $1 driver: + kubectl logs -n $NAMESPACE $1 driver + echo + exit 1 + fi + echo +} + +function check_leases() { + echo + echo "Current State of Leases: " + kubectl get lease -n $NAMESPACE + echo +} + +function send() { + echo + echo sshpass -p dangerous ssh -o StrictHostKeyChecking=no "$USER@${1}" "sudo ${2}" + sshpass -p dangerous ssh -o StrictHostKeyChecking=no "$USER@${1}" "sudo ${2}" +} + +#1 holder of lease +#ensure lease was transfered +function wait_transfer_lease() { + new_holder=$(kubectl get lease -n $NAMESPACE | awk '{print $2}' | sed -n '2 p') + while [ $1 == $new_holder ]; do + sleep 20 + check_leases $1 + echo Transfering lease... + new_holder=$(kubectl get lease -n $NAMESPACE | awk '{print $2}' | sed -n '2 p') + done +} + +#ensure a leader election was triggered, $1 = count of leader elections before this method was called +function verify_leader_election() { + echo + iteration=0 + new_count=$(kubectl describe lease -n $NAMESPACE csi-vxflexos-dellemc-com | grep LeaderElection | wc -l) + while (($new_count == $1)); do + if (($iteration == 60)); then + echo "While loop took 60 iterations and leader election was not detected. Exiting..." + exit 1 + fi + echo Waiting for leader election to start... + sleep 15 + new_count=$(kubectl describe lease -n $NAMESPACE csi-vxflexos-dellemc-com | grep LeaderElection | wc -l) + iteration=$(($iteration +1)) + done + echo Event triggered leader election + kubectl describe lease -n $NAMESPACE csi-vxflexos-dellemc-com | grep LeaderElection | tail -1 + echo +} + +while getopts ":h-:" optchar; do + case "${optchar}" in + -) + case "${OPTARG}" in + namespace) + NAMESPACE="${!OPTIND}" + OPTIND=$((OPTIND + 1)) + ;; + interface) + NET_INT="${!OPTIND}" + OPTIND=$((OPTIND + 1)) + ;; + time-down) + TIME_DOWN="${!OPTIND}" + OPTIND=$((OPTIND + 1)) + ;; + user) + USER="${!OPTIND}" + OPTIND=$((OPTIND + 1)) + ;; + skip-docker) + SKIPD="true" + ;; + safe-mode) + SAFE="true" + ;; + *) + echo "Unknown option -${OPTARG}" + echo "For help, run -h" + exit 1 + ;; + esac + ;; + + h) + print_help + exit 0 + ;; + *) + echo "Unknown option -${OPTARG}" + echo "For help, run -h" + exit 1 + ;; + + esac +done + +# Check current status of driver and leases +kubectl get pods -n $NAMESPACE -o wide +sleep 5 +check_leases +get_holder +not_holder=$(kubectl get pods -n $NAMESPACE | grep controller | awk '{print $1}' | grep -v $holder | sed -n '1 p') +echo + +# Check if Holder of lease acquired the lease, and a pod that doesn't hold the lease, hasn't acquired it +echo Ensure only pod with lease is accepting requests +check_driver_logs $holder "successfully acquired lease" 1 + +#docker only tests, will need to figure out how to translate to podman +if [[ $SKIPD != "true" ]]; then + # Shutdown lease holder's provisioner container, ensure a leader election is triggered + echo Will ssh into node: $node to kill controller pod\'s provisioner container, to verify this triggers a leader election + echo Since kubelet will restart the pod, we cannot check for lease transfer, since the controller pod may regain the lease + send $node "docker kill \$(docker ps -f \"name=k8s_provisioner_${holder}\" | awk '{print \$1}'| sed -n '2 p')" + verify_leader_election $old_count + + # Shutdown lease holder's driver container, ensure a leader election is triggered + get_holder + echo Will ssh into node: $node to kill controller pod\'s driver container, to verify this triggers a leader election + echo Since kubelet will restart the pod, we cannot check for lease transfer, since the controller pod may regain the lease + send $node "docker kill \$(docker ps -f \"name=k8s_driver_${holder}\" | awk '{print \$1}'| sed -n '2 p')" + verify_leader_election $old_count +fi + +if [[ $SAFE != "true" ]]; then + # Reboot node that the controller pod with lease is assigned to, ensure this triggers a leader election + get_holder + echo worker node with pod with lease will be rebooted, to trigger a leader election + echo ssh into node: $node for reboot + send $node "reboot" + verify_leader_election $old_count + check_leases + echo Ensure leader election triggered + get_holder + check_driver_logs $holder "successfully acquired lease" 1 + kubectl get pods -n $NAMESPACE -o wide + echo +else + echo "Skipping reboot, due to --safe option used" +fi + +#Bring down network interface of node that the controller pod with lease is assigned to, verify this triggers a leader election +echo Will now bring down network interface on worker node, to verify leader election triggered +echo ssh into node: $node to bring down network interface: ${NET_INT} for ${TIME_DOWN} seconds +echo This will take a few minutes... +sleep 5 +TIME_OUT=$((${TIME_DOWN}+30)) +echo timeout ${TIME_OUT} sshpass -p dangerous ssh -o StrictHostKeyChecking=no $USER@$node "sudo ifconfig ${NET_INT} down && sudo sleep ${TIME_DOWN} && sudo ifconfig ${NET_INT} up && sudo reboot" +timeout ${TIME_OUT} sshpass -p dangerous ssh -o StrictHostKeyChecking=no $USER@$node "sudo ifconfig ${NET_INT} down && sudo sleep ${TIME_DOWN} && sudo ifconfig ${NET_INT} up && sudo reboot" +sleep 60 +verify_leader_election $old_count +check_leases +echo Ensure lease is transfered +get_holder +check_driver_logs $holder "successfully acquired lease" 1 + +#check status of cluster, if test was able to complete these steps, it passed +kubectl get pods -n $NAMESPACE -o wide +echo +echo TEST PASSED +echo +echo Note: pods may not be fully recovered from this test. They should be back in a few minutes +echo diff --git a/test/helm/volumeclonetest.sh b/test/helm/volumeclonetest.sh new file mode 100644 index 00000000..4bf227f8 --- /dev/null +++ b/test/helm/volumeclonetest.sh @@ -0,0 +1,53 @@ +#!/bin/bash +NS=helmtest-vxflexos +source ./common.bash + +echo "installing a 2 volume container" +bash starttest.sh 2vols +echo "done installing a 2 volume container" +echo "marking volume" +kubectl exec -n $NS vxflextest-0 -- touch /data0/orig +kubectl exec -n $NS vxflextest-0 -- ls -l /data0 +kubectl exec -n $NS vxflextest-0 -- sync +kubectl exec -n $NS vxflextest-0 -- sync + +echo "Calculating checksum of /data0/orig" +data0checksum=$(kubectl exec vxflextest-0 -n $NS -- md5sum /data0/orig) +echo $data0checksum + +echo "updating container to add a volume cloned from another volume" +helm upgrade -n $NS 2vols 2vols+clone +echo "waiting for container to upgrade/stabalize" +sleep 20 +waitOnRunning + +kubectl describe pods -n $NS +kubectl exec -n $NS vxflextest-0 -- df | grep data +kubectl exec -n $NS vxflextest-0 -- mount | grep data +echo "updating container finished" +echo "marking volume" +kubectl exec -n $NS vxflextest-0 -- touch /data2/new +echo "listing /data0" +kubectl exec -n $NS vxflextest-0 -- ls -l /data0 +echo "listing /data2" +kubectl exec -n $NS vxflextest-0 -- ls -l /data2 + +echo "Calculating checksum of the cloned file(/data2/orig)" +data2checksum=$(kubectl exec vxflextest-0 -n $NS -- md5sum /data2/orig) +echo $data2checksum +echo "Comparing checksums" +echo $data0checksum +echo $data2checksum +data0chs=$(echo $data0checksum | awk '{print $1}') +data2chs=$(echo $data2checksum | awk '{print $1}') +if [ "$data0chs" = "$data2chs" ]; then + echo "Both the checksums match!!!" +else + echo "Checksums don't match" +fi + +sleep 5 + +echo "deleting container" +bash stoptest.sh 2vols +sleep 5 diff --git a/test/integration/features/integration.feature b/test/integration/features/integration.feature index a7e75ad5..2990e9f8 100644 --- a/test/integration/features/integration.feature +++ b/test/integration/features/integration.feature @@ -40,7 +40,7 @@ Feature: VxFlex OS CSI interface And when I call DeleteVolume Then there are no errors -@long + @long Scenario Outline: Create volume, create snapshot, delete snapshot, delete volume for multiple sizes Given a VxFlexOS service And a capability with voltype "block" access "single-writer" fstype "xfs" @@ -63,11 +63,11 @@ Feature: VxFlex OS CSI interface And there are no errors Examples: - | size | - | "8" | - | "16" | - | "32" | - | "64" | + | size | + | "8" | + | "16" | + | "32" | + | "64" | Scenario: Create volume, create snapshot, create volume from snapshot, delete original volume, delete new volume Given a VxFlexOS service @@ -91,6 +91,22 @@ Feature: VxFlex OS CSI interface And there are no errors And I call ListVolume + Scenario: Craete volume, clone volume, delete original volume, delete new volume + Given a VxFlexOS service + And a basic block volume request "integration1" "8" + When I call CreateVolume + And I call CloneVolume + And there are no errors + And I call ListVolume + And a valid ListVolumeResponse is returned + And I call ListSnapshot + And a valid ListSnapshotResponse is returned + And when I call DeleteVolume + And there are no errors + And when I call DeleteAllVolumes + And there are no errors + And I call ListVolume + Scenario: Create volume, create snapshot, create many volumes from snap, delete original volume, delete new volumes Given a VxFlexOS service And a basic block volume request "integration1" "8" @@ -103,7 +119,17 @@ Feature: VxFlex OS CSI interface And I call DeleteSnapshot And when I call DeleteVolume And when I call DeleteAllVolumes - + + Scenario: Craete volume, clone volume, clone many volumes, delete original volume, delete new volumes + Given a VxFlexOS service + And a basic block volume request "integration1" "8" + When I call CreateVolume + And I call CloneVolume + And there are no errors + And I call CloneManyVolumes + Then the error message should contain "There are too many snapshots in the VTree" + And when I call DeleteVolume + And when I call DeleteAllVolumes Scenario: Create volume, idempotent create snapshot, delete volume Given a VxFlexOS service @@ -135,7 +161,6 @@ Feature: VxFlex OS CSI interface And when I call DeleteAllVolumes And there are no errors -@xwip Scenario Outline: Create publish, node-publish, node-unpublish, unpublish, and delete basic volume Given a VxFlexOS service And a capability with voltype access fstype @@ -152,55 +177,55 @@ Feature: VxFlex OS CSI interface Then the error message should contain Examples: - | voltype | access | fstype | errormsg | - | "mount" | "single-writer" | "xfs" | "none" | - | "mount" | "single-writer" | "ext4" | "none" | - | "mount" | "multi-writer" | "ext4" | "multi-writer not allowed" | - | "block" | "single-writer" | "none" | "none" | - | "block" | "multi-writer" | "none" | "none" | - | "block" | "single-writer" | "none" | "none" | + | voltype | access | fstype | errormsg | + | "mount" | "single-writer" | "xfs" | "none" | + | "mount" | "single-writer" | "ext4" | "none" | + | "mount" | "multi-writer" | "ext4" | "multi-writer not allowed" | + | "block" | "single-writer" | "none" | "none" | + | "block" | "multi-writer" | "none" | "none" | + | "block" | "single-writer" | "none" | "none" | Scenario: Create volume with access mode read only many - Given a VxFlexOS service - And a capability with voltype "mount" access "single-writer" fstype "xfs" - And a volume request "multi-reader-test" "8" - When I call CreateVolume - And there are no errors - And when I call PublishVolume "SDC_GUID" - And when I call NodePublishVolume "SDC_GUID" - And when I call NodeUnpublishVolume "SDC_GUID" - And when I call UnpublishVolume "SDC_GUID" - And a capability with voltype "mount" access "multi-reader" fstype "xfs" - And when I call PublishVolume "SDC_GUID" - And when I call NodePublishVolumeWithPoint "SDC_GUID" "temp1" - And when I call NodePublishVolumeWithPoint "SDC_GUID" "temp2" - And when I call NodeUnpublishVolumeWithPoint "SDC_GUID" "temp1" - And when I call NodeUnpublishVolumeWithPoint "SDC_GUID" "temp2" - And when I call UnpublishVolume "SDC_GUID" - And when I call DeleteVolume - Then there are no errors + Given a VxFlexOS service + And a capability with voltype "mount" access "single-writer" fstype "xfs" + And a volume request "multi-reader-test" "8" + When I call CreateVolume + And there are no errors + And when I call PublishVolume "SDC_GUID" + And when I call NodePublishVolume "SDC_GUID" + And when I call NodeUnpublishVolume "SDC_GUID" + And when I call UnpublishVolume "SDC_GUID" + And a capability with voltype "mount" access "multi-reader" fstype "xfs" + And when I call PublishVolume "SDC_GUID" + And when I call NodePublishVolumeWithPoint "SDC_GUID" "temp1" + And when I call NodePublishVolumeWithPoint "SDC_GUID" "temp2" + And when I call NodeUnpublishVolumeWithPoint "SDC_GUID" "temp1" + And when I call NodeUnpublishVolumeWithPoint "SDC_GUID" "temp2" + And when I call UnpublishVolume "SDC_GUID" + And when I call DeleteVolume + Then there are no errors Scenario: Create block volume with access mode read write many - Given a VxFlexOS service - And a capability with voltype "block" access "multi-writer" fstype "" - And a volume request "block-multi-writer-test" "8" - When I call CreateVolume - And there are no errors - And when I call PublishVolume "SDC_GUID" - And when I call PublishVolume "ALT_GUID" - And when I call NodePublishVolumeWithPoint "SDC_GUID" "/tmp/tempdev1" - And there are no errors - And when I call NodePublishVolumeWithPoint "SDC_GUID" "/tmp/tempdev2" - And there are no errors - And when I call NodePublishVolume "ALT_GUID" - And there are no errors - And when I call NodeUnpublishVolume "ALT_GUID" - And when I call NodeUnpublishVolumeWithPoint "SDC_GUID" "/tmp/tempdev1" - And when I call NodeUnpublishVolumeWithPoint "SDC_GUID" "/tmp/tempdev2" - And when I call UnpublishVolume "SDC_GUID" - And when I call UnpublishVolume "ALT_GUID" - And when I call DeleteVolume - Then there are no errors + Given a VxFlexOS service + And a capability with voltype "block" access "multi-writer" fstype "" + And a volume request "block-multi-writer-test" "8" + When I call CreateVolume + And there are no errors + And when I call PublishVolume "SDC_GUID" + And when I call PublishVolume "ALT_GUID" + And when I call NodePublishVolumeWithPoint "SDC_GUID" "/tmp/tempdev1" + And there are no errors + And when I call NodePublishVolumeWithPoint "SDC_GUID" "/tmp/tempdev2" + And there are no errors + And when I call NodePublishVolume "ALT_GUID" + And there are no errors + And when I call NodeUnpublishVolume "ALT_GUID" + And when I call NodeUnpublishVolumeWithPoint "SDC_GUID" "/tmp/tempdev1" + And when I call NodeUnpublishVolumeWithPoint "SDC_GUID" "/tmp/tempdev2" + And when I call UnpublishVolume "SDC_GUID" + And when I call UnpublishVolume "ALT_GUID" + And when I call DeleteVolume + Then there are no errors Scenario: Create publish, unpublish, and delete basic volume Given a VxFlexOS service @@ -263,10 +288,10 @@ Feature: VxFlex OS CSI interface Then there are no errors Examples: - | numberOfVolumes | - | 1 | - | 2 | - | 5 | + | numberOfVolumes | + | 1 | + | 2 | + | 5 | Scenario Outline: Idempotent create volumes, publish, node publish, node unpublish, unpublish, delete volumes in parallel Given a VxFlexOS service @@ -294,14 +319,11 @@ Feature: VxFlex OS CSI interface And there are no errors And when I delete volumes in parallel Then there are no errors - - Examples: - | numberOfVolumes | - | 1 | - | 10 | - - + Examples: + | numberOfVolumes | + | 1 | + | 10 | Scenario: Expand Volume Mount Given a VxFlexOS service @@ -324,8 +346,7 @@ Feature: VxFlex OS CSI interface And when I call UnpublishVolume "SDC_GUID" And there are no errors And when I call DeleteVolume - Then there are no errors - + Then there are no errors Scenario: Expand Volume Block Given a VxFlexOS service @@ -348,9 +369,4 @@ Feature: VxFlex OS CSI interface And when I call UnpublishVolume "SDC_GUID" And there are no errors And when I call DeleteVolume - Then there are no errors - - | numberOfVolumes | - | 1 | - | 10 | - | 20 | + Then there are no errors diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go index 7dce8e6e..e71ecc96 100644 --- a/test/integration/integration_test.go +++ b/test/integration/integration_test.go @@ -3,6 +3,11 @@ package integration_test import ( "context" "fmt" + "net" + "os" + "testing" + "time" + "github.com/DATA-DOG/godog" "github.com/akutz/memconn" csi "github.com/container-storage-interface/spec/lib/go/csi" @@ -10,10 +15,6 @@ import ( "github.com/rexray/gocsi/utils" "github.com/stretchr/testify/assert" "google.golang.org/grpc" - "net" - "os" - "testing" - "time" ) const ( @@ -55,7 +56,7 @@ func TestMain(m *testing.M) { }, godog.Options{ Format: "pretty", Paths: []string{"features"}, - //Tags: "wip", + // Tags: "wip", }) if st := m.Run(); st > exitVal { exitVal = st diff --git a/test/integration/pool.yml b/test/integration/pool.yml index 30a7b8d0..dbaf48d5 100644 --- a/test/integration/pool.yml +++ b/test/integration/pool.yml @@ -1 +1 @@ -storagepool: viki_pool_HDD_20181031 +storagepool: pool1 diff --git a/test/integration/step_defs_test.go b/test/integration/step_defs_test.go index 4f22b8c4..b36799d9 100644 --- a/test/integration/step_defs_test.go +++ b/test/integration/step_defs_test.go @@ -4,14 +4,15 @@ import ( "context" "errors" "fmt" - "github.com/DATA-DOG/godog" - csi "github.com/container-storage-interface/spec/lib/go/csi" - ptypes "github.com/golang/protobuf/ptypes" "os" "os/exec" "strings" "syscall" "time" + + "github.com/DATA-DOG/godog" + csi "github.com/container-storage-interface/spec/lib/go/csi" + ptypes "github.com/golang/protobuf/ptypes" ) const ( @@ -64,6 +65,7 @@ func (f *feature) aBasicBlockVolumeRequest(name string, size int64) error { params["storagepool"] = os.Getenv("STORAGE_POOL") params["thickprovisioning"] = "false" req.Parameters = params + makeAUniqueName(&name) req.Name = name capacityRange := new(csi.CapacityRange) capacityRange.RequiredBytes = size * 1024 * 1024 * 1024 @@ -193,6 +195,7 @@ func (f *feature) getMountVolumeRequest(name string) *csi.CreateVolumeRequest { params := make(map[string]string) params["storagepool"] = os.Getenv("STORAGE_POOL") req.Parameters = params + makeAUniqueName(&name) req.Name = name capacityRange := new(csi.CapacityRange) capacityRange.RequiredBytes = 8 * 1024 * 1024 * 1024 @@ -315,6 +318,7 @@ func (f *feature) aVolumeRequest(name string, size int64) error { params["storagepool"] = os.Getenv("STORAGE_POOL") params["thickprovisioning"] = "true" req.Parameters = params + makeAUniqueName(&name) req.Name = name capacityRange := new(csi.CapacityRange) capacityRange.RequiredBytes = size * 1024 * 1024 * 1024 @@ -566,6 +570,18 @@ func (f *feature) iCallCreateVolumeFromSnapshot() error { return nil } +func (f *feature) iCallCloneVolume() error { + req := f.createVolumeRequest + req.Name = "cloneVol-" + req.Name + source := &csi.VolumeContentSource_VolumeSource{VolumeId: f.volID} + req.VolumeContentSource = new(csi.VolumeContentSource) + req.VolumeContentSource.Type = &csi.VolumeContentSource_Volume{Volume: source} + fmt.Printf("Calling Clone Volume\n") + _ = f.createAVolume(req, "single CloneVolume") + time.Sleep(SleepTime) + return nil +} + func (f *feature) createAVolume(req *csi.CreateVolumeRequest, voltype string) error { ctx := context.Background() client := csi.NewControllerClient(grpcClient) @@ -585,6 +601,7 @@ func (f *feature) iCallCreateManyVolumesFromSnapshot() error { for i := 1; i <= 130; i++ { req := f.createVolumeRequest req.Name = fmt.Sprintf("volFromSnap%d", i) + makeAUniqueName(&req.Name) source := &csi.VolumeContentSource_SnapshotSource{SnapshotId: f.snapshotID} req.VolumeContentSource = new(csi.VolumeContentSource) req.VolumeContentSource.Type = &csi.VolumeContentSource_Snapshot{Snapshot: source} @@ -598,6 +615,23 @@ func (f *feature) iCallCreateManyVolumesFromSnapshot() error { return nil } +func (f *feature) iCallCloneManyVolumes() error { + for i := 1; i <= 130; i++ { + req := f.createVolumeRequest + req.Name = fmt.Sprintf("cloneVol%d", i) + source := &csi.VolumeContentSource_VolumeSource{VolumeId: f.volID} + req.VolumeContentSource = new(csi.VolumeContentSource) + req.VolumeContentSource.Type = &csi.VolumeContentSource_Volume{Volume: source} + fmt.Printf("Calling Clone Volume\n") + err := f.createAVolume(req, "single CloneVolume") + if err != nil { + fmt.Printf("Error on the %d th volume: %s\n", i, err.Error()) + break + } + } + return nil +} + func (f *feature) iCallListVolume() error { var err error ctx := context.Background() @@ -1031,6 +1065,27 @@ func (f *feature) nodeExpandVolume(volID, volPath string) error { return err } +//add given suffix to name or use time as suffix and set to max of 30 characters +func makeAUniqueName(name *string) { + if name == nil { + temp := "tmp" + name = &temp + } + var suffix = os.Getenv("VOL_NAME_SUFFIX") + if len(suffix) == 0 { + now := time.Now() + suffix = fmt.Sprintf("%02d%02d%02d", now.Hour(), now.Minute(), now.Second()) + *name += "_" + suffix + } else { + *name += "_" + suffix + } + tmp := *name + if len(tmp) > 30 { + *name = tmp[len(tmp)-30:] + } + return +} + func FeatureContext(s *godog.Suite) { f := &feature{} s.Step(`^a VxFlexOS service$`, f.aVxFlexOSService) @@ -1072,4 +1127,6 @@ func FeatureContext(s *godog.Suite) { s.Step(`^I write block data$`, f.iWriteBlockData) s.Step(`^when I call ExpandVolume to "([^"]*)"$`, f.whenICallExpandVolumeTo) s.Step(`^when I call NodeExpandVolume$`, f.whenICallNodeExpandVolume) + s.Step(`^I call CloneVolume$`, f.iCallCloneVolume) + s.Step(`^I call CloneManyVolumes$`, f.iCallCloneManyVolumes) } diff --git a/test/sanity/README.md b/test/sanity/README.md index e8517bf5..3504d4be 100644 --- a/test/sanity/README.md +++ b/test/sanity/README.md @@ -1,19 +1,20 @@ -# Sanity Script Test +# Kubernetes Sanity Script Test This test runs the Kubernetes sanity test at https://github.com/kubernetes-csi/csi-test. -The version was v2.2.0 +The version was v3.1.0 To run the test: - 1. "go get github.com/kubernetes-csi/csi-test" - 2. "cd [path to csi-sanity]" - 3. "make clean install" - 4. Make sure env.sh(in csi-vxflexos) is up to date - 5. edit secrets.yaml and volParams.yaml to have the correct storage pool parameter - 6. Build CSI-vxflexos ("make clean build" in top level of directory) - 7.Run start_driver.sh - 8.Open up another window and bring it to csi-vxflexos/test/sanity - 9.Use run.sh to run the tests once + 1. run `git clone https://github.com/kubernetes-csi/csi-test.git` + 2. Check out tag v3.1.0 + 3. Cd to csi-test/cmd/csi-sanity + 4. run `make clean install` + 5. Make sure env.sh(in csi-vxflexos) is up to date + 6. Cd to csi-vxflexos/test/sanity and edit secrets.yaml and volParams.yaml to have the correct storage pool parameter + 7. Build csi-vxflexos ("make clean build" in csi-vxflexos) + 8. Run start_driver.sh + 9. Open up another window and bring it to csi-vxflexos/test/sanity + 10. Use run.sh to run the tests once Use debug_help.sh to run an sh file x amount of times debug_help.sh run.sh 100 30 runs run.sh 100 times, with thirty second breaks inbetween each run This is useful for catching non-consitent errors. A 30 second wait time is a good idea when running diff --git a/test/sanity/run.sh b/test/sanity/run.sh index d3391185..2f2e1734 100644 --- a/test/sanity/run.sh +++ b/test/sanity/run.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash rm -rf /tmp/csi-staging rm -rf /tmp/csi-mount csi-sanity --ginkgo.v --csi.endpoint=/root/csi-vxflexos/test/sanity/unix_sock --csi.testvolumeexpandsize 25769803776 --csi.testvolumesize 17179869184 --csi.testvolumeparameters=volParams.yaml --csi.secrets=secrets.yaml --ginkgo.skip "pagination should detect volumes added between pages and accept tokens when the last volume from a page is deleted|check the presence of new volumes and absence of deleted ones in the volume list|should fail when the volume is missing" diff --git a/test/sanity/secrets.yaml b/test/sanity/secrets.yaml index cd7ae6b2..9ffa2fb5 100644 --- a/test/sanity/secrets.yaml +++ b/test/sanity/secrets.yaml @@ -1,20 +1,2 @@ CreateVolumeSecret: - secretKey: secretval1 - storagepool: "viki_pool_HDD_20181031" - SYMID: "000197900046" - ServiceLevel: Optimized - SRP: SRP_1 - ApplicationPrefix: sanity -DeleteVolumeSecret: - secretKey: secretval2 -ControllerPublishVolumeSecret: - secretKey: secretval3 -ControllerUnpublishVolumeSecret: - secretKey: secretval4 -NodeStageVolumeSecret: - secretKey: secretval5 -NodePublishVolumeSecret: - secretKey: secretval6 -ControllerValidateVolumeCapabilitiesSecret: - secretKey: secretval7 - + storagepool: "pool1" diff --git a/test/sanity/start_driver.sh b/test/sanity/start_driver.sh index e03eb009..47c7eb1b 100644 --- a/test/sanity/start_driver.sh +++ b/test/sanity/start_driver.sh @@ -1,6 +1,6 @@ -s will run coverage analysis using the integration testing. -# The env.sh must point to a valid Unisphere deployment and the iscsi packages must be installed -# on this system. This will make real calls to Unisphere +#!/bin/bash +# The env.sh must point to a valid PowerFlex deployment and the iscsi packages must be installed +# on this system. This will make real calls to PowerFlex rm -f unix_sock . ../../env.sh diff --git a/test/sanity/volParams.yaml b/test/sanity/volParams.yaml index 30a7b8d0..dbaf48d5 100644 --- a/test/sanity/volParams.yaml +++ b/test/sanity/volParams.yaml @@ -1 +1 @@ -storagepool: viki_pool_HDD_20181031 +storagepool: pool1