├── .github ├── CODEOWNERS ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── common-workflows.yaml │ ├── go-version.yaml │ ├── image-version-update.yaml │ ├── release.yaml │ ├── update-libraries-to-commits.yaml │ └── update-libraries.yaml ├── .gitignore ├── .trivyignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── common └── constants.go ├── core ├── .gitignore ├── core.go ├── semver.tpl └── semver │ ├── semver.go │ └── semver_test.go ├── dell-csi-helm-installer ├── .gitignore ├── README.md ├── common.sh ├── csi-install.sh ├── csi-offline-bundle.md ├── csi-offline-bundle.sh ├── csi-uninstall.sh ├── verify-csi-unity.sh └── verify.sh ├── docker.mk ├── env.sh ├── go.mod ├── go.sum ├── hooks └── pre-commit ├── k8sutils ├── k8sutils.go └── k8sutils_test.go ├── licenses └── LICENSE ├── main.go ├── main_test.go ├── provider ├── provider.go └── provider_test.go ├── samples ├── secret │ ├── emptysecret.yaml │ └── secret.yaml ├── storageclass │ ├── unity-fc.yaml │ ├── unity-iscsi.yaml │ ├── unity-nfs.yaml │ └── unity-plain.yaml └── volumesnapshotclass │ └── snapclass-v1.yaml ├── scripts ├── check.sh └── run.sh ├── service ├── controller.go ├── controller_test.go ├── csi_extension_server.go ├── csi_extension_test.go ├── envvars.go ├── identity.go ├── identity_test.go ├── main_test.go ├── mount.go ├── mount_test.go ├── node.go ├── node_test.go ├── service.go ├── service_test.go ├── utils │ ├── emcutils.go │ ├── emcutils_test.go │ ├── logging.go │ └── logging_test.go ├── validator.go └── validator_test.go └── test ├── bdd-test ├── bdd_main_test.go ├── bdd_test.go ├── c.out ├── features │ └── bdd.feature └── run.sh ├── integration-test ├── features │ └── integration.feature ├── integration_main_test.go ├── integration_test.go └── run.sh ├── sample.yaml ├── sanity ├── README.md ├── params.yaml ├── run.sh ├── secrets.yaml └── start_driver.sh └── scale-test ├── 100volumes ├── Chart.yaml ├── templates │ └── test.yaml └── values.yaml ├── 10volumes ├── Chart.yaml ├── templates │ └── test.yaml └── values.yaml ├── 20volumes ├── Chart.yaml ├── templates │ └── test.yaml └── values.yaml ├── 2volumes ├── Chart.yaml ├── templates │ └── test.yaml └── values.yaml ├── 30volumes ├── Chart.yaml ├── templates │ └── test.yaml └── values.yaml ├── 50volumes ├── Chart.yaml ├── templates │ └── test.yaml └── values.yaml ├── README.md ├── rescaletest.sh ├── scaletest.sh └── serviceAccount.yaml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS 2 | # 3 | # Documentation for this file can be found at: 4 | # https://help.github.com/en/articles/about-code-owners 5 | # These are the default owners for the code and will 6 | # be requested for review when someone opens a pull request. 7 | # Order is alphabetical for easier maintenance. 8 | # 9 | # Aaron Tye (atye) 10 | # Christian Coffield (ChristianAtDell) 11 | # Keerthi Bandapati (bandak2) 12 | # Deepak Ghivari (Deepak-Ghivari) 13 | # Evgeny Uglov (EvgenyUglov) 14 | # Fernando Alfaro Campos (falfaroc) 15 | # Francis Nijay (francis-nijay) 16 | # Karthik K (karthikk92) 17 | # Spandita Panigrahi (panigs7) 18 | # Rajendra Indukuri (rajendraindukuri) 19 | # Santhosh Lakshmanan (santhoshatdell) 20 | # Shayna Finocchiaro (shaynafinocchiaro) 21 | 22 | # for all files: 23 | * @atye @ChristianAtDell @bandak2 @Deepak-Ghivari @EvgenyUglov @falfaroc @francis-nijay @karthikk92 @panigs7 @rajendraindukuri @santhoshatdell @shaynafinocchiaro 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # To get started with Dependabot version updates, you'll need to specify which 3 | # package ecosystems to update and where the package manifests are located. 4 | # Please see the documentation for all configuration options: 5 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 6 | 7 | version: 2 8 | updates: 9 | # Schedule for go module updates 10 | - package-ecosystem: "gomod" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | day: "sunday" 15 | time: "18:00" 16 | allow: 17 | # Allow direct updates for packages 18 | - dependency-type: direct 19 | ignore: 20 | - dependency-name: "*" 21 | update-types: 22 | - version-update:semver-patch 23 | # a group of dependencies will be updated together in one pull request 24 | groups: 25 | golang: 26 | # group all semantic versioning levels together in one pull request 27 | update-types: 28 | - major 29 | - minor 30 | patterns: 31 | - "*" 32 | 33 | # github actions 34 | - package-ecosystem: "github-actions" 35 | directory: "/" 36 | schedule: 37 | # Check for updates to GitHub Actions every week 38 | interval: "weekly" 39 | day: "saturday" 40 | groups: 41 | github-actions: 42 | patterns: 43 | - "*" 44 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | A few sentences describing the overall goals of the pull request's commits. 3 | 4 | # GitHub Issues 5 | List the GitHub issues impacted by this PR: 6 | 7 | | GitHub Issue # | 8 | | -------------- | 9 | | | 10 | 11 | # Checklist: 12 | 13 | - [ ] I have performed a self-review of my own code to ensure there are no formatting, vetting, linting, or security issues 14 | - [ ] I have verified that new and existing unit tests pass locally with my changes 15 | - [ ] I have not allowed coverage numbers to degenerate 16 | - [ ] I have maintained at least 90% code coverage 17 | - [ ] I have commented my code, particularly in hard-to-understand areas 18 | - [ ] I have made corresponding changes to the documentation 19 | - [ ] I have added tests that prove my fix is effective or that my feature works 20 | - [ ] Backward compatibility is not broken 21 | 22 | # How Has This Been Tested? 23 | Please describe the tests that you ran to verify your changes. Please also list any relevant details for your test configuration 24 | 25 | - [ ] Test A 26 | - [ ] Test B 27 | -------------------------------------------------------------------------------- /.github/workflows/common-workflows.yaml: -------------------------------------------------------------------------------- 1 | name: Common Workflows 2 | on: # yamllint disable-line rule:truthy 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: ["**"] 7 | 8 | jobs: 9 | 10 | # golang static analysis checks 11 | go-static-analysis: 12 | uses: dell/common-github-actions/.github/workflows/go-static-analysis.yaml@main 13 | name: Golang Validation 14 | 15 | common: 16 | name: Quality Checks 17 | uses: dell/common-github-actions/.github/workflows/go-common.yml@main 18 | -------------------------------------------------------------------------------- /.github/workflows/go-version.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Dell Inc., or its subsidiaries. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Reusable workflow to perform go version update on Golang based projects 10 | name: Go Version Update 11 | 12 | on: # yamllint disable-line rule:truthy 13 | workflow_dispatch: 14 | repository_dispatch: 15 | types: [go-update-workflow] 16 | 17 | jobs: 18 | # go version update 19 | go-version-update: 20 | uses: dell/common-github-actions/.github/workflows/go-version-workflow.yaml@main 21 | name: Go Version Update 22 | secrets: inherit 23 | -------------------------------------------------------------------------------- /.github/workflows/image-version-update.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Dell Inc., or its subsidiaries. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Reusable workflow to perform image version update on Golang based projects 10 | name: Image Version Update 11 | 12 | on: # yamllint disable-line rule:truthy 13 | workflow_dispatch: 14 | inputs: 15 | version: 16 | description: "Version to release (major, minor, patch) Ex: minor" 17 | required: true 18 | repository_dispatch: 19 | types: [image-update-workflow] 20 | 21 | jobs: 22 | # image version update 23 | image-version-update: 24 | uses: dell/common-github-actions/.github/workflows/image-version-workflow.yaml@main 25 | with: 26 | version: "${{ github.event.inputs.version || 'minor' }}" 27 | secrets: inherit 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release CSI-Unity 2 | # Invocable as a reusable workflow 3 | # Can be manually triggered 4 | on: # yamllint disable-line rule:truthy 5 | workflow_call: 6 | workflow_dispatch: 7 | inputs: 8 | option: 9 | description: 'Select version to release' 10 | required: true 11 | type: choice 12 | default: 'minor' 13 | options: 14 | - major 15 | - minor 16 | - patch 17 | - n-1/n-2 patch (Provide input in the below box) 18 | version: 19 | description: "Patch version to release. example: 2.1.x (Use this only if n-1/n-2 patch is selected)" 20 | required: false 21 | type: string 22 | repository_dispatch: 23 | types: [auto-release-workflow] 24 | jobs: 25 | process-inputs: 26 | name: Process Inputs 27 | runs-on: ubuntu-latest 28 | outputs: 29 | processedVersion: ${{ steps.set-version.outputs.versionEnv }} 30 | steps: 31 | - name: Process input 32 | id: set-version 33 | shell: bash 34 | run: | 35 | echo "Triggered by: ${{ github.event_name }}" 36 | if [[ "${{ github.event_name }}" == "repository_dispatch" ]]; then 37 | echo "versionEnv=minor" >> $GITHUB_OUTPUT 38 | exit 0 39 | fi 40 | if [[ "${{ github.event.inputs.version }}" != "" && "${{ github.event.inputs.option }}" == "n-1/n-2 patch (Provide input in the below box)" ]]; then 41 | # if both version and option are provided, then version takes precedence i.e. patch release for n-1/n-2 42 | echo "versionEnv=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT 43 | exit 0 44 | fi 45 | 46 | if [[ "${{ github.event.inputs.option }}" != "n-1/n-2 patch (Provide input in the below box)" ]]; then 47 | # if only option is provided, then option takes precedence i.e. minor, major or patch release 48 | echo "versionEnv=${{ github.event.inputs.option }}" >> $GITHUB_OUTPUT 49 | exit 0 50 | fi 51 | # if neither option nor version is provided, then minor release is taken by default (Auto-release) 52 | echo "versionEnv=minor" >> $GITHUB_OUTPUT 53 | csm-release: 54 | needs: [process-inputs] 55 | uses: dell/common-github-actions/.github/workflows/csm-release-driver-module.yaml@main 56 | name: Release CSM Drivers and Modules 57 | with: 58 | version: "${{ needs.process-inputs.outputs.processedVersion }}" 59 | images: 'csi-unity' 60 | secrets: inherit 61 | -------------------------------------------------------------------------------- /.github/workflows/update-libraries-to-commits.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Dell Inc., or its subsidiaries. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Reusable workflow to perform updates of Dell client libraries to latest commits 10 | name: Dell Libraries Commit Update 11 | on: # yamllint disable-line rule:truthy 12 | workflow_dispatch: 13 | repository_dispatch: 14 | types: [latest-commits-libraries] 15 | 16 | jobs: 17 | package-update: 18 | uses: dell/common-github-actions/.github/workflows/update-libraries-to-commits.yml@main 19 | name: Dell Libraries Update 20 | secrets: inherit 21 | -------------------------------------------------------------------------------- /.github/workflows/update-libraries.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Dell Inc., or its subsidiaries. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Reusable workflow to perform updates of Dell client libraries 10 | name: Dell Libraries Release Update 11 | on: # yamllint disable-line rule:truthy 12 | workflow_dispatch: 13 | repository_dispatch: 14 | types: [latest-released-libraries] 15 | 16 | jobs: 17 | package-update: 18 | uses: dell/common-github-actions/.github/workflows/update-libraries.yml@main 19 | name: Dell Libraries Update 20 | secrets: inherit 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | builds/ 3 | .idea 4 | .idea/ 5 | .vscode/ 6 | *.exe 7 | vendor/ 8 | bin/ 9 | semver.mk 10 | go.sum 11 | ./csi-unity 12 | csi-unity.exe 13 | helm/myvalues.yaml 14 | test.properties 15 | -------------------------------------------------------------------------------- /.trivyignore: -------------------------------------------------------------------------------- 1 | CVE-2019-1010022 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GOIMAGE 2 | ARG BASEIMAGE 3 | ARG GOPROXY 4 | 5 | # Stage to build the driver 6 | FROM $GOIMAGE as builder 7 | RUN mkdir -p /go/src 8 | COPY ./ /go/src/csi-unity 9 | 10 | WORKDIR /go/src/csi-unity 11 | RUN mkdir -p bin 12 | RUN go generate 13 | RUN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o bin/csi-unity 14 | # Print the version 15 | RUN go run core/semver/semver.go -f mk 16 | 17 | 18 | # Dockerfile to build Unity CSI Driver 19 | # Fetching the base ubi micro image with the require packges committed using buildah 20 | FROM $BASEIMAGE as driver 21 | 22 | COPY --from=builder /go/src/csi-unity/bin/csi-unity / 23 | COPY scripts/run.sh / 24 | RUN chmod 777 /run.sh 25 | ENTRYPOINT ["/run.sh"] 26 | 27 | # final stage 28 | FROM driver as final 29 | 30 | LABEL vendor="Dell Technologies" \ 31 | maintainer="Dell Technologies" \ 32 | name="csi-unity" \ 33 | summary="CSI Driver for Dell Unity XT" \ 34 | description="CSI Driver for provisioning persistent storage from Dell Unity XT" \ 35 | release="1.14.0" \ 36 | version="2.14.0" \ 37 | license="Apache-2.0" 38 | COPY licenses /licenses 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME:=csi-unity 2 | 3 | .PHONY: all 4 | all: go-build 5 | 6 | ifneq (on,$(GO111MODULE)) 7 | export GO111MODULE := on 8 | endif 9 | 10 | IMAGE_NAME=csi-unity 11 | IMAGE_REGISTRY=dellemc 12 | DEFAULT_IMAGE_TAG=$(shell date +%Y%m%d%H%M%S) 13 | ifeq ($(IMAGETAG),) 14 | export IMAGETAG="$(DEFAULT_IMAGE_TAG)" 15 | endif 16 | 17 | .PHONY: go-vendor 18 | go-vendor: 19 | go mod vendor 20 | 21 | .PHONY: go-build 22 | go-build: clean 23 | git config core.hooksPath hooks 24 | rm -f core/core_generated.go 25 | cd core && go generate 26 | go build . 27 | 28 | # Only unit testing utils for now. More work to do but need to start somewhere. 29 | unit-test: 30 | ( cd service/utils; go clean -cache; go test -v -coverprofile=c.out ./... ) 31 | 32 | # Integration tests using Godog. Populate env.sh with the hardware parameters 33 | integration-test: 34 | ( cd test/integration-test; sh run.sh ) 35 | 36 | # BDD tests using Godog. Populate env.sh with the hardware parameters 37 | bdd-test: 38 | ( cd test/bdd-test; sh run.sh ) 39 | 40 | .PHONY: download-csm-common 41 | download-csm-common: 42 | curl -O -L https://raw.githubusercontent.com/dell/csm/main/config/csm-common.mk 43 | 44 | # 45 | # Docker-related tasks 46 | # 47 | # Generates the docker container (but does not push) 48 | podman-build: download-csm-common go-build 49 | $(eval include csm-common.mk) 50 | podman build --pull -t $(IMAGE_REGISTRY)/$(IMAGE_NAME):$(IMAGETAG) --build-arg GOIMAGE=$(DEFAULT_GOIMAGE) --build-arg BASEIMAGE=$(CSM_BASEIMAGE) --build-arg GOPROXY=$(GOPROXY) . --format=docker 51 | 52 | podman-build-no-cache: download-csm-common go-build 53 | $(eval include csm-common.mk) 54 | podman build --pull --no-cache -t $(IMAGE_REGISTRY)/$(IMAGE_NAME):$(IMAGETAG) --build-arg GOIMAGE=$(DEFAULT_GOIMAGE) --build-arg BASEIMAGE=$(CSM_BASEIMAGE) --build-arg GOPROXY=$(GOPROXY) . --format=docker 55 | 56 | podman-push: 57 | podman push $(IMAGE_REGISTRY)/$(IMAGE_NAME):$(IMAGETAG) 58 | 59 | # 60 | # Docker-related tasks 61 | # 62 | # Generates the docker container (but does not push) 63 | docker-build: download-csm-common 64 | make -f docker.mk docker-build 65 | 66 | docker-push: 67 | make -f docker.mk docker-push 68 | 69 | version: 70 | go generate 71 | go run core/semver/semver.go -f mk >semver.mk 72 | make -f docker.mk version 73 | 74 | .PHONY: clean 75 | clean: 76 | rm -f core/core_generated.go 77 | go clean 78 | 79 | # 80 | # Tests-related tasks 81 | .PHONY: integ-test 82 | integ-test: go-build 83 | go test -v ./test/... 84 | 85 | check: 86 | sh scripts/check.sh 87 | 88 | .PHONY: actions action-help 89 | actions: ## Run all GitHub Action checks that run on a pull request creation 90 | @echo "Running all GitHub Action checks for pull request events..." 91 | @act -l | grep -v ^Stage | grep pull_request | grep -v image_security_scan | awk '{print $$2}' | while read WF; do \ 92 | echo "Running workflow: $${WF}"; \ 93 | act pull_request --no-cache-server --platform ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest --job "$${WF}"; \ 94 | done 95 | 96 | action-help: ## Echo instructions to run one specific workflow locally 97 | @echo "GitHub Workflows can be run locally with the following command:" 98 | @echo "act pull_request --no-cache-server --platform ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest --job " 99 | @echo "" 100 | @echo "Where '' is a Job ID returned by the command:" 101 | @echo "act -l" 102 | @echo "" 103 | @echo "NOTE: if act is not installed, it can be downloaded from https://github.com/nektos/act" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 14 | # CSI Driver for Dell Unity XT 15 | 16 | [![Go Report Card](https://goreportcard.com/badge/github.com/dell/csi-unity?style=flat-square)](https://goreportcard.com/report/github.com/dell/csi-unity) 17 | [![License](https://img.shields.io/github/license/dell/csi-unity?style=flat-square&color=blue&label=License)](https://github.com/dell/csi-unity/blob/main/LICENSE) 18 | [![Docker](https://img.shields.io/docker/pulls/dellemc/csi-unity.svg?logo=docker&style=flat-square&label=Pulls)](https://hub.docker.com/r/dellemc/csi-unity) 19 | [![Last Release](https://img.shields.io/github/v/release/dell/csi-unity?label=Latest&style=flat-square&logo=go)](https://github.com/dell/csi-unity/releases) 20 | 21 | **Repository for CSI Driver for Dell Unity XT** 22 | 23 | ## Description 24 | CSI Driver for Unity XT is part of the [CSM (Container Storage Modules)](https://github.com/dell/csm) open-source suite of Kubernetes storage enablers for Dell products. CSI Driver for Unity XT is a Container Storage Interface (CSI) driver that provides support for provisioning persistent storage using Dell Unity XT storage array. 25 | 26 | This project may be compiled as a stand-alone binary using Golang that, when run, provides a valid CSI endpoint. It also can be used as a precompiled container image. 27 | 28 | ## Table of Contents 29 | 30 | * [Code of Conduct](https://github.com/dell/csm/blob/main/docs/CODE_OF_CONDUCT.md) 31 | * [Maintainer Guide](https://github.com/dell/csm/blob/main/docs/MAINTAINER_GUIDE.md) 32 | * [Committer Guide](https://github.com/dell/csm/blob/main/docs/COMMITTER_GUIDE.md) 33 | * [Contributing Guide](https://github.com/dell/csm/blob/main/docs/CONTRIBUTING.md) 34 | * [List of Adopters](https://github.com/dell/csm/blob/main/docs/ADOPTERS.md) 35 | * [Support](#support) 36 | * [Security](https://github.com/dell/csm/blob/main/docs/SECURITY.md) 37 | * [Building](#building) 38 | * [Runtime Dependecies](#runtime-dependencies) 39 | * [Documentation](#documentation) 40 | 41 | ## Support 42 | For any issues, questions or feedback, please contact [Dell support](https://www.dell.com/support/incidents-online/en-us/contactus/product/container-storage-modules). 43 | 44 | ## Building 45 | This project is a Go module (see golang.org Module information for explanation). 46 | The dependencies for this project are in the go.mod file. 47 | 48 | To build the source, execute `make go-build`. 49 | 50 | To run unit tests, execute `make unit-test`. 51 | 52 | To build a podman based image, execute `make podman-build`. 53 | 54 | You can run an integration test on a Linux system by populating the env files at `test/integration/` with values for your Dell Unity XT systems and then run `make integration-test`. 55 | 56 | 57 | ## Runtime Dependencies 58 | Both the Controller and the Node portions of the driver can only be run on nodes which have network connectivity to “`Unisphere for Unity XT`” (which is used by the driver). 59 | 60 | ## Documentation 61 | For more detailed information on the driver, please refer to [Container Storage Modules documentation](https://dell.github.io/csm-docs/). 62 | -------------------------------------------------------------------------------- /common/constants.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package constants 16 | 17 | const ( 18 | 19 | // ParamCSILogLevel csi driver log level 20 | ParamCSILogLevel = "CSI_LOG_LEVEL" 21 | 22 | // ParamAllowRWOMultiPodAccess to enable multi pod access for RWO volume 23 | ParamAllowRWOMultiPodAccess = "ALLOW_RWO_MULTIPOD_ACCESS" 24 | 25 | // ParamMaxUnityVolumesPerNode defines maximum number of unity volumes per node 26 | ParamMaxUnityVolumesPerNode = "MAX_UNITY_VOLUMES_PER_NODE" 27 | 28 | // ParamSyncNodeInfoTimeInterval defines time interval to sync node info 29 | ParamSyncNodeInfoTimeInterval = "SYNC_NODE_INFO_TIME_INTERVAL" 30 | 31 | // ParamTenantName defines tenant name to be set while adding host entry 32 | ParamTenantName = "TENANT_NAME" 33 | ) 34 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | core_generated.go 2 | -------------------------------------------------------------------------------- /core/core.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | //go:generate go run semver/semver.go -f semver.tpl -o core_generated.go 15 | 16 | package core 17 | 18 | import "time" 19 | 20 | var ( 21 | // SemVer is the semantic version. 22 | SemVer = "unknown" 23 | 24 | // CommitSha7 is the short version of the commit hash from which 25 | // this program was built. 26 | CommitSha7 string 27 | 28 | // CommitSha32 is the long version of the commit hash from which 29 | // this program was built. 30 | CommitSha32 string 31 | 32 | // CommitTime is the commit timestamp of the commit from which 33 | // this program was built. 34 | CommitTime time.Time 35 | ) 36 | -------------------------------------------------------------------------------- /core/semver.tpl: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "time" 4 | 5 | func init() { 6 | SemVer = "{{.SemVer}}" 7 | CommitSha7 = "{{.Sha7}}" 8 | CommitSha32 = "{{.Sha32}}" 9 | CommitTime = time.Unix({{.Epoch}}, 0) 10 | } 11 | -------------------------------------------------------------------------------- /core/semver/semver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019-2025 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "flag" 21 | "fmt" 22 | "io" 23 | "os" 24 | "os/exec" 25 | "path/filepath" 26 | "regexp" 27 | "runtime" 28 | "strconv" 29 | "strings" 30 | "syscall" 31 | "text/template" 32 | "time" 33 | ) 34 | 35 | var ( 36 | format string 37 | output string 38 | export bool 39 | tpl *template.Template 40 | ) 41 | 42 | func init() { 43 | if flag.Lookup("f") == nil { 44 | flag.StringVar( 45 | &format, 46 | "f", 47 | "ver", 48 | "The output format: env, go, json, mk, rpm, ver") 49 | } 50 | if flag.Lookup("o") == nil { 51 | flag.StringVar( 52 | &output, 53 | "o", 54 | "", 55 | "The output file") 56 | } 57 | if flag.Lookup("x") == nil { 58 | flag.BoolVar( 59 | &export, 60 | "x", 61 | false, 62 | "Export env vars. Used with -f env") 63 | } 64 | } 65 | 66 | func initFlags() { 67 | format = flag.Lookup("f").Value.(flag.Getter).Get().(string) 68 | output = flag.Lookup("o").Value.(flag.Getter).Get().(string) 69 | export = flag.Lookup("x").Value.(flag.Getter).Get().(bool) 70 | } 71 | 72 | func main() { 73 | flag.Parse() 74 | initFlags() 75 | 76 | if strings.EqualFold("env", format) { 77 | format = "env" 78 | } else if strings.EqualFold("go", format) { 79 | format = "go" 80 | } else if strings.EqualFold("json", format) { 81 | format = "json" 82 | } else if strings.EqualFold("mk", format) { 83 | format = "mk" 84 | } else if strings.EqualFold("rpm", format) { 85 | format = "rpm" 86 | } else if strings.EqualFold("ver", format) { 87 | format = "ver" 88 | } else { 89 | if fileExists(format) { 90 | buf, err := ReadFile(format) // #nosec G304 91 | if err != nil { 92 | errorExit(fmt.Sprintf("error: read tpl failed: %v\n", err)) 93 | } 94 | format = string(buf) 95 | } 96 | tpl = template.Must(template.New("tpl").Parse(format)) 97 | format = "tpl" 98 | } 99 | 100 | var w io.Writer = os.Stdout 101 | if len(output) > 0 { 102 | fout, err := os.Create(filepath.Clean(output)) 103 | if err != nil { 104 | errorExit(fmt.Sprintf("error: %v\n", err)) 105 | } 106 | w = fout 107 | defer func() { 108 | if err := fout.Close(); err != nil { 109 | panic(err) 110 | } 111 | }() // #nosec G20 112 | } 113 | 114 | gitdesc := chkErr(doExec("git", "describe", "--long", "--dirty")) 115 | rx := regexp.MustCompile( 116 | `^[^\d]*(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z].+?))?(?:-(\d+)-g(.+?)(?:-(dirty))?)?\s*$`) 117 | m := rx.FindStringSubmatch(gitdesc) 118 | if len(m) == 0 { 119 | errorExit(fmt.Sprintf("error: match git describe failed: %s\n", gitdesc)) 120 | } 121 | 122 | goos := os.Getenv("XGOOS") 123 | if goos == "" { 124 | goos = runtime.GOOS 125 | } 126 | goarch := os.Getenv("XGOARCH") 127 | if goarch == "" { 128 | goarch = runtime.GOARCH 129 | } 130 | // get the build number. Jenkins exposes this as an 131 | // env variable called BUILD_NUMBER 132 | buildNumber := os.Getenv("BUILD_NUMBER") 133 | if buildNumber == "" { 134 | buildNumber = m[5] 135 | } 136 | buildType := os.Getenv("BUILD_TYPE") 137 | if buildType == "" { 138 | buildType = "X" 139 | } 140 | ver := &semver{ 141 | GOOS: goos, 142 | GOARCH: goarch, 143 | OS: goosToUname[goos], 144 | Arch: goarchToUname[goarch], 145 | Major: toInt(m[1]), 146 | Minor: toInt(m[2]), 147 | Patch: toInt(m[3]), 148 | Notes: m[4], 149 | Type: buildType, 150 | Build: toInt(buildNumber), 151 | Sha7: m[6], 152 | Sha32: chkErr(doExec("git", "log", "-n1", `--format=%H`)), 153 | Dirty: m[7] != "", 154 | Epoch: toInt64(chkErr(doExec("git", "log", "-n1", `--format=%ct`))), 155 | } 156 | ver.SemVer = ver.String() 157 | ver.SemVerRPM = ver.RPM() 158 | ver.BuildDate = ver.Timestamp().Format("Mon, 02 Jan 2006 15:04:05 MST") 159 | ver.ReleaseDate = ver.Timestamp().Format("06-01-02") 160 | 161 | switch format { 162 | case "env": 163 | for _, v := range ver.EnvVars() { 164 | if export { 165 | fmt.Fprint(w, "export ") 166 | } 167 | fmt.Fprintln(w, v) 168 | } 169 | case "go": 170 | case "json": 171 | enc := json.NewEncoder(w) 172 | enc.SetIndent("", " ") 173 | if err := enc.Encode(ver); err != nil { 174 | errorExit(fmt.Sprintf("error: encode to json failed: %v\n", err)) 175 | } 176 | case "mk": 177 | for _, v := range ver.EnvVars() { 178 | p := strings.SplitN(v, "=", 2) 179 | key := p[0] 180 | fmt.Fprintf(w, "%s ?=", key) 181 | if len(p) == 1 { 182 | fmt.Fprintln(w) 183 | } else { 184 | val := p[1] 185 | if strings.HasPrefix(val, `"`) && 186 | strings.HasSuffix(val, `"`) { 187 | val = val[1 : len(val)-1] 188 | } 189 | val = strings.Replace(val, "$", "$$", -1) 190 | fmt.Fprintf(w, " %s\n", val) 191 | } 192 | } 193 | case "rpm": 194 | fmt.Fprintln(w, ver.RPM()) 195 | case "tpl": 196 | if err := tpl.Execute(w, ver); err != nil { 197 | errorExit(fmt.Sprintf("error: template failed: %v\n", err)) 198 | } 199 | case "ver": 200 | fmt.Fprintln(w, ver.String()) 201 | } 202 | } 203 | 204 | var doExec = func(cmd string, args ...string) ([]byte, error) { 205 | c := exec.Command(cmd, args...) // #nosec G204 206 | c.Stderr = os.Stderr 207 | return c.Output() 208 | } 209 | 210 | func errorExit(message string) { 211 | fmt.Fprintf(os.Stderr, "%s", message) 212 | OSExit(1) 213 | } 214 | 215 | func chkErr(out []byte, err error) string { 216 | if err == nil { 217 | return strings.TrimSpace(string(out)) 218 | } 219 | 220 | e, ok := GetExitError(err) 221 | if !ok { 222 | OSExit(1) 223 | } 224 | 225 | status, ok := GetStatusError(e) 226 | if !ok { 227 | OSExit(1) 228 | } 229 | 230 | OSExit(status) 231 | return "" 232 | } 233 | 234 | type semver struct { 235 | GOOS string `json:"goos"` 236 | GOARCH string `json:"goarch"` 237 | OS string `json:"os"` 238 | Arch string `json:"arch"` 239 | Major int `json:"major"` 240 | Minor int `json:"minor"` 241 | Patch int `json:"patch"` 242 | Build int `json:"build"` 243 | Notes string `json:"notes"` 244 | Type string `json:"type"` 245 | Dirty bool `json:"dirty"` 246 | Sha7 string `json:"sha7"` 247 | Sha32 string `json:"sha32"` 248 | Epoch int64 `json:"epoch"` 249 | SemVer string `json:"semver"` 250 | SemVerRPM string `json:"semverRPM"` 251 | BuildDate string `json:"buildDate"` 252 | ReleaseDate string `json:"releaseDate"` 253 | } 254 | 255 | func (v *semver) String() string { 256 | buf := &bytes.Buffer{} 257 | fmt.Fprintf(buf, "%d.%d.%d", v.Major, v.Minor, v.Patch) 258 | if len(v.Notes) > 0 { 259 | fmt.Fprintf(buf, "-%s", v.Notes) 260 | } 261 | if v.Build > 0 { 262 | fmt.Fprintf(buf, "+%d", v.Build) 263 | } 264 | if v.Dirty { 265 | fmt.Fprint(buf, "+dirty") 266 | } 267 | return buf.String() 268 | } 269 | 270 | func (v *semver) RPM() string { 271 | return strings.Replace(v.String(), "-", "+", -1) 272 | } 273 | 274 | func (v *semver) EnvVars() []string { 275 | return []string{ 276 | fmt.Sprintf("GOOS=%s", v.GOOS), 277 | fmt.Sprintf("GOARCH=%s", v.GOARCH), 278 | fmt.Sprintf("OS=%s", v.OS), 279 | fmt.Sprintf("ARCH=%s", v.Arch), 280 | fmt.Sprintf("MAJOR=%d", v.Major), 281 | fmt.Sprintf("MINOR=%d", v.Minor), 282 | fmt.Sprintf("PATCH=%d", v.Patch), 283 | fmt.Sprintf("BUILD=%3.3d", v.Build), 284 | fmt.Sprintf("NOTES=\"%s\"", v.Notes), 285 | fmt.Sprintf("TYPE=%s", v.Type), 286 | fmt.Sprintf("DIRTY=%v", v.Dirty), 287 | fmt.Sprintf("SHA7=%s", v.Sha7), 288 | fmt.Sprintf("SHA32=%s", v.Sha32), 289 | fmt.Sprintf("EPOCH=%d", v.Epoch), 290 | fmt.Sprintf("SEMVER=\"%s\"", v.SemVer), 291 | fmt.Sprintf("SEMVER_RPM=\"%s\"", v.SemVerRPM), 292 | fmt.Sprintf("BUILD_DATE=\"%s\"", v.BuildDate), 293 | fmt.Sprintf("RELEASE_DATE=\"%s\"", v.ReleaseDate), 294 | } 295 | } 296 | 297 | func (v *semver) Timestamp() time.Time { 298 | return time.Unix(v.Epoch, 0) 299 | } 300 | 301 | func toInt(sz string) int { 302 | i, _ := strconv.Atoi(sz) 303 | return i 304 | } 305 | 306 | func toInt64(sz string) int64 { 307 | i, _ := strconv.Atoi(sz) 308 | return int64(i) 309 | } 310 | 311 | var goosToUname = map[string]string{ 312 | "android": "Android", 313 | "darwin": "Darwin", 314 | "dragonfly": "DragonFly", 315 | "freebsd": "kFreeBSD", 316 | "linux": "Linux", 317 | "nacl": "NaCl", 318 | "netbsd": "NetBSD", 319 | "openbsd": "OpenBSD", 320 | "plan9": "Plan9", 321 | "solaris": "Solaris", 322 | "windows": "Windows", 323 | } 324 | 325 | var goarchToUname = map[string]string{ 326 | "386": "i386", 327 | "amd64": "x86_64", 328 | "amd64p32": "x86_64_P32", 329 | "arm": "ARMv7", 330 | "arm64": "ARMv8", 331 | "mips": "MIPS32", 332 | "mips64": "MIPS64", 333 | "mips64le": "MIPS64LE", 334 | "mipsle": "MIPS32LE", 335 | "ppc64": "PPC64", 336 | "ppc64le": "PPC64LE", 337 | "s390x": "S390X", 338 | } 339 | 340 | func fileExists(filePath string) bool { 341 | if _, err := os.Stat(filePath); !os.IsNotExist(err) { 342 | return true 343 | } 344 | return false 345 | } 346 | 347 | // ReadFile is a wrapper around os.ReadFile 348 | var ReadFile = func(file string) ([]byte, error) { 349 | return os.ReadFile(file) // #nosec G304 350 | } 351 | 352 | // OSExit is a wrapper around os.Exit 353 | var OSExit = func(code int) { 354 | os.Exit(code) 355 | } 356 | 357 | // GetExitError is a wrapper around exec.ExitError 358 | var GetExitError = func(err error) (e *exec.ExitError, ok bool) { 359 | e, ok = err.(*exec.ExitError) 360 | return 361 | } 362 | 363 | // GetStatusError is a wrapper around syscall.WaitStatus 364 | var GetStatusError = func(exitError *exec.ExitError) (status int, ok bool) { 365 | if e, ok := exitError.Sys().(syscall.WaitStatus); ok { 366 | return e.ExitStatus(), true 367 | } 368 | return 1, false 369 | } 370 | -------------------------------------------------------------------------------- /core/semver/semver_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "io" 21 | "os" 22 | "os/exec" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestGetStatusError(_ *testing.T) { 29 | exitError := &exec.ExitError{ 30 | ProcessState: &os.ProcessState{}, 31 | } 32 | _, _ = GetStatusError(exitError) 33 | } 34 | 35 | func TestString(t *testing.T) { 36 | s := semver{"", "", "", "", 1, 2, 3, 4, "", "", true, "", "", 64, "", "", "", ""} 37 | assert.NotNil(t, s.String()) 38 | 39 | s = semver{"", "", "", "", 1, 2, 3, 4, "abc", "", true, "", "", 64, "", "", "", ""} 40 | assert.NotNil(t, s.String()) 41 | } 42 | 43 | func TestGetExitError(t *testing.T) { 44 | err := errors.New("error") 45 | _, ok := GetExitError(err) 46 | assert.False(t, ok) 47 | } 48 | 49 | func TestMainFunction(t *testing.T) { 50 | tests := []struct { 51 | name string 52 | format string 53 | outputFile string 54 | expectEmptyFile bool 55 | readFileFunc func(file string) ([]byte, error) 56 | }{ 57 | { 58 | name: "Write mk format to file", 59 | format: "mk", 60 | outputFile: "test_output.mk", 61 | }, 62 | { 63 | name: "Write env format to file", 64 | format: "env", 65 | outputFile: "test_output.env", 66 | }, 67 | { 68 | name: "Write json format to file", 69 | format: "json", 70 | outputFile: "test_output.json", 71 | }, 72 | { 73 | name: "Write ver format to file", 74 | format: "ver", 75 | outputFile: "test_output.ver", 76 | }, 77 | { 78 | name: "Write rpm format to file", 79 | format: "rpm", 80 | outputFile: "test_output.rpm", 81 | }, 82 | { 83 | name: "Write tpl format to file", 84 | format: "../semver.tpl", 85 | outputFile: "test_output.rpm", 86 | }, 87 | { 88 | name: "Write tpl format to file but error reading source file", 89 | format: "../semver.tpl", 90 | outputFile: "test_output.rpm", 91 | readFileFunc: func(_ string) ([]byte, error) { 92 | return nil, errors.New("error reading source file") 93 | }, 94 | expectEmptyFile: true, 95 | }, 96 | { 97 | // go format currently does not print any output, expect an empty file 98 | name: "Write go format to file", 99 | format: "go", 100 | outputFile: "test_output.go", 101 | expectEmptyFile: true, 102 | }, 103 | } 104 | 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | osArgs := os.Args 108 | os.Args = append(os.Args, "-f", tt.format) 109 | os.Args = append(os.Args, "-o", tt.outputFile) 110 | os.Args = append(os.Args, "-x", "true") 111 | 112 | oldReadFile := ReadFile 113 | if tt.readFileFunc != nil { 114 | ReadFile = tt.readFileFunc 115 | } 116 | oldOSExit := OSExit 117 | OSExit = func(_ int) {} 118 | 119 | oldDoExec := doExec 120 | doExec = func(_ string, _ ...string) ([]byte, error) { 121 | return []byte("v2.14.0-77-g38b3a19-dirty"), nil 122 | } 123 | 124 | main() 125 | 126 | // Open the file 127 | file, err := os.Open(tt.outputFile) 128 | if err != nil { 129 | t.Error(err) 130 | } 131 | defer file.Close() 132 | 133 | // Read the file contents 134 | contents, err := io.ReadAll(file) 135 | if err != nil { 136 | t.Error(err) 137 | } 138 | 139 | defer os.Remove(tt.outputFile) 140 | 141 | // make sure file is not empty 142 | if tt.expectEmptyFile { 143 | assert.Equal(t, 0, len(contents)) 144 | } else { 145 | assert.NotEqual(t, 0, len(contents)) 146 | } 147 | os.Args = osArgs 148 | ReadFile = oldReadFile 149 | OSExit = oldOSExit 150 | doExec = oldDoExec 151 | }) 152 | } 153 | } 154 | 155 | func TestChkErr(t *testing.T) { 156 | tests := []struct { 157 | name string 158 | out []byte 159 | err error 160 | wantOut string 161 | wantErr bool 162 | getExitError func(err error) (*exec.ExitError, bool) 163 | getStatusError func(exitError *exec.ExitError) (int, bool) 164 | }{ 165 | { 166 | name: "No error", 167 | out: []byte("output"), 168 | err: nil, 169 | wantOut: "output", 170 | wantErr: false, 171 | getExitError: func(_ error) (*exec.ExitError, bool) { 172 | return nil, true 173 | }, 174 | getStatusError: func(_ *exec.ExitError) (int, bool) { 175 | return 0, true 176 | }, 177 | }, 178 | { 179 | name: "Error with command", 180 | out: []byte("output"), 181 | err: errors.New("error"), 182 | wantOut: "", 183 | wantErr: true, 184 | getExitError: func(_ error) (*exec.ExitError, bool) { 185 | return nil, false 186 | }, 187 | getStatusError: func(_ *exec.ExitError) (int, bool) { 188 | return 1, false 189 | }, 190 | }, 191 | { 192 | name: "Error casting to ExitError", 193 | out: []byte("output"), 194 | err: errors.New("error"), 195 | wantOut: "", 196 | wantErr: true, 197 | getExitError: func(_ error) (*exec.ExitError, bool) { 198 | return nil, true 199 | }, 200 | getStatusError: func(_ *exec.ExitError) (int, bool) { 201 | return 1, false 202 | }, 203 | }, 204 | { 205 | name: "Error getting status from ExitError", 206 | out: []byte("output"), 207 | err: errors.New("error"), 208 | wantOut: "", 209 | wantErr: true, 210 | getExitError: func(_ error) (*exec.ExitError, bool) { 211 | return nil, false 212 | }, 213 | getStatusError: func(_ *exec.ExitError) (int, bool) { 214 | return 0, true 215 | }, 216 | }, 217 | } 218 | 219 | for _, tt := range tests { 220 | t.Run(tt.name, func(t *testing.T) { 221 | GetExitError = tt.getExitError 222 | GetStatusError = tt.getStatusError 223 | OSExit = func(_ int) {} 224 | 225 | gotOut := chkErr(tt.out, tt.err) 226 | if gotOut != tt.wantOut { 227 | t.Errorf("chkErr() gotOut = %v, want %v", gotOut, tt.wantOut) 228 | } 229 | }) 230 | } 231 | } 232 | 233 | func TestFileExists(t *testing.T) { 234 | tests := []struct { 235 | name string 236 | filePath string 237 | want bool 238 | }{ 239 | { 240 | name: "File exists", 241 | filePath: "semver.go", 242 | want: true, 243 | }, 244 | { 245 | name: "File does not exist", 246 | filePath: "non-existent.txt", 247 | want: false, 248 | }, 249 | { 250 | name: "File path is empty", 251 | filePath: "", 252 | want: false, 253 | }, 254 | } 255 | 256 | for _, tt := range tests { 257 | t.Run(tt.name, func(t *testing.T) { 258 | got := fileExists(tt.filePath) 259 | if got != tt.want { 260 | t.Errorf("fileExists(%s) = %v, want %v", tt.filePath, got, tt.want) 261 | } 262 | }) 263 | } 264 | } 265 | 266 | func TestErrorExit(t *testing.T) { 267 | message := "error message" 268 | 269 | if os.Getenv("INVOKE_ERROR_EXIT") == "1" { 270 | errorExit(message) 271 | return 272 | } 273 | // call the test again with INVOKE_ERROR_EXIT=1 so the errorExit function is invoked and we can check the return code 274 | cmd := exec.Command(os.Args[0], "-test.run=TestErrorExit") // #nosec G204 275 | cmd.Env = append(os.Environ(), "INVOKE_ERROR_EXIT=1") 276 | 277 | stderr, err := cmd.StderrPipe() 278 | if err != nil { 279 | fmt.Println("Error creating stderr pipe:", err) 280 | return 281 | } 282 | 283 | if err := cmd.Start(); err != nil { 284 | t.Error(err) 285 | } 286 | 287 | buf := make([]byte, 1024) 288 | n, err := stderr.Read(buf) 289 | if err != nil { 290 | t.Error(err) 291 | } 292 | 293 | err = cmd.Wait() 294 | if e, ok := err.(*exec.ExitError); ok && e.Success() { 295 | t.Error(err) 296 | } 297 | 298 | // check the output is the message we logged in errorExit 299 | assert.Equal(t, message, string(buf[:n])) 300 | } 301 | -------------------------------------------------------------------------------- /dell-csi-helm-installer/.gitignore: -------------------------------------------------------------------------------- 1 | images.manifest 2 | images.tar 3 | -------------------------------------------------------------------------------- /dell-csi-helm-installer/README.md: -------------------------------------------------------------------------------- 1 | 14 | # Helm Installer for Dell CSI Storage Providers 15 | 16 | ## Description 17 | 18 | This directory provides scripts to install, upgrade, uninstall the CSI drivers, and to verify the Kubernetes environment. 19 | These same scripts are present in all Dell Container Storage Interface ([CSI](https://github.com/container-storage-interface/spec)) drivers. This includes the drivers for: 20 | * [PowerFlex](https://github.com/dell/csi-vxflexos) 21 | * [PowerMax](https://github.com/dell/csi-powermax) 22 | * [PowerScale](https://github.com/dell/csi-powerscale) 23 | * [PowerStore](https://github.com/dell/csi-powerstore) 24 | * [Unity XT](https://github.com/dell/csi-unity) 25 | 26 | NOTE: This documentation uses the Unity XT driver as an example. If working with a different driver, substitute the name as appropriate. 27 | 28 | ## Dependencies 29 | 30 | Installing any of the Dell CSI Drivers requires a few utilities to be installed on the system running the installation. 31 | 32 | | Dependency | Usage | 33 | | ------------- | ----- | 34 | | `kubectl` | Kubectl is used to validate that the Kubernetes system meets the requirements of the driver. | 35 | | `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. | 36 | | `sshpass` | sshpass is used to check certain pre-requisities in worker nodes (in chosen drivers). | 37 | 38 | 39 | 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. 40 | 41 | ## Capabilities 42 | 43 | This project provides the following capabilitites, each one is discussed in detail later in this document. 44 | 45 | * Install a driver. When installing a driver, options are provided to specify the target namespace as well as options to control the types of verifications to be performed on the target system. 46 | * Upgrade a driver. Upgrading a driver is an effective way to either deploy a new version of the driver or to modify the parameters used in an initial deployment. 47 | * Uninstall a driver. This removes the driver and any installed storage classes. 48 | * Verify a Kubernetes system for suitability with a driver. These verification steps differ, slightly, from driver to driver but include verifiying version compatibility, namespace availability, existance of required secrets, and validating worker node compatibility with driver protocols such as iSCSI, Fibre Channel, NFS, etc 49 | 50 | 51 | Most of these usages require the creation/specification of a values file. These files specify configuration settings that are passed into the driver and configure it for use. To create one of these files, the following steps should be followed: 52 | 1. Download a template file for the driver to a new location, naming this new file is at the users discretion. The template files are always found at `https://github.com/dell/helm-charts/raw/csi-unity-2.14.0/charts/csi-unity/values.yaml` 53 | 2. Edit the file such that it contains the proper configuration settings for the specific environment. These files are yaml formatted so maintaining the file structure is important. 54 | 55 | For example, to create a values file for the Unity XT driver the following steps can be executed 56 | ``` 57 | # cd to the installation script directory 58 | cd dell-csi-helm-installer 59 | 60 | # download the template file 61 | wget -O my-unity-settings.yaml https://github.com/dell/helm-charts/raw/csi-unity-2.14.0/charts/csi-unity/values.yaml 62 | 63 | # edit the newly created values file 64 | vi my-unity-settings.yaml 65 | ``` 66 | 67 | These values files can then be archived for later reference or for usage when upgrading the driver. 68 | 69 | 70 | ### Install A Driver 71 | 72 | Installing a driver is performed via the `csi-install.sh` script. This script requires a few arguments: the target namespace and the user created values file. By default, this will verify the Kubernetes environment and present a list of warnings and/or errors. Errors must be addressed before installing, warning should be examined for their applicability. For example, in order to install the Unity driver into a namespace called "unity", the following command should be run: 73 | ``` 74 | ./csi-install.sh --namespace unity --values ./my-unity-settings.yaml 75 | ``` 76 | 77 | For usage information: 78 | ``` 79 | [dell-csi-helm-installer]# ./csi-install.sh -h 80 | Help for ./csi-install.sh 81 | 82 | Usage: ./csi-install.sh options... 83 | Options: 84 | Required 85 | --namespace[=] Kubernetes namespace containing the CSI driver 86 | --values[=] Values file, which defines configuration values 87 | Optional 88 | --release[=] Name to register with helm, default value will match the driver name 89 | --upgrade Perform an upgrade of the specified driver, default is false 90 | --node-verify-user[=] Username to SSH to worker nodes as, used to validate node requirements. Default is root 91 | --skip-verify Skip the kubernetes configuration verification to use the CSI driver, default will run verification 92 | --skip-verify-node Skip worker node verification checks 93 | -h Help 94 | ``` 95 | 96 | ### Upgrade A Driver 97 | 98 | Upgrading a driver is very similar to installation. The `csi-install.sh` script is run, with the same required arguments, along with a `--upgrade` argument. For example, to upgrade the previously installed Unity XT driver, the following command can be supplied: 99 | 100 | ``` 101 | ./csi-install.sh --namespace unity --values ./my-unity-settings.yaml --upgrade 102 | ``` 103 | 104 | For usage information: 105 | ``` 106 | [dell-csi-helm-installer]# ./csi-install.sh -h 107 | Help for ./csi-install.sh 108 | 109 | Usage: ./csi-install.sh options... 110 | Options: 111 | Required 112 | --namespace[=] Kubernetes namespace containing the CSI driver 113 | --values[=] Values file, which defines configuration values 114 | Optional 115 | --release[=] Name to register with helm, default value will match the driver name 116 | --upgrade Perform an upgrade of the specified driver, default is false 117 | --node-verify-user[=] Username to SSH to worker nodes as, used to validate node requirements. Default is root 118 | --skip-verify Skip the kubernetes configuration verification to use the CSI driver, default will run verification 119 | --skip-verify-node Skip worker node verification checks 120 | -h Help 121 | ``` 122 | 123 | ### Uninstall A Driver 124 | 125 | To uninstall a driver, the `csi-uninstall.sh` script provides a handy wrapper around the `helm` utility. The only required argument for uninstallation is the namespace name. To uninstall the Unity XT driver: 126 | 127 | ``` 128 | ./csi-uninstall.sh --namespace unity 129 | ``` 130 | 131 | For usage information: 132 | ``` 133 | [dell-csi-helm-installer]# ./csi-uninstall.sh -h 134 | Help for ./csi-uninstall.sh 135 | 136 | Usage: ./csi-uninstall.sh options... 137 | Options: 138 | Required 139 | --namespace[=] Kubernetes namespace to uninstall the CSI driver from 140 | Optional 141 | --release[=] Name to register with helm, default value will match the driver name 142 | -h Help 143 | ``` 144 | 145 | ### Verify A Kubernetes Environment 146 | 147 | The `verify.sh` script is run, automatically, as part of the installation and upgrade procedures and can also be run by itself. This provides a handy means to validate a Kubernetes system without meaning to actually perform the installation. To verify an environment, run `verify.sh` with the namespace name and values file options. 148 | 149 | ``` 150 | ./verify.sh --namespace unity --values ./my-unity-settings.yaml 151 | ``` 152 | 153 | For usage information: 154 | ``` 155 | [dell-csi-helm-installer]# ./verify.sh -h 156 | Help for ./verify.sh 157 | 158 | Usage: ./verify.sh options... 159 | Options: 160 | Required 161 | --namespace[=] Kubernetes namespace to install the CSI driver 162 | --values[=] Values file, which defines configuration values 163 | Optional 164 | --skip-verify-node Skip worker node verification checks 165 | --release[=] Name to register with helm, default value will match the driver name 166 | --node-verify-user[=] Username to SSH to worker nodes as, used to validate node requirements. Default is root 167 | -h Help Help 168 | ``` 169 | -------------------------------------------------------------------------------- /dell-csi-helm-installer/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) 2021 Dell Inc., or its subsidiaries. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | DRIVERDIR="${SCRIPTDIR}/../helm" 12 | 13 | RED='\033[0;31m' 14 | GREEN='\033[0;32m' 15 | YELLOW='\033[1;33m' 16 | DARK_GRAY='\033[1;30m' 17 | NC='\033[0m' # No Color 18 | 19 | function decho() { 20 | if [ -n "${DEBUGLOG}" ]; then 21 | echo "$@" | tee -a "${DEBUGLOG}" 22 | fi 23 | } 24 | 25 | function debuglog_only() { 26 | if [ -n "${DEBUGLOG}" ]; then 27 | echo "$@" >> "${DEBUGLOG}" 28 | fi 29 | } 30 | 31 | function log() { 32 | case $1 in 33 | separator) 34 | decho "------------------------------------------------------" 35 | ;; 36 | error) 37 | decho 38 | log separator 39 | printf "${RED}Error: $2\n" 40 | printf "${RED}Installation cannot continue${NC}\n" 41 | debuglog_only "Error: $2" 42 | debuglog_only "Installation cannot continue" 43 | exit 1 44 | ;; 45 | uninstall_error) 46 | log separator 47 | printf "${RED}Error: $2\n" 48 | printf "${RED}Uninstallation cannot continue${NC}\n" 49 | debuglog_only "Error: $2" 50 | debuglog_only "Uninstallation cannot continue" 51 | exit 1 52 | ;; 53 | step) 54 | printf "|\n|- %-65s" "$2" 55 | debuglog_only "${2}" 56 | ;; 57 | small_step) 58 | printf "%-61s" "$2" 59 | debuglog_only "${2}" 60 | ;; 61 | section) 62 | log separator 63 | printf "> %s\n" "$2" 64 | debuglog_only "${2}" 65 | log separator 66 | ;; 67 | smart_step) 68 | if [[ $3 == "small" ]]; then 69 | log small_step "$2" 70 | else 71 | log step "$2" 72 | fi 73 | ;; 74 | arrow) 75 | printf " %s\n %s" "|" "|--> " 76 | ;; 77 | step_success) 78 | printf "${GREEN}Success${NC}\n" 79 | ;; 80 | step_failure) 81 | printf "${RED}Failed${NC}\n" 82 | ;; 83 | step_warning) 84 | printf "${YELLOW}Warning${NC}\n" 85 | ;; 86 | info) 87 | printf "${DARK_GRAY}%s${NC}\n" "$2" 88 | ;; 89 | passed) 90 | printf "${GREEN}Success${NC}\n" 91 | ;; 92 | warnings) 93 | printf "${YELLOW}Warnings:${NC}\n" 94 | ;; 95 | errors) 96 | printf "${RED}Errors:${NC}\n" 97 | ;; 98 | *) 99 | echo -n "Unknown" 100 | ;; 101 | esac 102 | } 103 | 104 | function check_error() { 105 | if [[ $1 -ne 0 ]]; then 106 | log step_failure 107 | else 108 | log step_success 109 | fi 110 | } 111 | 112 | # 113 | # get_release will determine the helm release name to use 114 | # If ${RELEASE} is set, use that 115 | # Otherwise, use the driver name minus any "csi-" prefix 116 | # argument 1: Driver name 117 | function get_release_name() { 118 | local D="${1}" 119 | if [ ! -z "${RELEASE}" ]; then 120 | decho "${RELEASE}" 121 | return 122 | fi 123 | 124 | local PREFIX="csi-" 125 | R=${D#"$PREFIX"} 126 | decho "${R}" 127 | } 128 | 129 | function run_command() { 130 | local RC=0 131 | if [ -n "${DEBUGLOG}" ]; then 132 | local ME=$(basename "${0}") 133 | echo "---------------" >> "${DEBUGLOG}" 134 | echo "${ME}:${BASH_LINENO[0]} - Running command: $@" >> "${DEBUGLOG}" 135 | debuglog_only "Results:" 136 | eval "$@" | tee -a "${DEBUGLOG}" 137 | RC=${PIPESTATUS[0]} 138 | echo "---------------" >> "${DEBUGLOG}" 139 | else 140 | eval "$@" 141 | RC=$? 142 | fi 143 | return $RC 144 | } 145 | 146 | # dump out information about a helm chart to the debug file 147 | # takes a few arguments 148 | # $1 the namespace 149 | # $2 the release 150 | function debuglog_helm_status() { 151 | local NS="${1}" 152 | local RLS="${2}" 153 | 154 | debuglog_only "Getting information about Helm release: ${RLS}" 155 | debuglog_only "****************" 156 | debuglog_only "Helm Status:" 157 | helm status "${RLS}" -n "${NS}" >> "${DEBUGLOG}" 158 | debuglog_only "****************" 159 | debuglog_only "Manifest" 160 | helm get manifest "${RLS}" -n "${NS}" >> "${DEBUGLOG}" 161 | debuglog_only "****************" 162 | debuglog_only "Status of resources" 163 | helm get manifest "${RLS}" -n "${NS}" | kubectl get -f - >> "${DEBUGLOG}" 164 | 165 | } 166 | 167 | # determines if the current KUBECONFIG is pointing to an OpenShift cluster 168 | # echos "true" or "false" 169 | function isOpenShift() { 170 | # check if the securitycontextconstraints.security.openshift.io crd exists 171 | run_command kubectl get crd | grep securitycontextconstraints.security.openshift.io --quiet >/dev/null 2>&1 172 | local O=$? 173 | if [[ ${O} == 0 ]]; then 174 | # this is openshift 175 | echo "true" 176 | else 177 | echo "false" 178 | fi 179 | } 180 | 181 | # determines the version of OpenShift 182 | # echos version, or empty string if not OpenShift 183 | function OpenShiftVersion() { 184 | # check if this is OpenShift 185 | local O=$(isOpenShift) 186 | if [ "${O}" == "false" ]; then 187 | # this is not openshift 188 | echo "" 189 | else 190 | local V=$(run_command kubectl get clusterversions -o jsonpath="{.items[*].status.desired.version}") 191 | local MAJOR=$(echo "${V}" | awk -F '.' '{print $1}') 192 | local MINOR=$(echo "${V}" | awk -F '.' '{print $2}') 193 | echo "${MAJOR}.${MINOR}" 194 | fi 195 | } 196 | 197 | -------------------------------------------------------------------------------- /dell-csi-helm-installer/csi-uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) 2021 Dell Inc., or its subsidiaries. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 12 | PROG="${0}" 13 | DRIVER="csi-unity" 14 | 15 | # export the name of the debug log, so child processes will see it 16 | export DEBUGLOG="${SCRIPTDIR}/uninstall-debug.log" 17 | 18 | declare -a VALIDDRIVERS 19 | 20 | source "$SCRIPTDIR"/common.sh 21 | 22 | if [ -f "${DEBUGLOG}" ]; then 23 | rm -f "${DEBUGLOG}" 24 | fi 25 | 26 | # 27 | # usage will print command execution help and then exit 28 | function usage() { 29 | decho "Help for $PROG" 30 | decho 31 | decho "Usage: $PROG options..." 32 | decho "Options:" 33 | decho " Required" 34 | decho " --namespace[=] Kubernetes namespace to uninstall the CSI driver from" 35 | 36 | decho " Optional" 37 | decho " --release[=] Name to register with helm, default value will match the driver name" 38 | decho " -h Help" 39 | decho 40 | 41 | exit 0 42 | } 43 | 44 | 45 | 46 | # 47 | # validate_params will validate the parameters passed in 48 | function validate_params() { 49 | # make sure the driver was specified 50 | if [ -z "${DRIVER}" ]; then 51 | decho "No driver specified" 52 | exit 1 53 | fi 54 | # the namespace is required 55 | if [ -z "${NAMESPACE}" ]; then 56 | decho "No namespace specified" 57 | usage 58 | exit 1 59 | fi 60 | } 61 | 62 | 63 | # check_for_driver will see if the driver is installed within the namespace provided 64 | function check_for_driver() { 65 | NUM=$(run_command helm list --namespace "${NAMESPACE}" | grep "^${RELEASE}\b" | wc -l) 66 | if [ "${NUM}" == "0" ]; then 67 | log uninstall_error "The CSI Driver is not installed." 68 | exit 1 69 | fi 70 | } 71 | 72 | DRIVERDIR="${SCRIPTDIR}/../helm-charts/charts" 73 | 74 | while getopts ":h-:" optchar; do 75 | case "${optchar}" in 76 | -) 77 | case "${OPTARG}" in 78 | # NAMESPACE 79 | namespace) 80 | NAMESPACE="${!OPTIND}" 81 | OPTIND=$((OPTIND + 1)) 82 | ;; 83 | namespace=*) 84 | NAMESPACE=${OPTARG#*=} 85 | ;; 86 | # RELEASE 87 | release) 88 | RELEASE="${!OPTIND}" 89 | OPTIND=$((OPTIND + 1)) 90 | ;; 91 | release=*) 92 | RELEASE=${OPTARG#*=} 93 | ;; 94 | *) 95 | decho "Unknown option --${OPTARG}" 96 | decho "For help, run $PROG -h" 97 | exit 1 98 | ;; 99 | esac 100 | ;; 101 | h) 102 | usage 103 | ;; 104 | *) 105 | decho "Unknown option -${OPTARG}" 106 | decho "For help, run $PROG -h" 107 | exit 1 108 | ;; 109 | esac 110 | done 111 | 112 | # by default the NAME of the helm release of the driver is the same as the driver name 113 | RELEASE=$(get_release_name "${DRIVER}") 114 | 115 | # validate the parameters passed in 116 | validate_params 117 | 118 | check_for_driver 119 | run_command helm delete -n "${NAMESPACE}" "${RELEASE}" 120 | if [ $? -ne 0 ]; then 121 | decho "Removal of the CSI Driver was unsuccessful" 122 | exit 1 123 | fi 124 | 125 | decho "Removal of the CSI Driver is in progress." 126 | decho "It may take a few minutes for all pods to terminate." 127 | 128 | -------------------------------------------------------------------------------- /dell-csi-helm-installer/verify-csi-unity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) 2020 Dell Inc., or its subsidiaries. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # verify-csi-unity method 12 | function verify-csi-unity() { 13 | verify_k8s_versions "1.31" "1.33" 14 | verify_openshift_versions "4.18" "4.19" 15 | verify_namespace "${NS}" 16 | verify_required_secrets "${RELEASE}-creds" 17 | verify_optional_secrets "${RELEASE}-certs" 18 | snapshot_value=$(is_snapshot_enabled) 19 | if [ "$snapshot_value" == "true" ]; then 20 | verify_alpha_snap_resources 21 | fi 22 | verify_unity_protocol_installation 23 | if [ "$snapshot_value" == "true" ]; then 24 | verify_snap_requirements 25 | fi 26 | verify_helm_3 27 | verify_helm_values_version "${DRIVER_VERSION}" 28 | } 29 | 30 | function verify_unity_protocol_installation() { 31 | if [ ${NODE_VERIFY} -eq 0 ]; then 32 | return 33 | fi 34 | 35 | log smart_step "Verifying sshpass installation.." 36 | SSHPASS=$(which sshpass) 37 | if [ -z "$SSHPASS" ]; then 38 | found_warning "sshpass is not installed. It is mandatory to have ssh pass software for multi node kubernetes setup." 39 | fi 40 | 41 | 42 | log smart_step "Verifying iSCSI installation" "$1" 43 | 44 | error=0 45 | for node in $MINION_NODES; do 46 | # check if the iSCSI client is installed 47 | echo 48 | echo -n "Enter the ${NODEUSER} password of ${node}: " 49 | read -s nodepassword 50 | echo 51 | echo "$nodepassword" > protocheckfile 52 | chmod 0400 protocheckfile 53 | unset nodepassword 54 | run_command sshpass -f protocheckfile ssh -o StrictHostKeyChecking=no ${NODEUSER}@"${node}" "cat /etc/iscsi/initiatorname.iscsi" > /dev/null 2>&1 55 | rv=$? 56 | if [ $rv -ne 0 ]; then 57 | error=1 58 | found_warning "iSCSI client is either not found on node: $node or not able to verify" 59 | fi 60 | run_command sshpass -f protocheckfile ssh -o StrictHostKeyChecking=no ${NODEUSER}@"${node}" "pgrep iscsid" > /dev/null 2>&1 61 | rv1=$? 62 | if [ $rv1 -ne 0 ]; then 63 | error=1 64 | found_warning "iscsid service is either not running on node: $node or not able to verify" 65 | fi 66 | rm -f protocheckfile 67 | done 68 | check_error error 69 | } 70 | -------------------------------------------------------------------------------- /docker.mk: -------------------------------------------------------------------------------- 1 | # Includes the following generated file to get semantic version information 2 | include semver.mk 3 | ifdef NOTES 4 | RELNOTE="-$(NOTES)" 5 | else 6 | RELNOTE= 7 | endif 8 | 9 | # local build, use user and timestamp it 10 | NAME:=csi-unity 11 | DOCKER_IMAGE_NAME ?= ${NAME} 12 | VERSION:=$(shell date +%Y%m%d%H%M%S) 13 | BIN_DIR:=bin 14 | BIN_NAME:=${NAME} 15 | DOCKER_REPO ?= dellemc 16 | DOCKER_NAMESPACE ?= csi-unity 17 | 18 | .PHONY: docker-build 19 | docker-build: 20 | $(eval include csm-common.mk) 21 | echo ${VERSION} ${GITLAB_CI} ${CI_COMMIT_TAG} ${CI_COMMIT_SHA} 22 | rm -f core/core_generated.go 23 | cd core && go generate 24 | go run core/semver/semver.go -f mk >semver.mk 25 | mkdir -p ${BIN_DIR} 26 | GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -ldflags '-extldflags "-static"' -o ${BIN_DIR}/${BIN_NAME} 27 | docker build --pull -t ${DOCKER_REPO}/${DOCKER_NAMESPACE}/${DOCKER_IMAGE_NAME}:${IMAGETAG} --build-arg GOPROXY=$(GOPROXY) --build-arg BASEIMAGE=$(CSM_BASEIMAGE) --build-arg GOIMAGE=$(DEFAULT_GOIMAGE) . 28 | 29 | .PHONY: docker-push 30 | docker-push: docker-build 31 | docker push ${DOCKER_REPO}/${DOCKER_NAMESPACE}/${DOCKER_IMAGE_NAME}:${IMAGETAG} 32 | 33 | version: 34 | @echo "MAJOR $(MAJOR) MINOR $(MINOR) PATCH $(PATCH) BUILD ${BUILD} TYPE ${TYPE} RELNOTE $(RELNOTE) SEMVER $(SEMVER)" 35 | @echo "Target Version: $(VERSION)" 36 | 37 | -------------------------------------------------------------------------------- /env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | export X_CSI_UNITY_NODENAME= 15 | export X_CSI_UNITY_LONGNODENAME= 16 | export X_CSI_STAGING_TARGET_PATH= 17 | export X_CSI_EPHEMERAL_STAGING_PATH= 18 | export X_CSI_PUBLISH_TARGET_PATH= 19 | export CSI_ENDPOINT= 20 | export X_CSI_DEBUG= 21 | export arrayId= 22 | export STORAGE_POOL= 23 | export NAS_SERVER= 24 | export X_CSI_REQ_LOGGING= 25 | export X_CSI_REP_LOGGING= 26 | export GOUNITY_DEBUG= 27 | export DRIVER_NAME= 28 | export DRIVER_SECRET= 29 | export DRIVER_CONFIG= 30 | export X_CSI_UNITY_AUTOPROBE= 31 | export X_CSI_UNITY_ALLOW_MULTI_POD_ACCESS= 32 | export X_CSI_HEALTH_MONITOR_ENABLED= 33 | export CSI_LOG_LEVEL= 34 | export ALLOW_RWO_MULTIPOD_ACCESS= 35 | export MAX_UNITY_VOLUMES_PER_NODE= 36 | export SYNC_NODE_INFO_TIME_INTERVAL= 37 | export TENANT_NAME= 38 | # if user has not provided any allowed networks, set it to empty else provide comma separated allowed networks 39 | export X_CSI_ALLOWED_NETWORKS="" 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dell/csi-unity 2 | 3 | go 1.24 4 | 5 | require ( 6 | bou.ke/monkey v1.0.2 7 | github.com/container-storage-interface/spec v1.6.0 8 | github.com/cucumber/godog v0.15.0 9 | github.com/dell/dell-csi-extensions/podmon v1.8.0 10 | github.com/dell/gobrick v1.14.0 11 | github.com/dell/gocsi v1.14.0 12 | github.com/dell/gofsutil v1.19.0 13 | github.com/dell/goiscsi v1.12.0 14 | github.com/dell/gounity v1.21.0 15 | github.com/fsnotify/fsnotify v1.9.0 16 | github.com/kubernetes-csi/csi-lib-utils v0.7.0 17 | github.com/sirupsen/logrus v1.9.3 18 | github.com/spf13/viper v1.20.0 19 | github.com/stretchr/testify v1.10.0 20 | golang.org/x/net v0.40.0 21 | google.golang.org/grpc v1.72.0 22 | google.golang.org/protobuf v1.36.5 23 | gopkg.in/yaml.v2 v2.4.0 24 | gopkg.in/yaml.v3 v3.0.1 25 | k8s.io/apimachinery v0.22.2 26 | k8s.io/client-go v0.22.2 27 | ) 28 | 29 | require ( 30 | github.com/akutz/gosync v0.1.0 // indirect 31 | github.com/coreos/go-semver v0.3.1 // indirect 32 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 33 | github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect 34 | github.com/cucumber/messages/go/v21 v21.0.1 // indirect 35 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 36 | github.com/dell/gonvme v1.11.0 // indirect 37 | github.com/evanphx/json-patch v4.9.0+incompatible // indirect 38 | github.com/go-logr/logr v1.4.2 // indirect 39 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 40 | github.com/gofrs/uuid v4.4.0+incompatible // indirect 41 | github.com/gogo/protobuf v1.3.2 // indirect 42 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 43 | github.com/golang/mock v1.6.0 // indirect 44 | github.com/golang/protobuf v1.5.4 // indirect 45 | github.com/google/gofuzz v1.2.0 // indirect 46 | github.com/googleapis/gnostic v0.4.1 // indirect 47 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 48 | github.com/hashicorp/go-memdb v1.3.4 // indirect 49 | github.com/hashicorp/golang-lru v1.0.2 // indirect 50 | github.com/imdario/mergo v0.3.5 // indirect 51 | github.com/json-iterator/go v1.1.12 // indirect 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 53 | github.com/modern-go/reflect2 v1.0.2 // indirect 54 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 55 | github.com/pkg/errors v0.9.1 // indirect 56 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 57 | github.com/sagikazarmark/locafero v0.7.0 // indirect 58 | github.com/sourcegraph/conc v0.3.0 // indirect 59 | github.com/spf13/afero v1.12.0 // indirect 60 | github.com/spf13/cast v1.7.1 // indirect 61 | github.com/spf13/pflag v1.0.6 // indirect 62 | github.com/stretchr/objx v0.5.2 // indirect 63 | github.com/subosito/gotenv v1.6.0 // indirect 64 | go.etcd.io/etcd/api/v3 v3.5.17 // indirect 65 | go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect 66 | go.etcd.io/etcd/client/v3 v3.5.17 // indirect 67 | go.uber.org/multierr v1.11.0 // indirect 68 | go.uber.org/zap v1.27.0 // indirect 69 | golang.org/x/crypto v0.38.0 // indirect 70 | golang.org/x/oauth2 v0.26.0 // indirect 71 | golang.org/x/sync v0.14.0 // indirect 72 | golang.org/x/sys v0.33.0 // indirect 73 | golang.org/x/term v0.32.0 // indirect 74 | golang.org/x/text v0.25.0 // indirect 75 | golang.org/x/time v0.9.0 // indirect 76 | google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect 77 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 78 | gopkg.in/inf.v0 v0.9.1 // indirect 79 | k8s.io/api v0.22.2 // indirect 80 | k8s.io/klog v1.0.0 // indirect 81 | k8s.io/klog/v2 v2.130.1 // indirect 82 | k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 // indirect 83 | k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect 84 | sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect 85 | sigs.k8s.io/yaml v1.4.0 // indirect 86 | ) 87 | 88 | replace ( 89 | k8s.io/api => k8s.io/api v0.20.2 90 | k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.20.2 91 | k8s.io/apimachinery => k8s.io/apimachinery v0.20.2 92 | k8s.io/apiserver => k8s.io/apiserver v0.20.2 93 | k8s.io/cli-runtime => k8s.io/cli-runtime v0.20.2 94 | k8s.io/client-go => k8s.io/client-go v0.20.2 95 | k8s.io/cloud-provider => k8s.io/cloud-provider v0.20.2 96 | k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.20.2 97 | k8s.io/code-generator => k8s.io/code-generator v0.20.2 98 | k8s.io/component-base => k8s.io/component-base v0.20.2 99 | k8s.io/component-helpers => k8s.io/component-helpers v0.22.2 100 | k8s.io/controller-manager => k8s.io/controller-manager v0.20.2 101 | k8s.io/cri-api => k8s.io/cri-api v0.20.2 102 | k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.20.2 103 | k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.20.2 104 | k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.20.2 105 | k8s.io/kube-proxy => k8s.io/kube-proxy v0.20.2 106 | k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.20.2 107 | k8s.io/kubectl => k8s.io/kubectl v0.20.2 108 | k8s.io/kubelet => k8s.io/kubelet v0.20.2 109 | k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.20.2 110 | k8s.io/metrics => k8s.io/metrics v0.20.2 111 | k8s.io/mount-utils => k8s.io/mount-utils v0.20.2 112 | k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.20.2 113 | k8s.io/scheduler => k8s.io/schduler v0.20.2 114 | ) 115 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # git gofmt pre-commit hook 4 | # 5 | # This script does not handle file names that contain spaces. 6 | gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$') 7 | [ -z "$gofiles" ] && exit 0 8 | 9 | unformatted=$(gofmt -l $gofiles) 10 | [ -z "$unformatted" ] && exit 0 11 | 12 | # Some files are not gofmt'd. Print message and fail. 13 | 14 | echo >&2 "Go files must be formatted with gofmt. Please run:" 15 | for fn in $unformatted; do 16 | echo >&2 " gofmt -w $PWD/$fn" 17 | done 18 | 19 | exit 1 20 | 21 | -------------------------------------------------------------------------------- /k8sutils/k8sutils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020-2025 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package k8sutils 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "time" 22 | 23 | "github.com/kubernetes-csi/csi-lib-utils/leaderelection" 24 | log "github.com/sirupsen/logrus" 25 | "k8s.io/client-go/kubernetes" 26 | "k8s.io/client-go/rest" 27 | "k8s.io/client-go/tools/clientcmd" 28 | ) 29 | 30 | var ( 31 | buildConfigFromFlags = clientcmd.BuildConfigFromFlags 32 | newForConfig = kubernetes.NewForConfig 33 | inClusterConfig = rest.InClusterConfig 34 | ) 35 | 36 | // leaderElection interface 37 | type leaderElection interface { 38 | Run() error 39 | WithNamespace(namespace string) 40 | } 41 | 42 | // ExitFunc is a variable that holds the os.Exit function 43 | var ExitFunc = os.Exit 44 | 45 | // RunLeaderElection runs the leader election and handles errors 46 | func RunLeaderElection(le leaderElection) { 47 | if err := le.Run(); err != nil { 48 | _, _ = fmt.Fprintf(os.Stderr, "failed to initialize leader election: %v", err) 49 | ExitFunc(1) 50 | } 51 | } 52 | 53 | // CreateKubeClientSet - Returns kubeclient set 54 | func CreateKubeClientSet(kubeconfig string) (*kubernetes.Clientset, error) { 55 | var clientset *kubernetes.Clientset 56 | if kubeconfig != "" { 57 | // use the current context in kubeconfig 58 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 59 | if err != nil { 60 | return nil, err 61 | } 62 | // create the clientset 63 | clientset, err = kubernetes.NewForConfig(config) 64 | if err != nil { 65 | return nil, err 66 | } 67 | } else { 68 | config, err := rest.InClusterConfig() 69 | if err != nil { 70 | return nil, err 71 | } 72 | // creates the clientset 73 | clientset, err = kubernetes.NewForConfig(config) 74 | if err != nil { 75 | return nil, err 76 | } 77 | } 78 | return clientset, nil 79 | } 80 | 81 | // LeaderElection - Initialize leader selection 82 | func LeaderElection(clientset *kubernetes.Clientset, lockName string, namespace string, 83 | leaderElectionRenewDeadline, leaderElectionLeaseDuration, leaderElectionRetryPeriod time.Duration, runFunc func(ctx context.Context), 84 | ) { 85 | log.Info("Starting leader election setup...") 86 | 87 | le := leaderelection.NewLeaderElection(clientset, lockName, runFunc) 88 | log.Infof("Leader election created with lock name: %s", lockName) 89 | 90 | le.WithNamespace(namespace) 91 | log.Infof("Namespace set to: %s", namespace) 92 | 93 | le.WithLeaseDuration(leaderElectionLeaseDuration) 94 | log.Infof("Lease duration set to: %v", leaderElectionLeaseDuration) 95 | 96 | le.WithRenewDeadline(leaderElectionRenewDeadline) 97 | log.Infof("Renew deadline set to: %v", leaderElectionRenewDeadline) 98 | 99 | le.WithRetryPeriod(leaderElectionRetryPeriod) 100 | log.Infof("Retry period set to: %v", leaderElectionRetryPeriod) 101 | 102 | log.Info("Running leader election...") 103 | RunLeaderElection(le) 104 | log.Info("Leader election completed.") 105 | } 106 | -------------------------------------------------------------------------------- /k8sutils/k8sutils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package k8sutils 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "io/ioutil" 22 | "os" 23 | "testing" 24 | "time" 25 | 26 | "github.com/stretchr/testify/mock" 27 | "github.com/stretchr/testify/require" 28 | "k8s.io/client-go/kubernetes" 29 | "k8s.io/client-go/rest" 30 | ) 31 | 32 | var exitFunc = os.Exit 33 | 34 | type MockLeaderElection struct { 35 | mock.Mock 36 | } 37 | 38 | func (m *MockLeaderElection) Run() error { 39 | args := m.Called() 40 | return args.Error(0) 41 | } 42 | 43 | func (m *MockLeaderElection) WithNamespace(namespace string) { 44 | m.Called(namespace) 45 | } 46 | 47 | // MockExit is a mock implementation of os.Exit 48 | var MockExit = func(code int) { 49 | fmt.Printf("os.Exit(%d) called\n", code) 50 | } 51 | 52 | func TestLeaderElection(t *testing.T) { 53 | clientset := &kubernetes.Clientset{} // Mock or use a fake clientset if needed 54 | runFunc := func(_ context.Context) { 55 | fmt.Println("Running leader function") 56 | } 57 | 58 | mockLE := new(MockLeaderElection) 59 | mockLE.On("Run").Return(nil) // Mocking a successful run 60 | 61 | // Override exitFunc to prevent test from exiting 62 | exitCalled := false 63 | oldExit := exitFunc 64 | defer func() { recover(); exitFunc = oldExit }() 65 | exitFunc = func(_ int) { exitCalled = true; panic("exitFunc called") } 66 | 67 | // Simulate LeaderElection function 68 | func() { 69 | defer func() { 70 | if r := recover(); r != nil { 71 | exitCalled = true 72 | } 73 | }() 74 | LeaderElection(clientset, "test-lock", "test-namespace", time.Second, time.Second*2, time.Second*3, runFunc) 75 | }() 76 | 77 | if !exitCalled { 78 | t.Errorf("exitFunc was called unexpectedly") 79 | } 80 | } 81 | 82 | func TestCreateKubeClientSet(t *testing.T) { 83 | // Test cases 84 | tests := []struct { 85 | name string 86 | kubeconfig string 87 | configErr error 88 | clientErr error 89 | wantErr bool 90 | }{ 91 | { 92 | name: "Valid kubeconfig", 93 | kubeconfig: "valid_kubeconfig", 94 | configErr: nil, 95 | clientErr: nil, 96 | wantErr: false, 97 | }, 98 | { 99 | name: "Invalid kubeconfig", 100 | kubeconfig: "invalid_kubeconfig", 101 | configErr: errors.New("config error"), 102 | clientErr: nil, 103 | wantErr: true, 104 | }, 105 | { 106 | name: "In-cluster config", 107 | kubeconfig: "", 108 | configErr: nil, 109 | clientErr: nil, 110 | wantErr: false, 111 | }, 112 | { 113 | name: "In-cluster config error", 114 | kubeconfig: "", 115 | configErr: errors.New("config error"), 116 | clientErr: nil, 117 | wantErr: true, 118 | }, 119 | { 120 | name: "New for config error", 121 | kubeconfig: "", 122 | configErr: nil, 123 | clientErr: errors.New("client error"), 124 | wantErr: true, 125 | }, 126 | { 127 | name: "New for config error with valid kubeconfig", 128 | kubeconfig: "valid_kubeconfig", 129 | configErr: nil, 130 | clientErr: errors.New("client error"), 131 | wantErr: true, 132 | }, 133 | } 134 | 135 | // Save original functions 136 | origBuildConfigFromFlags := buildConfigFromFlags 137 | origInClusterConfig := inClusterConfig 138 | origNewForConfig := newForConfig 139 | 140 | // Restore original functions after tests 141 | defer func() { 142 | buildConfigFromFlags = origBuildConfigFromFlags 143 | inClusterConfig = origInClusterConfig 144 | newForConfig = origNewForConfig 145 | }() 146 | 147 | for _, tt := range tests { 148 | t.Run(tt.name, func(t *testing.T) { 149 | // Create a temporary kubeconfig file for the valid kubeconfig test case 150 | if tt.name == "Valid kubeconfig" || tt.name == "New for config error with valid kubeconfig" { 151 | tmpFile, err := ioutil.TempFile("", "kubeconfig") 152 | require.NoError(t, err) 153 | defer os.Remove(tmpFile.Name()) 154 | 155 | kubeconfigContent := ` 156 | apiVersion: v1 157 | clusters: 158 | - cluster: 159 | server: https://127.0.0.1:6443 160 | name: test-cluster 161 | contexts: 162 | - context: 163 | cluster: test-cluster 164 | user: test-user 165 | name: test-context 166 | current-context: test-context 167 | kind: Config 168 | preferences: {} 169 | users: 170 | - name: test-user 171 | user: 172 | token: test-token 173 | ` 174 | err = ioutil.WriteFile(tmpFile.Name(), []byte(kubeconfigContent), 0o600) 175 | require.NoError(t, err) 176 | tt.kubeconfig = tmpFile.Name() 177 | } 178 | 179 | // Mock environment variables for in-cluster config 180 | if tt.name == "In-cluster config" || tt.name == "In-cluster config error" { 181 | os.Setenv("KUBERNETES_SERVICE_HOST", "127.0.0.1") 182 | os.Setenv("KUBERNETES_SERVICE_PORT", "443") 183 | defer os.Unsetenv("KUBERNETES_SERVICE_HOST") 184 | defer os.Unsetenv("KUBERNETES_SERVICE_PORT") 185 | } 186 | 187 | // Mock functions 188 | buildConfigFromFlags = func(_, _ string) (*rest.Config, error) { 189 | return &rest.Config{}, tt.configErr 190 | } 191 | inClusterConfig = func() (*rest.Config, error) { 192 | return &rest.Config{}, tt.configErr 193 | } 194 | newForConfig = func(_ *rest.Config) (*kubernetes.Clientset, error) { 195 | if tt.clientErr != nil { 196 | return nil, tt.clientErr 197 | } 198 | return &kubernetes.Clientset{}, nil 199 | } 200 | 201 | clientset, err := CreateKubeClientSet(tt.kubeconfig) 202 | if (err != nil) != tt.wantErr { 203 | return 204 | } 205 | if !tt.wantErr && clientset == nil { 206 | t.Errorf("CreateKubeClientSet() = nil, want non-nil") 207 | } 208 | }) 209 | } 210 | } 211 | 212 | func TestRunLeaderElection(t *testing.T) { 213 | // Save the original ExitFunc 214 | origExitFunc := ExitFunc 215 | // Restore the original ExitFunc after the test 216 | defer func() { ExitFunc = origExitFunc }() 217 | 218 | // Override ExitFunc with the mock implementation 219 | ExitFunc = MockExit 220 | 221 | t.Run("RunLeaderElection_Success", func(t *testing.T) { 222 | mockLE := new(MockLeaderElection) 223 | mockLE.On("Run").Return(nil) 224 | 225 | RunLeaderElection(mockLE) 226 | 227 | mockLE.AssertExpectations(t) 228 | }) 229 | 230 | t.Run("RunLeaderElection_Error", func(t *testing.T) { 231 | mockLE := new(MockLeaderElection) 232 | mockLE.On("Run").Return(errors.New("leader election error")) 233 | 234 | // Capture the output 235 | oldStderr := os.Stderr 236 | r, w, _ := os.Pipe() 237 | os.Stderr = w 238 | 239 | // Run the function 240 | RunLeaderElection(mockLE) 241 | 242 | // Restore stderr 243 | w.Close() 244 | os.Stderr = oldStderr 245 | 246 | // Read the captured output 247 | var buf [1024]byte 248 | n, _ := r.Read(buf[:]) 249 | output := string(buf[:n]) 250 | 251 | require.Contains(t, output, "failed to initialize leader election: leader election error") 252 | mockLE.AssertExpectations(t) 253 | }) 254 | } 255 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020-2025 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "flag" 20 | "fmt" 21 | "os" 22 | "strings" 23 | "time" 24 | 25 | "github.com/dell/gocsi" 26 | "k8s.io/client-go/kubernetes" 27 | 28 | "github.com/dell/csi-unity/k8sutils" 29 | "github.com/dell/csi-unity/provider" 30 | "github.com/dell/csi-unity/service" 31 | ) 32 | 33 | type leaderElection interface { 34 | Run() error 35 | WithNamespace(namespace string) 36 | } 37 | 38 | var exitFunc = os.Exit 39 | 40 | func validateArgs(driverConfigParamsfile *string, driverName *string, driverSecret *string) { 41 | if *driverConfigParamsfile == "" { 42 | fmt.Fprintf(os.Stderr, "driver-config argument is mandatory") 43 | exitFunc(1) 44 | } 45 | if *driverName == "" { 46 | fmt.Fprintf(os.Stderr, "driver-name argument is mandatory") 47 | exitFunc(1) 48 | } 49 | if *driverSecret == "" { 50 | fmt.Fprintf(os.Stderr, "driver-secret argument is mandatory") 51 | exitFunc(3) 52 | } 53 | } 54 | 55 | func checkLeaderElectionError(err error) { 56 | if err != nil { 57 | fmt.Fprintf(os.Stderr, "failed to initialize leader election: %v", err) 58 | exitFunc(1) 59 | } 60 | } 61 | 62 | func main() { 63 | mainR(gocsi.Run, func(kubeconfig string) (kubernetes.Interface, error) { 64 | return k8sutils.CreateKubeClientSet(kubeconfig) 65 | }, func(clientset kubernetes.Interface, lockName, namespace string, renewDeadline, leaseDuration, retryPeriod time.Duration, run func(ctx context.Context)) { 66 | k8sutils.LeaderElection(clientset.(*kubernetes.Clientset), lockName, namespace, renewDeadline, leaseDuration, retryPeriod, run) 67 | }) 68 | } 69 | 70 | // main is ignored when this package is built as a go plug-in. 71 | func mainR(runFunc func(ctx context.Context, name, desc string, usage string, sp gocsi.StoragePluginProvider), createKubeClientSet func(kubeconfig string) (kubernetes.Interface, error), leaderElection func(clientset kubernetes.Interface, lockName, namespace string, renewDeadline, leaseDuration, retryPeriod time.Duration, run func(ctx context.Context))) { 72 | driverName := flag.String("driver-name", "", "driver name") 73 | driverSecret := flag.String("driver-secret", "", "driver secret yaml file") 74 | driverConfig := flag.String("driver-config", "", "driver config yaml file") 75 | enableLeaderElection := flag.Bool("leader-election", false, "boolean to enable leader election") 76 | leaderElectionNamespace := flag.String("leader-election-namespace", "", "namespace where leader election lease will be created") 77 | leaderElectionLeaseDuration := flag.Duration("leader-election-lease-duration", 15*time.Second, "Duration, in seconds, that non-leader candidates will wait to force acquire leadership") 78 | leaderElectionRenewDeadline := flag.Duration("leader-election-renew-deadline", 10*time.Second, "Duration, in seconds, that the acting leader will retry refreshing leadership before giving up.") 79 | leaderElectionRetryPeriod := flag.Duration("leader-election-retry-period", 5*time.Second, "Duration, in seconds, the LeaderElector clients should wait between tries of actions") 80 | flag.Parse() 81 | 82 | service.Name = *driverName 83 | service.DriverSecret = *driverSecret 84 | service.DriverConfig = *driverConfig 85 | // validate driver name, driver secret and driver config arguments 86 | validateArgs(driverConfig, driverName, driverSecret) 87 | 88 | // Always set X_CSI_DEBUG to false irrespective of what user has specified 89 | _ = os.Setenv(gocsi.EnvVarDebug, "false") 90 | // We always want to enable Request and Response logging (no reason for users to control this) 91 | _ = os.Setenv(gocsi.EnvVarReqLogging, "true") 92 | _ = os.Setenv(gocsi.EnvVarRepLogging, "true") 93 | 94 | kubeconfig := flag.String("kubeconfig", "", "absolute path to the kubeconfig file") 95 | flag.Parse() 96 | run := func(ctx context.Context) { 97 | runFunc( 98 | ctx, 99 | "csi-unity.dellemc.com", 100 | "An Unity Container Storage Interface (CSI) Plugin", 101 | usage, 102 | provider.New()) 103 | } 104 | if *enableLeaderElection == false { 105 | run(context.TODO()) 106 | } else { 107 | driverName := strings.Replace("csi-unity.dellemc.com", ".", "-", -1) 108 | lockName := fmt.Sprintf("driver-%s", driverName) 109 | k8sclientset, err := createKubeClientSet(*kubeconfig) 110 | checkLeaderElectionError(err) 111 | // Attempt to become leader and start the driver 112 | 113 | leaderElection(k8sclientset, lockName, *leaderElectionNamespace, 114 | *leaderElectionRenewDeadline, *leaderElectionLeaseDuration, *leaderElectionRetryPeriod, run) 115 | } 116 | } 117 | 118 | const usage = ` 119 | X_CSI_UNITY_NODENAME 120 | Specifies the name of the node where the Node plugin is running. 121 | The default value is empty. 122 | ` 123 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "flag" 22 | "fmt" 23 | "os" 24 | "testing" 25 | "time" 26 | 27 | "github.com/dell/gocsi" 28 | "github.com/stretchr/testify/mock" 29 | "github.com/stretchr/testify/require" 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | "k8s.io/client-go/kubernetes" 32 | "k8s.io/client-go/kubernetes/fake" 33 | "k8s.io/client-go/tools/leaderelection" 34 | "k8s.io/client-go/tools/leaderelection/resourcelock" 35 | ) 36 | 37 | type MockGocsi struct { 38 | mock.Mock 39 | } 40 | 41 | func (m *MockGocsi) Run(ctx context.Context, name, desc, usage string, sp gocsi.StoragePluginProvider) { 42 | m.Called(ctx, name, desc, usage, sp) 43 | } 44 | 45 | var osExit = os.Exit 46 | 47 | func expectMockExit(int) { 48 | osExit(0) 49 | } 50 | 51 | func mockExit(int) { 52 | panic("os.Exit called") 53 | } 54 | 55 | func mockCreateKubeClientSet(_ string) (kubernetes.Interface, error) { 56 | return fake.NewSimpleClientset(), nil 57 | } 58 | 59 | func mockLeaderElection(_ kubernetes.Interface, _, _ string, _, _, _ time.Duration, run func(ctx context.Context)) { 60 | // Mock leader election logic 61 | run(context.TODO()) 62 | } 63 | 64 | func TestMainFunctionWithoutLeaderElection(t *testing.T) { 65 | // Save the original command-line arguments and restore them after the test 66 | origArgs := os.Args 67 | // Reset the flag.CommandLine 68 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 69 | 70 | defer func() { os.Args = origArgs }() 71 | 72 | // Mock the gocsi.Run function 73 | mockGocsi := new(MockGocsi) 74 | 75 | // Mock os.Exit 76 | osExit = mockExit 77 | defer func() { osExit = os.Exit }() 78 | 79 | // Test case: Leader election disabled 80 | t.Run("LeaderElectionDisabled", func(t *testing.T) { 81 | // Create a new flag set for this test case 82 | flagSet := flag.NewFlagSet("test", flag.ExitOnError) 83 | os.Args = []string{"cmd", "--leader-election=false", "--driver-config=config.yaml", "--driver-name=csi-unity.dellemc.com", "--driver-secret=secret.yaml"} 84 | flagSet.Bool("leader-election", false, "Enables leader election.") 85 | flagSet.String("driver-config", "config.yaml", "yaml file with driver config params") 86 | flagSet.String("driver-name", "csi-unity.dellemc.com", "drivername") 87 | flagSet.String("driver-secret", "secret.yaml", "driver secret yaml file") 88 | flagSet.Parse(os.Args[1:]) 89 | 90 | mockGocsi.On("Run", mock.Anything, "csi-unity.dellemc.com", "An Unity Container Storage Interface (CSI) Plugin", usage, mock.Anything).Return() 91 | 92 | defer func() { 93 | if r := recover(); r != nil { 94 | t.Errorf("os.Exit was called: %v", r) 95 | } 96 | }() 97 | 98 | mainR(mockGocsi.Run, mockCreateKubeClientSet, mockLeaderElection) 99 | 100 | mockGocsi.AssertCalled(t, "Run", mock.Anything, "csi-unity.dellemc.com", "An Unity Container Storage Interface (CSI) Plugin", usage, mock.Anything) 101 | }) 102 | } 103 | 104 | func TestMainFunctionWithLeaderElection(t *testing.T) { 105 | // Save the original command-line arguments and restore them after the test 106 | origArgs := os.Args 107 | // Reset the flag.CommandLine 108 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 109 | defer func() { os.Args = origArgs }() 110 | 111 | // Mock the gocsi.Run function 112 | mockGocsi := new(MockGocsi) 113 | 114 | // Mock os.Exit 115 | osExit = mockExit 116 | defer func() { osExit = os.Exit }() 117 | 118 | // Set required environment variables for Kubernetes client 119 | os.Setenv("KUBERNETES_SERVICE_HOST", "127.0.0.1") 120 | os.Setenv("KUBERNETES_SERVICE_PORT", "6443") 121 | defer func() { 122 | os.Unsetenv("KUBERNETES_SERVICE_HOST") 123 | os.Unsetenv("KUBERNETES_SERVICE_PORT") 124 | }() 125 | 126 | // reset os.Args before next test 127 | os.Args = origArgs 128 | 129 | // Test case: Leader election enabled 130 | t.Run("LeaderElectionEnabled", func(t *testing.T) { 131 | // Create a new flag set for this test case 132 | flagSet := flag.NewFlagSet("test", flag.ExitOnError) 133 | os.Args = []string{ 134 | "cmd", 135 | "--leader-election=true", 136 | "--leader-election-namespace=default", 137 | "--leader-election-lease-duration=15s", 138 | "--leader-election-renew-deadline=10s", 139 | "--leader-election-retry-period=5s", 140 | "--driver-config=config.yaml", 141 | "--driver-name=csi-unity.dellemc.com", 142 | "--driver-secret=secret.yaml", 143 | } 144 | flagSet.Bool("leader-election", true, "Enables leader election.") 145 | flagSet.String("leader-election-namespace", "default", "The namespace where leader election lease will be created.") 146 | flagSet.Duration("leader-election-lease-duration", 15*time.Second, "Duration, in seconds, that non-leader candidates will wait to force acquire leadership") 147 | flagSet.Duration("leader-election-renew-deadline", 10*time.Second, "Duration, in seconds, that the acting leader will retry refreshing leadership before giving up.") 148 | flagSet.Duration("leader-election-retry-period", 5*time.Second, "Duration, in seconds, the LeaderElector clients should wait between tries of actions") 149 | flagSet.String("driver-config", "config.yaml", "yaml file with driver config params") 150 | flagSet.String("driver-name", "csi-unity.dellemc.com", "driver name") 151 | flagSet.String("driver-secret", "secret.yaml", "driver secret yaml file") 152 | flagSet.Parse(os.Args[1:]) 153 | 154 | // Mock Kubernetes client 155 | client := fake.NewSimpleClientset() 156 | 157 | lock := &resourcelock.LeaseLock{ 158 | LeaseMeta: metav1.ObjectMeta{ 159 | Name: "driver-csi-unity-dellemc-com", 160 | Namespace: "default", 161 | }, 162 | Client: client.CoordinationV1(), 163 | LockConfig: resourcelock.ResourceLockConfig{ 164 | Identity: "test-identity", 165 | }, 166 | } 167 | 168 | leaderElectionConfig := leaderelection.LeaderElectionConfig{ 169 | Lock: lock, 170 | LeaseDuration: 15 * time.Second, 171 | RenewDeadline: 10 * time.Second, 172 | RetryPeriod: 5 * time.Second, 173 | Callbacks: leaderelection.LeaderCallbacks{ 174 | OnStartedLeading: func(_ context.Context) { 175 | // Perform leader-specific tasks here 176 | }, 177 | OnStoppedLeading: func() { 178 | }, 179 | OnNewLeader: func(_ string) { 180 | // Perform leader-specific tasks here 181 | }, 182 | }, 183 | } 184 | 185 | mockLeaderElection := func(_ kubernetes.Interface, lockName, namespace string, renewDeadline, leaseDuration, retryPeriod time.Duration, _ func(ctx context.Context)) { 186 | require.Equal(t, "driver-csi-unity-dellemc-com", lockName) 187 | require.Equal(t, "default", namespace) 188 | require.Equal(t, 10*time.Second, renewDeadline) 189 | require.Equal(t, 15*time.Second, leaseDuration) 190 | require.Equal(t, 5*time.Second, retryPeriod) 191 | go leaderelection.RunOrDie(context.TODO(), leaderElectionConfig) 192 | } 193 | defer func() { 194 | if r := recover(); r != nil { 195 | t.Errorf("os.Exit was called: %v", r) 196 | } 197 | }() 198 | 199 | mainR(mockGocsi.Run, mockCreateKubeClientSet, mockLeaderElection) 200 | }) 201 | } 202 | 203 | var exitCode int 204 | 205 | func fakeExit(code int) { 206 | exitCode = code 207 | } 208 | 209 | func TestCheckLeaderElectionError(_ *testing.T) { 210 | // Mock the function to return an error 211 | err := errors.New("mock error") 212 | 213 | // Capture the output 214 | _, out, _ := os.Pipe() 215 | 216 | // Replace os.Exit with fakeExit 217 | oldExit := exitFunc 218 | exitFunc = fakeExit 219 | defer func() { exitFunc = oldExit }() 220 | 221 | // Call the function that includes the error handling 222 | checkLeaderElectionError(err) 223 | 224 | // Restore the original Stderr 225 | out.Close() 226 | } 227 | 228 | func TestValidateArgs(t *testing.T) { 229 | // Save the original exit function and restore it after the test 230 | origExit := exitFunc 231 | defer func() { exitFunc = origExit }() 232 | 233 | tests := []struct { 234 | name string 235 | driverConfigParamsfile string 236 | driverName string 237 | driverSecret string 238 | expectedExitCode int 239 | }{ 240 | {"MissingDriverConfig", "", "driverName", "driverSecret", 1}, 241 | {"MissingDriverName", "driverConfig", "", "driverSecret", 1}, 242 | {"MissingDriverSecret", "driverConfig", "driverName", "", 3}, 243 | {"AllArgsPresent", "driverConfig", "driverName", "driverSecret", 0}, 244 | } 245 | 246 | for _, tt := range tests { 247 | t.Run(tt.name, func(t *testing.T) { 248 | // Mock the exit function 249 | exitCode := 0 250 | exitFunc = func(code int) { 251 | exitCode = code 252 | panic(fmt.Sprintf("os.Exit called with code %d", code)) 253 | } 254 | 255 | // Convert strings to pointers 256 | driverConfigParamsfile := tt.driverConfigParamsfile 257 | driverName := tt.driverName 258 | driverSecret := tt.driverSecret 259 | 260 | // Run the function and capture the panic 261 | defer func() { 262 | if r := recover(); r != nil { 263 | if exitCode != tt.expectedExitCode { 264 | t.Errorf("expected exit code %d, got %d", tt.expectedExitCode, exitCode) 265 | } 266 | } else if tt.expectedExitCode != 0 { 267 | t.Errorf("expected exit code %d, but no panic occurred", tt.expectedExitCode) 268 | } 269 | }() 270 | 271 | validateArgs(&driverConfigParamsfile, &driverName, &driverSecret) 272 | 273 | if tt.expectedExitCode == 0 && exitCode != 0 { 274 | t.Errorf("expected no exit, but got exit code %d", exitCode) 275 | } 276 | }) 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /provider/provider.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package provider 16 | 17 | import ( 18 | "github.com/dell/csi-unity/service" 19 | "github.com/dell/gocsi" 20 | ) 21 | 22 | // New returns a new CSI Storage Plug-in Provider. 23 | func New() gocsi.StoragePluginProvider { 24 | svc := service.New() 25 | return &gocsi.StoragePlugin{ 26 | Controller: svc, 27 | Identity: svc, 28 | Node: svc, 29 | BeforeServe: svc.BeforeServe, 30 | RegisterAdditionalServers: svc.RegisterAdditionalServers, 31 | 32 | EnvVars: []string{ 33 | // Enable request validation 34 | gocsi.EnvVarSpecReqValidation + "=true", 35 | 36 | // Enable serial volume access 37 | gocsi.EnvVarSerialVolAccess + "=true", 38 | 39 | // Treat the following fields as required: 40 | // * ControllerPublishVolumeRequest.NodeId 41 | // * GetNodeIDResponse.NodeId 42 | // gocsi.EnvVarRequireNodeID + "=true", 43 | 44 | // Treat the following fields as required: 45 | // * ControllerPublishVolumeResponse.PublishInfo 46 | // * NodePublishVolumeRequest.PublishInfo 47 | // gocsi.EnvVarRequirePubVolInfo + "=false", 48 | }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /provider/provider_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package provider 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/dell/csi-unity/service" 21 | "github.com/dell/gocsi" 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | func TestNew(t *testing.T) { 27 | pluginProvider := New() 28 | require.NotNil(t, pluginProvider, "StoragePluginProvider should not be nil") 29 | 30 | plugin, ok := pluginProvider.(*gocsi.StoragePlugin) 31 | require.True(t, ok, "Returned provider should be of type *gocsi.StoragePlugin") 32 | require.NotNil(t, plugin, "*gocsi.StoragePlugin instance should not be nil") 33 | 34 | svc := service.New() 35 | 36 | assert.Equal(t, svc, plugin.Controller, "Controller should be equal to the service") 37 | assert.Equal(t, svc, plugin.Identity, "Identity should be equal to the service") 38 | assert.Equal(t, svc, plugin.Node, "Node should be equal to the service") 39 | assert.NotNil(t, plugin.BeforeServe, "BeforeServe should not be nil") 40 | assert.NotNil(t, plugin.RegisterAdditionalServers, "RegisterAdditionalServers should not be nil") 41 | 42 | expectedEnvVars := []string{ 43 | gocsi.EnvVarSpecReqValidation + "=true", 44 | gocsi.EnvVarSerialVolAccess + "=true", 45 | } 46 | assert.ElementsMatch(t, expectedEnvVars, plugin.EnvVars, "Environment variables should match the expected values") 47 | } 48 | -------------------------------------------------------------------------------- /samples/secret/emptysecret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: unity-certs-0 5 | namespace: unity 6 | type: Opaque 7 | data: 8 | cert-0: "" 9 | -------------------------------------------------------------------------------- /samples/secret/secret.yaml: -------------------------------------------------------------------------------- 1 | storageArrayList: 2 | # Array ID of Unity XT 3 | # Default value: None 4 | # Examples: "ABC00000000001" 5 | - arrayId: "ABC00000000001" 6 | # username for connecting to Unity XT Unisphere REST API server 7 | # Default value: None 8 | # Examples: "user1" 9 | username: "user" 10 | # password for connecting to Unity XT Unisphere REST API server 11 | # Default value: None 12 | # Examples: "Password" 13 | password: "password" 14 | # HTTPS endpoint of the Unity XT Unisphere REST API server 15 | # Default value: None 16 | # Examples: "https://10.0.0.0" 17 | endpoint: "https://1.2.3.4/" 18 | # indicates if client side validation of server's certificate can be skipped 19 | # Default value: None 20 | # Examples: false, true 21 | skipCertificateValidation: true 22 | # default array (would be used by storage classes without arrayId parameter) 23 | # Default value: None 24 | # Examples: false, true 25 | isDefault: true 26 | 27 | # # To add more Unity XT arrays, uncomment the following lines and provide the required values 28 | # - arrayId: "ABC00000000002" 29 | # username: "user" 30 | # password: "password" 31 | # endpoint: "https://1.2.3.5/" 32 | # skipCertificateValidation: true 33 | # isDefault: false 34 | -------------------------------------------------------------------------------- /samples/storageclass/unity-fc.yaml: -------------------------------------------------------------------------------- 1 | # This is a sample manifest for utilizing the topology feature and mount options. 2 | # PVCs created using this storage class will be scheduled 3 | # only on the nodes with FC connectivity to the array 4 | 5 | # Change all instances of to the array serial ID of the unisphere instance used 6 | 7 | # Provide mount options through "mountOptions" attribute 8 | # to create PVCs with mount options. 9 | 10 | apiVersion: storage.k8s.io/v1 11 | kind: StorageClass 12 | metadata: 13 | name: unity--fc 14 | # If using custom driver name, change the following to point to the custom name 15 | # Default value: csi-unity.dellemc.com 16 | # Optional: false 17 | # Examples: "csi-driver-unity", "csi-unity.dellemc.com" 18 | provisioner: csi-unity.dellemc.com 19 | # reclaimPolicy - Configure what happens to a Persistent Volume when the PVC 20 | # it is bound to is to be deleted 21 | # Allowed values: 22 | # Delete: the underlying persistent volume will be deleted along with the persistent volume claim. 23 | # Retain: the underlying persistent volume remain. 24 | # Optional: false 25 | # Default value: None 26 | reclaimPolicy: Delete 27 | # allowVolumeExpansion - Attribute to allow volume expansion 28 | # Allowed values: 29 | # "true" - Volume can be resized 30 | # "false" - Volume cannot be resized 31 | # Optional: true 32 | # Default value: "true" 33 | allowVolumeExpansion: true 34 | # volumeBindingMode- controls when volume binding and dynamic provisioning should occur. 35 | # Allowed values: 36 | # Immediate- indicates that volume binding and dynamic provisioning 37 | # occurs once the PersistentVolumeClaim is created 38 | # WaitForFirstConsumer- will delay the binding and provisioning of a PersistentVolume 39 | # until a Pod using the PersistentVolumeClaim is created 40 | ## Optional: true 41 | # Default value: "Immediate" 42 | volumeBindingMode: WaitForFirstConsumer 43 | parameters: 44 | # protocol - Defines"FC" or "FIBRE" for fibrechannel, "ISCSI" for iSCSI, or "" for autoselection. 45 | # Allowed values: 46 | # "FC" - Fiber Channel protocol 47 | # "FIBER" - Fiber Channel protocol 48 | # "ISCSI" - iSCSI protocol 49 | # "" - Automatic selection of transport protocol 50 | # Optional: false 51 | # Default value: "" 52 | protocol: FC 53 | # arrayId - Serial Id of the array that will be used for provisioning. 54 | # Allowed values: String 55 | # Default value: None 56 | # Optional: false 57 | # Examples: "APM000000001", "APM000000002" 58 | arrayId: 59 | # storagePool - Defines storage pool. The value should be picked from the column labeled "CLI ID" of Pools in the Unisphere GUI. 60 | # Allowed values: String 61 | # Default value: None 62 | # Optional: false 63 | # Examples: pool_0 64 | storagePool: 65 | # thinProvisioned- Defines Boolean to choose value of thinProvisioned while creating a new volume 66 | # Allowed values: 67 | # "true" - for thin provision 68 | # "false" - for thick provision 69 | # Optional: true 70 | # Default value: false 71 | thinProvisioned: "true" 72 | # isDataReductionEnabled - Defines Boolean to choose value of is DataReductionEnabled while creating a new volume 73 | # Allowed values: 74 | # "true" - Enables data reduction for all-flash storage pool. 75 | # "false" - Disables data reduction. 76 | # Optional: true 77 | # Default value: false 78 | isDataReductionEnabled: "true" 79 | # TieringPolicy - Tiering policy to be used during provisioning 80 | # Requires FAST VP license. 81 | # Allowed values: String 82 | # Optional: true 83 | # Examples: "0" 84 | # Default value: None 85 | # Accepted values: 86 | # "0" for "Start High Then Auto-Tier" 87 | # "1" for "Auto-Tier" 88 | # "2" for "Highest Available Tier" 89 | # "3" for "Lowest Available Tier" 90 | tieringPolicy: 91 | # hostIOLimitName - Insert Host IO Limit Name that is to be used for provisioning here. 92 | # Allowed values: String 93 | # Optional: true 94 | # Default value: None 95 | # Examples: "Autotier" 96 | hostIOLimitName: 97 | # csi.storage.k8s.io/fstype - Set the filesystem type to format the new volume 98 | # Default value: ext4 99 | # Accepted values: 100 | # "ext4" 101 | # "xfs" 102 | csi.storage.k8s.io/fstype: xfs 103 | # Restrict provisioning to specific topologies 104 | # Allowed values: map of key-value pairs 105 | # Optional: false 106 | # Default value: None 107 | # Examples: "apm0020280XXXX" , "apm0021340XXXX" 108 | # Here first three characters should be in small letters. 109 | allowedTopologies: 110 | - matchLabelExpressions: 111 | - key: csi-unity.dellemc.com/-fc 112 | values: 113 | - "true" 114 | # mountOptions - Defines mount input values. 115 | # Default value: [] 116 | # Optional: false 117 | # Examples: 118 | # "hard" - option for mounting with NFS 119 | # "context" - option for mounting with block storage 120 | mountOptions: ["", "", ..., ""] 121 | -------------------------------------------------------------------------------- /samples/storageclass/unity-iscsi.yaml: -------------------------------------------------------------------------------- 1 | # This is a sample manifest for utilizing the topology feature and mount options. 2 | # PVCs created using this storage class will be scheduled 3 | # only on the nodes with iSCSI connectivity to the array 4 | 5 | # Change all instances of to the array serial ID of the unisphere instance used 6 | 7 | # Provide mount options through "mountOptions" attribute 8 | # to create PVCs with mount options. 9 | 10 | apiVersion: storage.k8s.io/v1 11 | kind: StorageClass 12 | metadata: 13 | name: unity--iscsi 14 | # If using custom driver name, change the following to point to the custom name 15 | # Default value: None 16 | # Optional: false 17 | # Examples: "csi-driver-unity", "csi-unity.dellemc.com" 18 | provisioner: csi-unity.dellemc.com 19 | # reclaimPolicy - Configure what happens to a Persistent Volume when the PVC 20 | # it is bound to is to be deleted 21 | # Allowed values: 22 | # Delete: the underlying persistent volume will be deleted along with the persistent volume claim. 23 | # Retain: the underlying persistent volume remain. 24 | # Optional: false 25 | # Default value: None 26 | reclaimPolicy: Delete 27 | # allowVolumeExpansion - Attribute to allow volume expansion 28 | # Allowed values: 29 | # "true" - Volume can be resized 30 | # "false" - Volume cannot be resized 31 | # Default value: "true" 32 | # Optional: true 33 | allowVolumeExpansion: true 34 | # volumeBindingMode- controls when volume binding and dynamic provisioning should occur. 35 | # Allowed values: 36 | # Immediate- indicates that volume binding and dynamic provisioning 37 | # occurs once the PersistentVolumeClaim is created 38 | # WaitForFirstConsumer- will delay the binding and provisioning of a PersistentVolume 39 | # until a Pod using the PersistentVolumeClaim is created 40 | # Default value: "Immediate" 41 | # Optional: true 42 | volumeBindingMode: WaitForFirstConsumer 43 | parameters: 44 | # protocol - Defines"FC" or "FIBRE" for fibrechannel, "ISCSI" for iSCSI, or "" for autoselection. 45 | # Allowed values: 46 | # "FC" - Fiber Channel protocol 47 | # "FIBER" - Fiber Channel protocol 48 | # "ISCSI" - iSCSI protocol 49 | # "" - Automatic selection of transport protocol 50 | # Default value: "" 51 | # Optional: false 52 | protocol: iSCSI 53 | # arrayId - Serial Id of the array that will be used for provisioning. 54 | # Allowed values: String 55 | # Default value: None 56 | # Optional: false 57 | # Examples: "APM000000001", "APM000000002" 58 | arrayId: 59 | # storagePool - Defines storage pool. The value should be picked from the column labeled "CLI ID" of Pools in the Unisphere GUI. 60 | # Allowed values: String 61 | # Default value: None 62 | # Optional: false 63 | # Examples: pool_0 64 | storagePool: 65 | # thinProvisioned- Defines Boolean to choose value of thinProvisioned while creating a new volume 66 | # Allowed values: 67 | # "true" - for thin provision 68 | # "false" - for thick provision 69 | # Default value: false 70 | # Optional: true 71 | thinProvisioned: "true" 72 | # isDataReductionEnabled - Defines Boolean to choose value of is DataReductionEnabled while creating a new volume 73 | # Allowed values: 74 | # "true" - Enables data reduction for all-flash storage pool. 75 | # "false" - Disables data reduction. 76 | # Optional: true 77 | # Default value: false 78 | isDataReductionEnabled: "true" 79 | # TieringPolicy - Tiering policy to be used during provisioning 80 | # Requires FAST VP license. 81 | # Allowed values: String 82 | # Optional: true 83 | # Examples: "0" 84 | # Default value: None 85 | # Accepted values: 86 | # "0" for "Start High Then Auto-Tier" 87 | # "1" for "Auto-Tier" 88 | # "2" for "Highest Available Tier" 89 | # "3" for "Lowest Available Tier" 90 | tieringPolicy: 91 | # hostIOLimitName- Insert Host IO Limit Name that is to be used for provisioning here. 92 | # Allowed values: String 93 | # Default value: None 94 | # Optional: true 95 | # Examples: "Autotier" 96 | hostIOLimitName: 97 | # csi.storage.k8s.io/fstype - Set the filesystem type to format the new volume 98 | # Default value: ext4 99 | # Accepted values: 100 | # "ext4" 101 | # "xfs" 102 | csi.storage.k8s.io/fstype: xfs 103 | # Restrict provisioning to specific topologies 104 | # Allowed values: map of key-value pairs 105 | # Default value: None 106 | # Optional: false 107 | # Examples: "apm0020280XXXX" , "apm0021340XXXX" 108 | # Here first three characters should be in small letters. 109 | allowedTopologies: 110 | - matchLabelExpressions: 111 | - key: csi-unity.dellemc.com/-iscsi 112 | values: 113 | - "true" 114 | # mountOptions - Defines mount input values. 115 | # Default value: [] 116 | # Optional: false 117 | # Examples: 118 | # "hard" - option for mounting with NFS 119 | # "context" - option for mounting with block storage 120 | mountOptions: ["", "", ..., ""] 121 | -------------------------------------------------------------------------------- /samples/storageclass/unity-nfs.yaml: -------------------------------------------------------------------------------- 1 | # This is a sample manifest for utilizing the topology feature and mount options. 2 | # PVCs created using this storage class will be scheduled on any available node 3 | 4 | # Change all instances of to the array serial ID of the unisphere instance used 5 | 6 | # Provide mount options through "mountOptions" attribute 7 | # to create PVCs with mount options. 8 | 9 | apiVersion: storage.k8s.io/v1 10 | kind: StorageClass 11 | metadata: 12 | name: unity--nfs 13 | # If using custom driver name, change the following to point to the custom name 14 | # Default value: csi-unity.dellemc.com 15 | # Examples: "csi-driver-unity", "csi-unity.dellemc.com" 16 | provisioner: csi-unity.dellemc.com 17 | # reclaimPolicy - Configure what happens to a Persistent Volume when the PVC 18 | # it is bound to is to be deleted 19 | # Allowed values: 20 | # Delete: the underlying persistent volume will be deleted along with the persistent volume claim. 21 | # Retain: the underlying persistent volume remain. 22 | # Optional: false 23 | # Default value: None 24 | reclaimPolicy: Delete 25 | # allowVolumeExpansion - Attribute to allow volume expansion 26 | # Allowed values: 27 | # "true" - Volume can be resized 28 | # "false" - Volume cannot be resized 29 | # Default value: "true" 30 | # Optional: true 31 | allowVolumeExpansion: true 32 | # volumeBindingMode- controls when volume binding and dynamic provisioning should occur. 33 | # Allowed values: 34 | # Immediate- indicates that volume binding and dynamic provisioning 35 | # occurs once the PersistentVolumeClaim is created 36 | # WaitForFirstConsumer- will delay the binding and provisioning of a PersistentVolume 37 | # until a Pod using the PersistentVolumeClaim is created 38 | # Default value: "Immediate" 39 | # Optional: true 40 | volumeBindingMode: WaitForFirstConsumer 41 | parameters: 42 | # protocol - Defines"FC" or "FIBRE" for fibrechannel, "ISCSI" for iSCSI, or "" for autoselection. 43 | # Allowed values: 44 | # "FC" - Fiber Channel protocol 45 | # "FIBER" - Fiber Channel protocol 46 | # "ISCSI" - iSCSI protocol 47 | # "" - Automatic selection of transport protocol 48 | # Default value: "" 49 | # Optional: false 50 | protocol: NFS 51 | # arrayId - Serial Id of the array that will be used for provisioning. 52 | # Allowed values: String 53 | # Default value: None 54 | # Examples: "APM000000001", "APM000000002" 55 | arrayId: 56 | # storagePool - Defines storage pool. The value should be picked from the column labeled "CLI ID" of Pools in the Unisphere GUI. 57 | # Allowed values: String 58 | # Default value: None 59 | # Optional: false 60 | # Examples: pool_0 61 | storagePool: 62 | # thinProvisioned- Defines Boolean to choose value of thinProvisioned while creating a new volume 63 | # Allowed values: 64 | # "true" - for thin provision 65 | # "false" - for thick provision 66 | # Default value: false 67 | # Optional: true 68 | thinProvisioned: "true" 69 | # isDataReductionEnabled - Defines Boolean to choose value of is DataReductionEnabled while creating a new volume 70 | # Allowed values: 71 | # "true" - Enables data reduction for all-flash storage pool. 72 | # "false" - Disables data reduction. 73 | # Optional: true 74 | # Default value: false 75 | isDataReductionEnabled: "true" 76 | # TieringPolicy - Tiering policy to be used during provisioning 77 | # Requires FAST VP license. 78 | # Allowed values: String 79 | # Optional: true 80 | # Examples: "0" 81 | # Default value: None 82 | # Accepted values: 83 | # "0" for "Start High Then Auto-Tier" 84 | # "1" for "Auto-Tier" 85 | # "2" for "Highest Available Tier" 86 | # "3" for "Lowest Available Tier" 87 | tieringPolicy: 88 | # nasServer - Defines storage NAS Servers. The value should be picked from the column labeled "CLI ID" of NAS Servers tab in the Unisphere GUI. 89 | # Default value: None 90 | # Optional: false 91 | # Examples : nasserver_0 92 | nasServer: 93 | # hostIoSize - Insert Host IO Size that is to be set for the filesystem. 94 | # Default value: None 95 | # Optional: false 96 | # Examples : 8192 97 | hostIoSize: 98 | # csi.storage.k8s.io/fstype - Set the filesystem type; it is ignored with NFS protocol if different from 'nfs' 99 | # Default value: ext4 100 | # Accepted values: 101 | # "nfs" 102 | csi.storage.k8s.io/fstype: nfs 103 | # Restrict provisioning to specific topologies 104 | # Allowed values: map of key-value pairs 105 | # Optional: false 106 | # Default value: None 107 | # Examples: "apm0020280XXXX" , "apm0021340XXXX" 108 | # Here first three characters should be in small letters. 109 | allowedTopologies: 110 | - matchLabelExpressions: 111 | - key: csi-unity.dellemc.com/-nfs 112 | values: 113 | - "true" 114 | # mountOptions - Defines mount input values. 115 | # Default value: [] 116 | # Optional: false 117 | # Examples: 118 | # "hard" - option for mounting with NFS 119 | # "context" - option for mounting with block storage 120 | mountOptions: ["", "", ..., ""] 121 | -------------------------------------------------------------------------------- /samples/storageclass/unity-plain.yaml: -------------------------------------------------------------------------------- 1 | # This is a sample manifest to create plain storageclass without any features like topology, mountOptions etc. 2 | 3 | # Change all instances of to the array serial ID of the unisphere instance used 4 | 5 | apiVersion: storage.k8s.io/v1 6 | kind: StorageClass 7 | metadata: 8 | name: unity--iscsi 9 | provisioner: csi-unity.dellemc.com 10 | reclaimPolicy: Delete 11 | allowVolumeExpansion: true 12 | # volumeBindingMode- controls when volume binding and dynamic provisioning should occur. 13 | # Allowed values: 14 | # Immediate- indicates that volume binding and dynamic provisioning 15 | # occurs once the PersistentVolumeClaim is created 16 | volumeBindingMode: Immediate 17 | parameters: 18 | protocol: iSCSI 19 | # arrayId - Serial Id of the array that will be used for provisioning. 20 | # Allowed values: String 21 | # Default value: None 22 | # Optional: false 23 | # Examples: "APM000000001", "APM000000002" 24 | arrayId: 25 | # storagePool - Define Storage pool CLI Id of the storage array. 26 | # Allowed values: String 27 | # Default value: None 28 | # Optional: false 29 | # Examples: pool_0 30 | storagePool: 31 | thinProvisioned: "true" 32 | isDataReductionEnabled: "true" 33 | # tieringPolicy - Define Tiering policy that is to be used for provisioning 34 | # Allowed values: integer 35 | # Default value: None 36 | # Optional: false 37 | # Examples: 0 38 | tieringPolicy: 39 | # hostIOLimitName - Insert Host IO Limit Name that is to be used for provisioning here. 40 | # Allowed values: String 41 | # Default value: None 42 | # Optional: false 43 | # Examples: "Autotyre" 44 | hostIOLimitName: 45 | # csi.storage.k8s.io/fstype - Set the filesystem type to format the new volume 46 | # Default value: ext4 47 | # Accepted values: 48 | # "ext4" 49 | # "xfs" 50 | csi.storage.k8s.io/fstype: ext4 51 | -------------------------------------------------------------------------------- /samples/volumesnapshotclass/snapclass-v1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: snapshot.storage.k8s.io/v1 2 | kind: VolumeSnapshotClass 3 | metadata: 4 | name: unity-snapclass 5 | driver: csi-unity.dellemc.com 6 | # Configure what happens to a VolumeSnapshotContent when the VolumeSnapshot object 7 | # it is bound to is to be deleted 8 | # Allowed values: 9 | # Delete: the underlying storage snapshot will be deleted along with the VolumeSnapshotContent object. 10 | # Retain: both the underlying snapshot and VolumeSnapshotContent remain. 11 | # Default value: None 12 | # Optional: false 13 | # Examples: Delete 14 | deletionPolicy: Delete 15 | -------------------------------------------------------------------------------- /scripts/check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright © 2021 Dell Inc. or its subsidiaries. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | if [ -f "../vendor" ]; then 16 | # Tell the applicable Go tools to use the vendor directory, if it exists. 17 | MOD_FLAGS="-mod=vendor" 18 | fi 19 | FMT_TMPFILE=/tmp/check_fmt 20 | FMT_COUNT_TMPFILE=${FMT_TMPFILE}.count 21 | 22 | fmt_count() { 23 | if [ ! -f $FMT_COUNT_TMPFILE ]; then 24 | echo "0" 25 | fi 26 | 27 | head -1 $FMT_COUNT_TMPFILE 28 | } 29 | 30 | fmt() { 31 | gofmt -d ./service ./common/ ./core/ ./k8sutils/ ./provider/ | tee $FMT_TMPFILE 32 | cat $FMT_TMPFILE | wc -l > $FMT_COUNT_TMPFILE 33 | if [ ! `cat $FMT_COUNT_TMPFILE` -eq "0" ]; then 34 | echo Found `cat $FMT_COUNT_TMPFILE` formatting issue\(s\). 35 | return 1 36 | fi 37 | } 38 | 39 | echo === Checking format... 40 | fmt 41 | FMT_RETURN_CODE=$? 42 | echo === Finished 43 | 44 | echo === Vetting... 45 | go vet ${MOD_FLAGS} ./service/... ./common/... 46 | VET_RETURN_CODE=$? 47 | echo === Finished 48 | 49 | echo === Linting... 50 | (command -v golint >/dev/null 2>&1 \ 51 | || GO111MODULE=on go install golang.org/x/lint/golint@latest) \ 52 | && golint --set_exit_status ./service/... ./common/... 53 | LINT_RETURN_CODE=$? 54 | echo === Finished 55 | 56 | # Report output. 57 | fail_checks=0 58 | [ "${FMT_RETURN_CODE}" != "0" ] && echo "Formatting checks failed!" && fail_checks=1 59 | [ "${VET_RETURN_CODE}" != "0" ] && echo "Vetting checks failed!" && fail_checks=1 60 | [ "${LINT_RETURN_CODE}" != "0" ] && echo "Linting checks failed!" && fail_checks=1 61 | 62 | exit ${fail_checks} -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | echo "Endpoint ${CSI_ENDPOINT}" 16 | if [[ -S ${CSI_ENDPOINT} ]]; then 17 | rm -rf ${CSI_ENDPOINT} 18 | echo "Removed endpoint $CSI_ENDPOINT" 19 | ls -l ${CSI_ENDPOINT} 20 | elif [[ ${CSI_ENDPOINT} == unix://* ]]; then 21 | ENDPOINT=${CSI_ENDPOINT/unix:\/\//} 22 | ls -l ${ENDPOINT} 23 | if [[ -S ${ENDPOINT} ]]; then 24 | rm -rf ${ENDPOINT} 25 | echo "Removed endpoint $ENDPOINT" 26 | fi 27 | fi 28 | exec /csi-unity "$@" 29 | -------------------------------------------------------------------------------- /service/envvars.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package service 16 | 17 | const ( 18 | // EnvEndpoint is the name of the environment variable used to set the 19 | // HTTP endpoint of the Unity XT Gateway 20 | EnvEndpoint = "X_CSI_UNITY_ENDPOINT" 21 | 22 | // EnvNodeName is the name of the enviroment variable used to set the 23 | // hostname where the node service is running 24 | EnvNodeName = "X_CSI_UNITY_NODENAME" 25 | 26 | // EnvAutoProbe is the name of the environment variable used to specify 27 | // that the controller service should automatically probe itself if it 28 | // receives incoming requests before having been probed, in direct 29 | // violation of the CSI spec 30 | EnvAutoProbe = "X_CSI_UNITY_AUTOPROBE" 31 | 32 | // EnvPvtMountDir is required to Node Unstage volume where the volume has been mounted 33 | // as a global mount via CSI-Unity v1.0 or v1.1 34 | EnvPvtMountDir = "X_CSI_PRIVATE_MOUNT_DIR" 35 | 36 | // EnvEphemeralStagingPath - Ephemeral staging path 37 | EnvEphemeralStagingPath = "X_CSI_EPHEMERAL_STAGING_PATH" 38 | 39 | // EnvISCSIChroot is the path to which the driver will chroot before 40 | // running any iscsi commands. This value should only be set when instructed 41 | // by technical support. 42 | EnvISCSIChroot = "X_CSI_ISCSI_CHROOT" 43 | 44 | // EnvKubeConfigPath indicates kubernetes configuration that has to be used by CSI Driver 45 | EnvKubeConfigPath = "KUBECONFIG" 46 | 47 | // SyncNodeInfoTimeInterval - Time interval to add node info to array. Default 60 minutes. 48 | // X_CSI_UNITY_SYNC_NODEINFO_INTERVAL has been deprecated and will be removes in a future release 49 | SyncNodeInfoTimeInterval = "X_CSI_UNITY_SYNC_NODEINFO_INTERVAL" 50 | 51 | // EnvAllowRWOMultiPodAccess - Environment variable to configure sharing of a single volume across multiple pods within the same node 52 | // Multi-node access is still not allowed for ReadWriteOnce Mount volumes. 53 | // Enabling this option techincally violates the CSI 1.3 spec in the NodePublishVolume stating the required error returns. 54 | // X_CSI_UNITY_ALLOW_MULTI_POD_ACCESS has been deprecated and will be removes in a future release 55 | EnvAllowRWOMultiPodAccess = "X_CSI_UNITY_ALLOW_MULTI_POD_ACCESS" 56 | 57 | // EnvIsVolumeHealthMonitorEnabled - Environment variable to enable/disable health monitor of CSI volumes 58 | EnvIsVolumeHealthMonitorEnabled = "X_CSI_HEALTH_MONITOR_ENABLED" 59 | 60 | // EnvAllowedNetworks indicates list of networks on which NFS traffic is allowed 61 | EnvAllowedNetworks = "X_CSI_ALLOWED_NETWORKS" 62 | ) 63 | -------------------------------------------------------------------------------- /service/identity.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package service 16 | 17 | import ( 18 | "strings" 19 | 20 | "github.com/container-storage-interface/spec/lib/go/csi" 21 | "github.com/dell/csi-unity/core" 22 | "golang.org/x/net/context" 23 | ) 24 | 25 | func (s *service) Probe( 26 | ctx context.Context, 27 | req *csi.ProbeRequest) ( 28 | *csi.ProbeResponse, error, 29 | ) { 30 | ctx, log, _ := GetRunidLog(ctx) 31 | log.Infof("Executing Probe with args: %+v", *req) 32 | if strings.EqualFold(s.mode, "controller") { 33 | if err := s.controllerProbe(ctx, ""); err != nil { 34 | log.Error("Identity probe failed:", err) 35 | return nil, err 36 | } 37 | } 38 | if strings.EqualFold(s.mode, "node") { 39 | if err := s.nodeProbe(ctx, ""); err != nil { 40 | log.Error("Identity probe failed:", err) 41 | return nil, err 42 | } 43 | } 44 | log.Info("Identity probe success") 45 | return &csi.ProbeResponse{}, nil 46 | } 47 | 48 | func (s *service) GetPluginInfo( 49 | _ context.Context, 50 | _ *csi.GetPluginInfoRequest) ( 51 | *csi.GetPluginInfoResponse, error, 52 | ) { 53 | return &csi.GetPluginInfoResponse{ 54 | Name: Name, 55 | VendorVersion: core.SemVer, 56 | Manifest: Manifest, 57 | }, nil 58 | } 59 | 60 | func (s *service) GetPluginCapabilities( 61 | ctx context.Context, 62 | req *csi.GetPluginCapabilitiesRequest) ( 63 | *csi.GetPluginCapabilitiesResponse, error, 64 | ) { 65 | ctx, log, _ := GetRunidLog(ctx) 66 | log.Infof("Executing GetPluginCapabilities with args: %+v", *req) 67 | return &csi.GetPluginCapabilitiesResponse{ 68 | Capabilities: []*csi.PluginCapability{ 69 | { 70 | Type: &csi.PluginCapability_Service_{ 71 | Service: &csi.PluginCapability_Service{ 72 | Type: csi.PluginCapability_Service_CONTROLLER_SERVICE, 73 | }, 74 | }, 75 | }, 76 | { 77 | Type: &csi.PluginCapability_VolumeExpansion_{ 78 | VolumeExpansion: &csi.PluginCapability_VolumeExpansion{ 79 | Type: csi.PluginCapability_VolumeExpansion_ONLINE, 80 | }, 81 | }, 82 | }, 83 | { 84 | Type: &csi.PluginCapability_VolumeExpansion_{ 85 | VolumeExpansion: &csi.PluginCapability_VolumeExpansion{ 86 | Type: csi.PluginCapability_VolumeExpansion_OFFLINE, 87 | }, 88 | }, 89 | }, 90 | { 91 | Type: &csi.PluginCapability_Service_{ 92 | Service: &csi.PluginCapability_Service{ 93 | Type: csi.PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS, 94 | }, 95 | }, 96 | }, 97 | }, 98 | }, nil 99 | } 100 | -------------------------------------------------------------------------------- /service/identity_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package service 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "sync" 22 | "testing" 23 | 24 | "github.com/container-storage-interface/spec/lib/go/csi" 25 | "github.com/dell/csi-unity/core" 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | type probeService interface { 30 | controllerProbe(ctx context.Context, arrayID string) error 31 | nodeProbe(ctx context.Context, arrayID string) error 32 | } 33 | 34 | type mockProbeService struct { 35 | mockControllerProbe func(ctx context.Context, arrayID string) error 36 | mockNodeProbe func(ctx context.Context, arrayID string) error 37 | } 38 | 39 | func (m *mockProbeService) controllerProbe(ctx context.Context, arrayID string) error { 40 | if m.mockControllerProbe != nil { 41 | return m.mockControllerProbe(ctx, arrayID) 42 | } 43 | return nil 44 | } 45 | 46 | func (m *mockProbeService) nodeProbe(ctx context.Context, arrayID string) error { 47 | if m.mockNodeProbe != nil { 48 | return m.mockNodeProbe(ctx, arrayID) 49 | } 50 | return nil 51 | } 52 | 53 | func TestService_Probe(t *testing.T) { 54 | tests := []struct { 55 | name string 56 | mode string 57 | probeMock *mockProbeService 58 | expectErr bool 59 | }{ 60 | { 61 | name: "Controller Mode - Success", 62 | mode: "controller", 63 | probeMock: &mockProbeService{ 64 | mockControllerProbe: func(_ context.Context, _ string) error { return nil }, 65 | }, 66 | expectErr: false, 67 | }, 68 | { 69 | name: "Controller Mode - Failure", 70 | mode: "controller", 71 | probeMock: &mockProbeService{ 72 | mockControllerProbe: func(_ context.Context, _ string) error { return errors.New("controller probe failed") }, 73 | }, 74 | expectErr: true, 75 | }, 76 | { 77 | name: "Node Mode - Success", 78 | mode: "node", 79 | probeMock: &mockProbeService{ 80 | mockNodeProbe: func(_ context.Context, _ string) error { return nil }, 81 | }, 82 | expectErr: false, 83 | }, 84 | { 85 | name: "Node Mode - Failure", 86 | mode: "node", 87 | probeMock: &mockProbeService{ 88 | mockNodeProbe: func(_ context.Context, _ string) error { return errors.New("node probe failed") }, 89 | }, 90 | expectErr: true, 91 | }, 92 | } 93 | 94 | for _, tt := range tests { 95 | t.Run(tt.name, func(t *testing.T) { 96 | var err error 97 | if tt.mode == "controller" { 98 | err = tt.probeMock.controllerProbe(context.TODO(), "") 99 | } else { 100 | err = tt.probeMock.nodeProbe(context.TODO(), "") 101 | } 102 | if tt.expectErr { 103 | assert.Error(t, err) 104 | } else { 105 | assert.NoError(t, err) 106 | } 107 | }) 108 | } 109 | } 110 | 111 | func TestService_GetPluginInfo(t *testing.T) { 112 | s := &service{} 113 | 114 | resp, err := s.GetPluginInfo(context.TODO(), &csi.GetPluginInfoRequest{}) 115 | assert.NoError(t, err) 116 | assert.NotNil(t, resp) 117 | assert.Equal(t, Name, resp.Name) 118 | assert.Equal(t, core.SemVer, resp.VendorVersion) 119 | assert.Equal(t, Manifest, resp.Manifest) 120 | } 121 | 122 | func TestService_GetPluginCapabilities(t *testing.T) { 123 | s := &service{} 124 | resp, err := s.GetPluginCapabilities(context.TODO(), &csi.GetPluginCapabilitiesRequest{}) 125 | 126 | assert.NoError(t, err) 127 | assert.NotNil(t, resp) 128 | assert.Len(t, resp.Capabilities, 4) 129 | 130 | expectedCapabilities := []*csi.PluginCapability{ 131 | { 132 | Type: &csi.PluginCapability_Service_{ 133 | Service: &csi.PluginCapability_Service{ 134 | Type: csi.PluginCapability_Service_CONTROLLER_SERVICE, 135 | }, 136 | }, 137 | }, 138 | { 139 | Type: &csi.PluginCapability_VolumeExpansion_{ 140 | VolumeExpansion: &csi.PluginCapability_VolumeExpansion{ 141 | Type: csi.PluginCapability_VolumeExpansion_ONLINE, 142 | }, 143 | }, 144 | }, 145 | { 146 | Type: &csi.PluginCapability_VolumeExpansion_{ 147 | VolumeExpansion: &csi.PluginCapability_VolumeExpansion{ 148 | Type: csi.PluginCapability_VolumeExpansion_OFFLINE, 149 | }, 150 | }, 151 | }, 152 | { 153 | Type: &csi.PluginCapability_Service_{ 154 | Service: &csi.PluginCapability_Service{ 155 | Type: csi.PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS, 156 | }, 157 | }, 158 | }, 159 | } 160 | 161 | for i, expected := range expectedCapabilities { 162 | assert.Equal(t, expected.Type, resp.Capabilities[i].Type) 163 | } 164 | } 165 | 166 | func TestRunProbe(t *testing.T) { 167 | svc := &service{ 168 | arrays: &sync.Map{}, 169 | mode: "controller", 170 | } 171 | ctx := context.Background() 172 | req := &csi.ProbeRequest{} 173 | _, err := svc.Probe(ctx, req) 174 | assert.Error(t, err) 175 | 176 | svc.mode = "node" 177 | _, err = svc.Probe(ctx, req) 178 | assert.Error(t, err) 179 | 180 | svc.mode = "" 181 | _, err = svc.Probe(ctx, req) 182 | assert.NoError(t, err) 183 | } 184 | -------------------------------------------------------------------------------- /service/main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package service 16 | 17 | import ( 18 | "bufio" 19 | "context" 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "os" 25 | "strings" 26 | "testing" 27 | 28 | "github.com/dell/csi-unity/service/utils" 29 | "github.com/sirupsen/logrus" 30 | ) 31 | 32 | type testConfig struct { 33 | unityConfig string 34 | service *service 35 | ctx context.Context 36 | defaultArray string 37 | } 38 | 39 | var testConf *testConfig 40 | 41 | func TestMain(m *testing.M) { 42 | fmt.Println("------------In TestMain--------------") 43 | os.Setenv("GOUNITY_DEBUG", "true") 44 | os.Setenv("X_CSI_DEBUG", "true") 45 | 46 | // for this tutorial, we will hard code it to config.txt 47 | // testProp, err := readTestProperties("../test.properties") 48 | // if err != nil { 49 | // panic("The system cannot find the file specified") 50 | // } 51 | 52 | // if err != nil { 53 | // fmt.Println(err) 54 | // } 55 | testConf = &testConfig{} 56 | 57 | // testConf.unityConfig = testProp["DRIVER_CONFIG"] 58 | testConf.service = new(service) 59 | DriverConfig = testConf.unityConfig 60 | // os.Setenv("X_CSI_UNITY_NODENAME", testProp["X_CSI_UNITY_NODENAME"]) 61 | testConf.service.BeforeServe(context.Background(), nil, nil) 62 | fmt.Println() 63 | 64 | entry := logrus.WithField(utils.RUNID, "test-1") 65 | testConf.ctx = context.WithValue(context.Background(), utils.UnityLogger, entry) 66 | 67 | for _, v := range testConf.service.getStorageArrayList() { 68 | if v.IsDefaultArray { 69 | testConf.defaultArray = v.ArrayID 70 | break 71 | } 72 | } 73 | code := m.Run() 74 | fmt.Println("------------End of TestMain--------------") 75 | os.Exit(code) 76 | } 77 | 78 | func readTestProperties(filename string) (map[string]string, error) { 79 | // init with some bogus data 80 | configPropertiesMap := map[string]string{} 81 | if len(filename) == 0 { 82 | return nil, errors.New("Error reading properties file " + filename) 83 | } 84 | file, err := os.Open(filename) 85 | if err != nil { 86 | return nil, err 87 | } 88 | defer file.Close() 89 | 90 | reader := bufio.NewReader(file) 91 | 92 | for { 93 | line, err := reader.ReadString('\n') 94 | 95 | // check if the line has = sign 96 | // and process the line. Ignore the rest. 97 | if equal := strings.Index(line, "="); equal >= 0 { 98 | if key := strings.TrimSpace(line[:equal]); len(key) > 0 { 99 | value := "" 100 | if len(line) > equal { 101 | value = strings.TrimSpace(line[equal+1:]) 102 | } 103 | // assign the config map 104 | configPropertiesMap[key] = value 105 | } 106 | } 107 | if err == io.EOF { 108 | break 109 | } 110 | if err != nil { 111 | return nil, err 112 | } 113 | } 114 | return configPropertiesMap, nil 115 | } 116 | 117 | func prettyPrintJSON(obj interface{}) string { 118 | data, _ := json.Marshal(obj) 119 | return string(data) 120 | } 121 | -------------------------------------------------------------------------------- /service/utils/logging.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020-2025 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package utils 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "reflect" 21 | "runtime" 22 | "strconv" 23 | "strings" 24 | "sync" 25 | "time" 26 | 27 | "github.com/sirupsen/logrus" 28 | ) 29 | 30 | var ( 31 | singletonLog *logrus.Logger 32 | once sync.Once 33 | ) 34 | 35 | const ( 36 | // Default log format will output [INFO]: 2006-01-02T15:04:05Z07:00 - Log message 37 | defaultLogFormat = "time=\"%time%\" level=%lvl% %arrayid% %runid% msg=\"%msg%\"" 38 | defaultTimestampFormat = time.RFC3339 39 | ) 40 | 41 | // Formatter implements logrus.Formatter interface. 42 | type Formatter struct { 43 | // logrus.TextFormatter 44 | // Timestamp format 45 | TimestampFormat string 46 | // Available standard keys: time, msg, lvl 47 | // Also can include custom fields but limited to strings. 48 | // All of fields need to be wrapped inside %% i.e %time% %msg% 49 | LogFormat string 50 | 51 | CallerPrettyfier func(*runtime.Frame) (function string, file string) 52 | } 53 | 54 | // Format building log message. 55 | func (f *Formatter) Format(entry *logrus.Entry) ([]byte, error) { 56 | output := f.LogFormat 57 | if output == "" { 58 | output = defaultLogFormat 59 | } 60 | 61 | timestampFormat := f.TimestampFormat 62 | if timestampFormat == "" { 63 | timestampFormat = defaultTimestampFormat 64 | } 65 | 66 | output = strings.Replace(output, "%time%", entry.Time.Format(timestampFormat), 1) 67 | output = strings.Replace(output, "%msg%", entry.Message, 1) 68 | level := strings.ToUpper(entry.Level.String()) 69 | output = strings.Replace(output, "%lvl%", strings.ToLower(level), 1) 70 | 71 | fields := entry.Data 72 | x, b := fields[RUNID] 73 | if b { 74 | output = strings.Replace(output, "%runid%", fmt.Sprintf("runid=%v", x), 1) 75 | } else { 76 | output = strings.Replace(output, "%runid%", "", 1) 77 | } 78 | x, b = fields[ARRAYID] 79 | 80 | if b { 81 | output = strings.Replace(output, "%arrayid%", fmt.Sprintf("arrayid=%v", x), 1) 82 | } else { 83 | output = strings.Replace(output, "%arrayid%", "", 1) 84 | } 85 | 86 | for k, val := range entry.Data { 87 | switch v := val.(type) { 88 | case string: 89 | output = strings.Replace(output, "%"+k+"%", v, 1) 90 | case int: 91 | s := strconv.Itoa(v) 92 | output = strings.Replace(output, "%"+k+"%", s, 1) 93 | case bool: 94 | s := strconv.FormatBool(v) 95 | output = strings.Replace(output, "%"+k+"%", s, 1) 96 | } 97 | } 98 | 99 | var funcVal, fileVal string 100 | if entry.HasCaller() { 101 | if f.CallerPrettyfier != nil { 102 | funcVal, fileVal = f.CallerPrettyfier(entry.Caller) 103 | } else { 104 | funcVal = entry.Caller.Function 105 | fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) 106 | } 107 | 108 | if funcVal != "" { 109 | output = fmt.Sprintf("%s func=\"%s\"", output, funcVal) 110 | } 111 | if fileVal != "" { 112 | output = fmt.Sprintf("%s file=\"%s\"", output, fileVal) 113 | } 114 | } 115 | 116 | output = fmt.Sprintf("%s\n", output) 117 | 118 | return []byte(output), nil 119 | } 120 | 121 | // GetLogger - get logger 122 | func GetLogger() *logrus.Logger { 123 | once.Do(func() { 124 | singletonLog = logrus.New() 125 | fmt.Println("csi-unity logger initiated. This should be called only once.") 126 | 127 | // Setting default level to Info since the driver is yet to read secrect that has the debug level set 128 | singletonLog.Level = logrus.InfoLevel 129 | 130 | singletonLog.SetReportCaller(true) 131 | singletonLog.Formatter = &Formatter{ 132 | CallerPrettyfier: func(f *runtime.Frame) (string, string) { 133 | filename1 := strings.Split(f.File, "dell/csi-unity") 134 | if len(filename1) > 1 { 135 | return fmt.Sprintf("%s()", f.Function), fmt.Sprintf("dell/csi-unity%s:%d", filename1[1], f.Line) 136 | } 137 | filename2 := strings.Split(f.File, "dell/gounity") 138 | if len(filename2) > 1 { 139 | return fmt.Sprintf("%s()", f.Function), fmt.Sprintf("dell/gounity%s:%d", filename2[1], f.Line) 140 | } 141 | return fmt.Sprintf("%s()", f.Function), fmt.Sprintf("%s:%d", f.File, f.Line) 142 | }, 143 | } 144 | }) 145 | 146 | return singletonLog 147 | } 148 | 149 | // ChangeLogLevel - change log level 150 | func ChangeLogLevel(logLevel string) { 151 | switch strings.ToLower(logLevel) { 152 | 153 | case "debug": 154 | singletonLog.Level = logrus.DebugLevel 155 | break 156 | 157 | case "warn", "warning": 158 | singletonLog.Level = logrus.WarnLevel 159 | break 160 | 161 | case "error": 162 | singletonLog.Level = logrus.ErrorLevel 163 | break 164 | 165 | case "info": 166 | // Default level will be Info 167 | fallthrough 168 | 169 | default: 170 | singletonLog.Level = logrus.InfoLevel 171 | } 172 | } 173 | 174 | type unityLog string 175 | 176 | // Constants which can be used across modules 177 | const ( 178 | UnityLogger unityLog = "unitylog" 179 | LogFields unityLog = "fields" 180 | RUNID = "runid" 181 | ARRAYID = "arrayid" 182 | ) 183 | 184 | // GetRunidLogger - Get runid logger 185 | func GetRunidLogger(ctx context.Context) *logrus.Entry { 186 | tempLog := ctx.Value(UnityLogger) 187 | if ctx.Value(UnityLogger) != nil && reflect.TypeOf(tempLog) == reflect.TypeOf(&logrus.Entry{}) { 188 | return ctx.Value(UnityLogger).(*logrus.Entry) 189 | } 190 | return nil 191 | } 192 | 193 | // GetRunidAndLogger - Get runid and logger 194 | func GetRunidAndLogger(ctx context.Context) (string, *logrus.Entry) { 195 | rid := "" 196 | fields, ok := ctx.Value(LogFields).(logrus.Fields) 197 | if ok && fields != nil && reflect.TypeOf(fields) == reflect.TypeOf(logrus.Fields{}) { 198 | if fields[RUNID] != nil { 199 | rid = fields[RUNID].(string) 200 | } 201 | } 202 | 203 | tempLog := ctx.Value(UnityLogger) 204 | if tempLog != nil && reflect.TypeOf(tempLog) == reflect.TypeOf(&logrus.Entry{}) { 205 | // rid = fmt.Sprintf("%s", tempLog.(*logrus.Logger).Data[RUNID]) 206 | return rid, tempLog.(*logrus.Entry) 207 | } 208 | return rid, nil 209 | } 210 | 211 | var GetRunidAndLoggerWrapper = func(ctx context.Context) (string, *logrus.Entry) { 212 | return GetRunidAndLogger(ctx) 213 | } 214 | -------------------------------------------------------------------------------- /service/utils/logging_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020-2025 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package utils 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "runtime" 21 | "strings" 22 | "sync" 23 | "testing" 24 | "time" 25 | 26 | "github.com/sirupsen/logrus" 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestGetRunidLogger(t *testing.T) { 31 | log := GetLogger() 32 | ctx := context.Background() 33 | entry := log.WithField(RUNID, "1111") 34 | ctx = context.WithValue(ctx, UnityLogger, entry) 35 | 36 | logEntry := GetRunidLogger(ctx) 37 | logEntry.Message = "Hi This is log test1" 38 | message, _ := logEntry.String() 39 | fmt.Println(message) 40 | assert.True(t, strings.Contains(message, `runid=1111 msg="Hi This is log test1"`), "Log message not found") 41 | 42 | entry = logEntry.WithField(ARRAYID, "arr0000") 43 | entry.Message = "Hi this is TestSetArrayIdContext" 44 | message, _ = entry.String() 45 | assert.True(t, strings.Contains(message, `arrayid=arr0000 runid=1111 msg="Hi this is TestSetArrayIdContext"`), "Log message not found") 46 | 47 | // Test case where logger is not present in context 48 | ctx = context.Background() // Reset context 49 | logEntry = GetRunidLogger(ctx) 50 | assert.Nil(t, logEntry, "Log entry should be nil when logger is not present in context") 51 | } 52 | 53 | func TestChangeLogLevel(t *testing.T) { 54 | // Ensure singletonLog is initialized before any tests 55 | GetLogger() 56 | 57 | t.Run("Debug level case", func(t *testing.T) { 58 | ChangeLogLevel("debug") 59 | if singletonLog.Level != logrus.DebugLevel { 60 | t.Errorf("expected DebugLevel, got %v", singletonLog.Level) 61 | } 62 | }) 63 | 64 | t.Run("Warn level case", func(t *testing.T) { 65 | ChangeLogLevel("warn") 66 | if singletonLog.Level != logrus.WarnLevel { 67 | t.Errorf("expected WarnLevel, got %v", singletonLog.Level) 68 | } 69 | }) 70 | 71 | t.Run("Warning level case", func(t *testing.T) { 72 | ChangeLogLevel("warning") 73 | if singletonLog.Level != logrus.WarnLevel { 74 | t.Errorf("expected WarnLevel, got %v", singletonLog.Level) 75 | } 76 | }) 77 | 78 | t.Run("Error level case", func(t *testing.T) { 79 | ChangeLogLevel("error") 80 | if singletonLog.Level != logrus.ErrorLevel { 81 | t.Errorf("expected ErrorLevel, got %v", singletonLog.Level) 82 | } 83 | }) 84 | 85 | t.Run("Info level case", func(t *testing.T) { 86 | ChangeLogLevel("info") 87 | if singletonLog.Level != logrus.InfoLevel { 88 | t.Errorf("expected InfoLevel, got %v", singletonLog.Level) 89 | } 90 | }) 91 | 92 | t.Run("Default level case with unknown input", func(t *testing.T) { 93 | ChangeLogLevel("unknown") 94 | if singletonLog.Level != logrus.InfoLevel { 95 | t.Errorf("expected InfoLevel, got %v", singletonLog.Level) 96 | } 97 | }) 98 | } 99 | 100 | func TestGetRunidAndLogger(t *testing.T) { 101 | log := logrus.New() 102 | entry := log.WithField(RUNID, "1111") 103 | 104 | tests := []struct { 105 | name string 106 | ctx context.Context 107 | expectedRID string 108 | expectedLog *logrus.Entry 109 | }{ 110 | { 111 | name: "Logger and RunID present in context", 112 | ctx: func() context.Context { 113 | ctx := context.Background() 114 | fields := logrus.Fields{RUNID: "1111"} 115 | ctx = context.WithValue(ctx, LogFields, fields) 116 | ctx = context.WithValue(ctx, UnityLogger, entry) 117 | return ctx 118 | }(), 119 | expectedRID: "1111", 120 | expectedLog: entry, 121 | }, 122 | { 123 | name: "Logger present but no RunID in context", 124 | ctx: func() context.Context { 125 | ctx := context.Background() 126 | ctx = context.WithValue(ctx, UnityLogger, entry) 127 | return ctx 128 | }(), 129 | expectedRID: "", 130 | expectedLog: entry, 131 | }, 132 | { 133 | name: "RunID present but no Logger in context", 134 | ctx: func() context.Context { 135 | ctx := context.Background() 136 | fields := logrus.Fields{RUNID: "1111"} 137 | ctx = context.WithValue(ctx, LogFields, fields) 138 | return ctx 139 | }(), 140 | expectedRID: "1111", 141 | expectedLog: nil, 142 | }, 143 | { 144 | name: "Neither Logger nor RunID present in context", 145 | ctx: context.Background(), 146 | expectedRID: "", 147 | expectedLog: nil, 148 | }, 149 | } 150 | 151 | for _, test := range tests { 152 | t.Run(test.name, func(t *testing.T) { 153 | rid, logEntry := GetRunidAndLogger(test.ctx) 154 | assert.Equal(t, test.expectedRID, rid, "RunID should be %v for test %v", test.expectedRID, test.name) 155 | assert.Equal(t, test.expectedLog, logEntry, "Log entry should be %v for test %v", test.expectedLog, test.name) 156 | }) 157 | } 158 | } 159 | 160 | // resetOnce is a helper function to reset the once variable for testing purposes 161 | func resetOnce() { 162 | once = sync.Once{} 163 | } 164 | 165 | func TestGetLogger(t *testing.T) { 166 | // Ensure singletonLog is nil before the test 167 | singletonLog = nil 168 | resetOnce() 169 | 170 | // Call GetLogger and check if it initializes the logger 171 | logger := GetLogger() 172 | assert.NotNil(t, logger, "Logger should not be nil") 173 | assert.Equal(t, logrus.InfoLevel, logger.Level, "Logger level should be Info") 174 | assert.True(t, logger.ReportCaller, "Logger should report caller") 175 | 176 | // Call GetLogger again and check if it returns the same instance 177 | logger2 := GetLogger() 178 | assert.Equal(t, logger, logger2, "GetLogger should return the same instance") 179 | 180 | // Check if the formatter is set correctly 181 | formatter, ok := logger.Formatter.(*Formatter) 182 | assert.True(t, ok, "Logger formatter should be of type *Formatter") 183 | assert.NotNil(t, formatter.CallerPrettyfier, "CallerPrettyfier should not be nil") 184 | 185 | // Test the CallerPrettyfier function 186 | frame := &runtime.Frame{ 187 | Function: "TestFunction", 188 | File: "dell/csi-unity/testfile.go", 189 | Line: 42, 190 | } 191 | function, file := formatter.CallerPrettyfier(frame) 192 | assert.Equal(t, "TestFunction()", function, "Function name should be formatted correctly") 193 | assert.Equal(t, "dell/csi-unity/testfile.go:42", file, "File name should be formatted correctly") 194 | 195 | frame.File = "dell/gounity/testfile.go" 196 | function, file = formatter.CallerPrettyfier(frame) 197 | assert.Equal(t, "TestFunction()", function, "Function name should be formatted correctly") 198 | assert.Equal(t, "dell/gounity/testfile.go:42", file, "File name should be formatted correctly") 199 | 200 | frame.File = "otherpath/testfile.go" 201 | function, file = formatter.CallerPrettyfier(frame) 202 | assert.Equal(t, "TestFunction()", function, "Function name should be formatted correctly") 203 | assert.Equal(t, "otherpath/testfile.go:42", file, "File name should be formatted correctly") 204 | } 205 | 206 | func TestFormatter_Format(t *testing.T) { 207 | formatter := &Formatter{ 208 | LogFormat: "%time% [%lvl%] %msg% %runid% %arrayid% %intField% %boolField%", 209 | TimestampFormat: "2006-01-02 15:04:05", 210 | CallerPrettyfier: func(f *runtime.Frame) (string, string) { 211 | return fmt.Sprintf("%s()", f.Function), fmt.Sprintf("%s:%d", f.File, f.Line) 212 | }, 213 | } 214 | 215 | entry := &logrus.Entry{ 216 | Time: time.Date(2025, time.February, 27, 15, 0, 0, 0, time.UTC), 217 | Level: logrus.InfoLevel, 218 | Message: "Test message", 219 | Data: logrus.Fields{ 220 | RUNID: "1234", 221 | ARRAYID: "5678", 222 | "intField": 42, 223 | "boolField": true, 224 | }, 225 | Caller: &runtime.Frame{ 226 | Function: "main.TestFunction", 227 | File: "main.go", 228 | Line: 42, 229 | }, 230 | } 231 | entry.Logger = logrus.New() 232 | entry.Logger.SetReportCaller(true) 233 | 234 | expectedOutput := "2025-02-27 15:00:00 [info] Test message runid=1234 arrayid=5678 42 true func=\"main.TestFunction()\" file=\"main.go:42\"\n" 235 | output, err := formatter.Format(entry) 236 | assert.NoError(t, err, "Formatter should not return an error") 237 | assert.Equal(t, expectedOutput, string(output), "Formatted output should match expected output") 238 | } 239 | -------------------------------------------------------------------------------- /test/bdd-test/bdd_main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2019-2024 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package bdd_test 16 | 17 | import ( 18 | "context" 19 | "flag" 20 | "fmt" 21 | "os" 22 | "strconv" 23 | "testing" 24 | "time" 25 | 26 | "gopkg.in/yaml.v2" 27 | 28 | "github.com/container-storage-interface/spec/lib/go/csi" 29 | "github.com/cucumber/godog" 30 | "github.com/cucumber/godog/colors" 31 | "github.com/dell/csi-unity/provider" 32 | "github.com/dell/csi-unity/service" 33 | "github.com/dell/gocsi/utils" 34 | "google.golang.org/grpc" 35 | ) 36 | 37 | var ( 38 | grpcClient *grpc.ClientConn 39 | stop func() 40 | opt = godog.Options{Output: colors.Colored(os.Stdout)} 41 | ) 42 | 43 | func init() { 44 | godog.BindFlags("godog.", flag.CommandLine, &opt) 45 | } 46 | 47 | // To parse the secret json file 48 | type StorageArrayList struct { 49 | StorageArrayList []StorageArrayConfig `yaml:"storageArrayList"` 50 | } 51 | 52 | type StorageArrayConfig struct { 53 | ArrayID string `yaml:"arrayId"` 54 | } 55 | 56 | func TestMain(m *testing.M) { 57 | os.Setenv("X_CSI_MODE", "") 58 | 59 | file, err := os.ReadFile(os.Getenv("DRIVER_SECRET")) 60 | if err != nil { 61 | panic("Driver Config missing") 62 | } 63 | arrayIDList := StorageArrayList{} 64 | _ = yaml.Unmarshal([]byte(file), &arrayIDList) 65 | if len(arrayIDList.StorageArrayList) == 0 { 66 | panic("Array Info not provided") 67 | } 68 | for i := 0; i < len(arrayIDList.StorageArrayList); i++ { 69 | arrayIdvar := "Array" + strconv.Itoa(i+1) + "-Id" 70 | os.Setenv(arrayIdvar, arrayIDList.StorageArrayList[i].ArrayID) 71 | } 72 | 73 | ctx := context.Background() 74 | fmt.Printf("calling startServer") 75 | grpcClient, stop = startServer(ctx) 76 | fmt.Printf("back from startServer") 77 | time.Sleep(5 * time.Second) 78 | 79 | flag.Parse() 80 | opt.Format = "pretty" 81 | opt.Paths = []string{"features"} 82 | exitVal := godog.TestSuite{ 83 | Name: "godog", 84 | ScenarioInitializer: FeatureContext, 85 | Options: &opt, 86 | }.Run() 87 | if st := m.Run(); st > exitVal { 88 | exitVal = st 89 | } 90 | stop() 91 | os.Exit(exitVal) 92 | } 93 | 94 | func TestIdentityGetPluginInfo(t *testing.T) { 95 | ctx := context.Background() 96 | fmt.Printf("testing GetPluginInfo\n") 97 | client := csi.NewIdentityClient(grpcClient) 98 | info, err := client.GetPluginInfo(ctx, &csi.GetPluginInfoRequest{}) 99 | if err != nil { 100 | fmt.Printf("GetPluginInfo %s:\n", err.Error()) 101 | t.Error("GetPluginInfo failed") 102 | } else { 103 | fmt.Printf("testing GetPluginInfo passed: %s\n", info.GetName()) 104 | } 105 | } 106 | 107 | func startServer(ctx context.Context) (*grpc.ClientConn, func()) { 108 | // Create a new SP instance and serve it with a piped connection. 109 | sp := provider.New() 110 | lis, err := utils.GetCSIEndpointListener() 111 | if err != nil { 112 | fmt.Printf("couldn't open listener: %s\n", err.Error()) 113 | return nil, nil 114 | } 115 | service.Name = os.Getenv("DRIVER_NAME") 116 | service.DriverConfig = os.Getenv("DRIVER_CONFIG") 117 | service.DriverSecret = os.Getenv("DRIVER_SECRET") 118 | fmt.Printf("lis: %v\n", lis) 119 | go func() { 120 | fmt.Printf("starting server\n") 121 | if err := sp.Serve(ctx, lis); err != nil { 122 | fmt.Printf("http: Server closed. Error: %v", err) 123 | } 124 | }() 125 | network, addr, err := utils.GetCSIEndpoint() 126 | if err != nil { 127 | return nil, nil 128 | } 129 | fmt.Printf("network %v addr %v\n", network, addr) 130 | 131 | clientOpts := []grpc.DialOption{ 132 | grpc.WithInsecure(), 133 | } 134 | 135 | // Create a client for the piped connection. 136 | fmt.Printf("calling gprc.DialContext, ctx %v, addr %s, clientOpts %v\n", ctx, addr, clientOpts) 137 | client, err := grpc.DialContext(ctx, "unix:"+addr, clientOpts...) 138 | if err != nil { 139 | fmt.Printf("DialContext returned error: %s", err.Error()) 140 | } 141 | fmt.Printf("grpc.DialContext returned ok\n") 142 | 143 | return client, func() { 144 | client.Close() 145 | sp.GracefulStop(ctx) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /test/bdd-test/c.out: -------------------------------------------------------------------------------- 1 | mode: set 2 | -------------------------------------------------------------------------------- /test/bdd-test/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | # This will run coverage analysis using the integration testing. 15 | # The env.sh must point to a valid Unity XT Array# on this system. 16 | 17 | 18 | source ../../env.sh 19 | mkdir $(dirname "${CSI_ENDPOINT}") || rm -f ${CSI_ENDPOINT} 20 | echo $SDC_GUID 21 | go test -v -coverprofile=c.out -timeout 60m -coverpkg=github.com/dell/csi-unity/service *test.go & 22 | # go test -v --godog.tags=wip -coverprofile=c.out -timeout 60m -coverpkg=github.com/dell/csi-unity/service *test.go & 23 | wait 24 | go tool cover -html=c.out -------------------------------------------------------------------------------- /test/integration-test/features/integration.feature: -------------------------------------------------------------------------------- 1 | Feature: CSI interface 2 | As a consumer of the CSI interface 3 | I want to run a system test 4 | So that I know the service functions correctly. 5 | 6 | Scenario: Controller get capabilities, create, validate capabilities and delete basic volume 7 | Given a CSI service 8 | When I call Controller Get Capabilities 9 | Then there are no errors 10 | And a basic block volume request name "gditest-vol1" arrayId "Array1-Id" protocol "FC" size "5" 11 | When I call CreateVolume 12 | Then there are no errors 13 | When I call validate volume capabilities with protocol "FC" with same access mode 14 | Then there are no errors 15 | And when I call DeleteVolume 16 | Then there are no errors 17 | 18 | Scenario: Create, validate capabilities and delete basic volume 19 | Given a CSI service 20 | And a basic block volume request name "gditest-vol2" arrayId "Array1-Id" protocol "FC" size "5" 21 | When I call CreateVolume 22 | Then there are no errors 23 | When I call validate volume capabilities with protocol "FC" with different access mode 24 | Then the error message should contain "Unsupported capability" 25 | And when I call DeleteVolume 26 | Then there are no errors 27 | 28 | Scenario: Create, expand and delete basic volume 29 | Given a CSI service 30 | And a basic block volume request name "gditest-vol3" arrayId "Array1-Id" protocol "FC" size "2" 31 | When I call CreateVolume 32 | Then there are no errors 33 | When I call Controller Expand Volume "3" 34 | Then there are no errors 35 | And a basic block volume request name "gditest-vol3" arrayId "Array1-Id" protocol "FC" size "3" 36 | When I call CreateVolume 37 | Then there are no errors 38 | And when I call DeleteVolume 39 | Then there are no errors 40 | 41 | Scenario: Controller expand volume with smaller new size 42 | Given a CSI service 43 | And a basic block volume request name "gditest-vol4" arrayId "Array1-Id" protocol "FC" size "3" 44 | When I call CreateVolume 45 | Then there are no errors 46 | When I call Controller Expand Volume "2" 47 | Then the error message should contain "requested new capacity smaller than existing capacity" 48 | And when I call DeleteVolume 49 | Then there are no errors 50 | 51 | Scenario: Idempotent create and delete basic volume 52 | Given a CSI service 53 | And a basic block volume request name "gditest-vol5" arrayId "Array1-Id" protocol "FC" size "5" 54 | When I call CreateVolume 55 | And I call CreateVolume 56 | And when I call DeleteVolume 57 | And when I call DeleteVolume 58 | Then there are no errors 59 | 60 | Scenario: Create a volume from snapshot of thin volume 61 | Given a CSI service 62 | And a basic block volume request name "gditest-vol6" arrayId "Array1-Id" protocol "FC" size "5" 63 | When I call CreateVolume 64 | And there are no errors 65 | Given a create snapshot request "snap_volforsnap" 66 | When I call CreateSnapshot 67 | And there are no errors 68 | Given a basic block volume request with volume content source with name "gditest-vol7" arrayId "Array1-Id" protocol "FC" size "5" 69 | When I call CreateVolume 70 | Then there are no errors 71 | And when I call DeleteVolume 72 | Then there are no errors 73 | Given a delete snapshot request 74 | When I call DeleteSnapshot 75 | Then there are no errors 76 | And When I call DeleteAllCreatedVolumes 77 | Then there are no errors 78 | 79 | Scenario: Create, publish, unpublish, and delete basic volume with idempotency check for publish and unpublish 80 | Given a CSI service 81 | And a basic block volume request name "gditest-vol8" arrayId "Array1-Id" protocol "FC" size "5" 82 | When I call CreateVolume 83 | And there are no errors 84 | And when I call PublishVolume 85 | And there are no errors 86 | And when I call PublishVolume 87 | And there are no errors 88 | And when I call UnpublishVolume 89 | And there are no errors 90 | And when I call UnpublishVolume 91 | And there are no errors 92 | And when I call DeleteVolume 93 | Then there are no errors 94 | 95 | Scenario: Create and delete basic 264000G volume 96 | Given a CSI service 97 | And a basic block volume request name "gditest-vol9" arrayId "Array1-Id" protocol "FC" size "264000" 98 | When I call CreateVolume 99 | Then the error message should contain "The system could not create the LUNs because specified size is too big." 100 | And when I call DeleteVolume 101 | Then there are no errors 102 | 103 | Scenario: Create and delete basic 96G volume 104 | Given a CSI service 105 | And a basic block volume request name "gditest-vol10" arrayId "Array1-Id" protocol "FC" size "96" 106 | When I call CreateVolume 107 | Then there are no errors 108 | And when I call DeleteVolume 109 | Then there are no errors 110 | 111 | Scenario: Create volume, create snapshot, delete snapshot and delete volume 112 | Given a CSI service 113 | And a basic block volume request name "gditest-vol11" arrayId "Array1-Id" protocol "FC" size "5" 114 | When I call CreateVolume 115 | And there are no errors 116 | Given a create snapshot request "snap_integration1" 117 | When I call CreateSnapshot 118 | And there are no errors 119 | Given a delete snapshot request 120 | And I call DeleteSnapshot 121 | And there are no errors 122 | And when I call DeleteVolume 123 | And there are no errors 124 | 125 | Scenario: Create volume, idempotent create snapshot, idempotent delete snapshot delete volume 126 | Given a CSI service 127 | And a basic block volume request name "gditest-vol12" arrayId "Array1-Id" protocol "FC" size "5" 128 | When I call CreateVolume 129 | And there are no errors 130 | Given a create snapshot request "snap_integration1" 131 | When I call CreateSnapshot 132 | And there are no errors 133 | Given a create snapshot request "snap_integration1" 134 | When I call CreateSnapshot 135 | And there are no errors 136 | Given a delete snapshot request 137 | And I call DeleteSnapshot 138 | And there are no errors 139 | Given a delete snapshot request 140 | And I call DeleteSnapshot 141 | And there are no errors 142 | And when I call DeleteVolume 143 | And there are no errors 144 | 145 | Scenario: Node stage, publish, unpublish and unstage volume with idempotency 146 | Given a CSI service 147 | And a basic block volume request name "gditest-vol13" arrayId "Array1-Id" protocol "FC" size "5" 148 | When I call CreateVolume 149 | And there are no errors 150 | And when I call PublishVolume 151 | And there are no errors 152 | And when I call NodeStageVolume fsType "ext4" 153 | And there are no errors 154 | And when I call NodeStageVolume fsType "ext4" 155 | And there are no errors 156 | And when I call NodePublishVolume fsType "ext4" readonly "false" 157 | Then there are no errors 158 | And when I call NodePublishVolume fsType "ext4" readonly "false" 159 | Then there are no errors 160 | And when I call NodeUnPublishVolume 161 | And there are no errors 162 | And when I call NodeUnPublishVolume 163 | Then there are no errors 164 | And when I call NodeUnstageVolume 165 | And there are no errors 166 | And when I call NodeUnstageVolume 167 | And there are no errors 168 | And when I call UnpublishVolume 169 | And there are no errors 170 | And when I call DeleteVolume 171 | Then there are no errors 172 | 173 | Scenario: Node stage, publish, unpublish and unstage volume for iSCSI 174 | Given a CSI service 175 | And a basic block volume request name "gditest-vol14" arrayId "Array1-Id" protocol "iSCSI" size "5" 176 | When I call CreateVolume 177 | And there are no errors 178 | And when I call PublishVolume 179 | And there are no errors 180 | And when I call NodeStageVolume fsType "ext4" 181 | And there are no errors 182 | And when I call NodePublishVolume fsType "ext4" readonly "false" 183 | Then there are no errors 184 | And when I call NodeUnPublishVolume 185 | And there are no errors 186 | And when I call NodeUnstageVolume 187 | And there are no errors 188 | And when I call UnpublishVolume 189 | And there are no errors 190 | And when I call DeleteVolume 191 | Then there are no errors 192 | 193 | Scenario: Node stage, publish, unpublish and unstage volume for NFS 194 | Given a CSI service 195 | And a basic block volume request name "gditest-vol15" arrayId "Array1-Id" protocol "NFS" size "5" 196 | When I call CreateVolume 197 | And there are no errors 198 | And when I call PublishVolume 199 | And there are no errors 200 | And when I call NodeStageVolume fsType "" 201 | And there are no errors 202 | And when I call NodePublishVolume fsType "" readonly "false" 203 | Then there are no errors 204 | And when I call NodeUnPublishVolume 205 | And there are no errors 206 | And when I call NodeUnstageVolume 207 | And there are no errors 208 | And when I call UnpublishVolume 209 | And there are no errors 210 | And when I call DeleteVolume 211 | Then there are no errors -------------------------------------------------------------------------------- /test/integration-test/integration_main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package integration_test 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "os" 22 | "strconv" 23 | "testing" 24 | "time" 25 | 26 | "github.com/container-storage-interface/spec/lib/go/csi" 27 | "github.com/cucumber/godog" 28 | "github.com/dell/csi-unity/provider" 29 | "github.com/dell/csi-unity/service" 30 | "github.com/dell/gocsi/utils" 31 | "google.golang.org/grpc" 32 | ) 33 | 34 | var grpcClient *grpc.ClientConn 35 | 36 | // To parse the secret json file 37 | type StorageArrayList struct { 38 | StorageArrayList []StorageArrayConfig `json:"storageArrayList"` 39 | } 40 | 41 | type StorageArrayConfig struct { 42 | ArrayID string `json:"arrayId"` 43 | } 44 | 45 | func TestMain(m *testing.M) { 46 | var stop func() 47 | os.Setenv("X_CSI_MODE", "") 48 | 49 | file, err := os.ReadFile(os.Getenv("DRIVER_CONFIG")) 50 | if err != nil { 51 | panic("Driver Config missing") 52 | } 53 | arrayIDList := StorageArrayList{} 54 | _ = json.Unmarshal([]byte(file), &arrayIDList) 55 | if len(arrayIDList.StorageArrayList) == 0 { 56 | panic("Array Info not provided") 57 | } 58 | for i := 0; i < len(arrayIDList.StorageArrayList); i++ { 59 | arrayIdvar := "Array" + strconv.Itoa(i+1) + "-Id" 60 | os.Setenv(arrayIdvar, arrayIDList.StorageArrayList[i].ArrayID) 61 | } 62 | 63 | ctx := context.Background() 64 | fmt.Printf("calling startServer") 65 | grpcClient, stop = startServer(ctx) 66 | fmt.Printf("back from startServer") 67 | time.Sleep(5 * time.Second) 68 | opt := godog.Options{ 69 | Format: "pretty", 70 | Paths: []string{"features"}, 71 | } 72 | exitVal := godog.TestSuite{ 73 | Name: "godog", 74 | ScenarioInitializer: FeatureContext, 75 | Options: &opt, 76 | }.Run() 77 | if st := m.Run(); st > exitVal { 78 | exitVal = st 79 | } 80 | stop() 81 | os.Exit(exitVal) 82 | } 83 | 84 | func TestIdentityGetPluginInfo(t *testing.T) { 85 | ctx := context.Background() 86 | fmt.Printf("testing GetPluginInfo\n") 87 | client := csi.NewIdentityClient(grpcClient) 88 | info, err := client.GetPluginInfo(ctx, &csi.GetPluginInfoRequest{}) 89 | if err != nil { 90 | fmt.Printf("GetPluginInfo %s:\n", err.Error()) 91 | t.Error("GetPluginInfo failed") 92 | } else { 93 | fmt.Printf("testing GetPluginInfo passed: %s\n", info.GetName()) 94 | } 95 | } 96 | 97 | func startServer(ctx context.Context) (*grpc.ClientConn, func()) { 98 | // Create a new SP instance and serve it with a piped connection. 99 | sp := provider.New() 100 | lis, err := utils.GetCSIEndpointListener() 101 | if err != nil { 102 | fmt.Printf("couldn't open listener: %s\n", err.Error()) 103 | return nil, nil 104 | } 105 | service.Name = os.Getenv("DRIVER_NAME") 106 | service.DriverConfig = os.Getenv("DRIVER_CONFIG") 107 | fmt.Printf("lis: %v\n", lis) 108 | go func() { 109 | fmt.Printf("starting server\n") 110 | if err := sp.Serve(ctx, lis); err != nil { 111 | fmt.Printf("http: Server closed. Error: %v", err) 112 | } 113 | }() 114 | network, addr, err := utils.GetCSIEndpoint() 115 | if err != nil { 116 | return nil, nil 117 | } 118 | fmt.Printf("network %v addr %v\n", network, addr) 119 | 120 | clientOpts := []grpc.DialOption{ 121 | grpc.WithInsecure(), 122 | } 123 | 124 | // Create a client for the piped connection. 125 | fmt.Printf("calling gprc.DialContext, ctx %v, addr %s, clientOpts %v\n", ctx, addr, clientOpts) 126 | client, err := grpc.DialContext(ctx, "unix:"+addr, clientOpts...) 127 | if err != nil { 128 | fmt.Printf("DialContext returned error: %s", err.Error()) 129 | } 130 | fmt.Printf("grpc.DialContext returned ok\n") 131 | 132 | return client, func() { 133 | client.Close() 134 | sp.GracefulStop(ctx) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /test/integration-test/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # This will run coverage analysis using the integration testing. 16 | # The env.sh must point to a valid Unity Array# on this system. 17 | 18 | rm -f /root/go/bin/csi.sock 19 | source ../../env.sh 20 | echo $SDC_GUID 21 | go test -v -coverprofile=c.out -timeout 30m -coverpkg=github.com/dell/csi-unity/service *test.go & 22 | wait 23 | go tool cover -html=c.out 24 | 25 | -------------------------------------------------------------------------------- /test/sample.yaml: -------------------------------------------------------------------------------- 1 | # This test creates 2 different PersistentVolumeClaims 2 | # These PVCs use ext4 and xfs default storage classes 3 | # PVs are mounted to pod from StatefulSet 4 | # 5 | # To test the driver just run from root directory of repository 6 | # > kubectl create -f ./tests/sample/ 7 | # 8 | # You can find all resources in unity namespace. 9 | # Check if pod is created and Ready and Running by running 10 | # > kubectl get all -n unity 11 | # (if it's in CrashLoopback state then driver installation wasn't successful, check logs of node and controller) 12 | # After that can go into created container and check if everything is mounted correctly. 13 | # 14 | # After that you can uninstall the testing PVCs and StatefulSet 15 | # > kubectl delete -f ./tests/simple/ 16 | # 17 | apiVersion: v1 18 | kind: Namespace 19 | metadata: 20 | name: unity 21 | --- 22 | kind: PersistentVolumeClaim 23 | apiVersion: v1 24 | metadata: 25 | name: pvol0 26 | namespace: unity 27 | spec: 28 | accessModes: 29 | - ReadWriteOnce 30 | volumeMode: Filesystem 31 | resources: 32 | requests: 33 | storage: 8Gi 34 | storageClassName: unity 35 | --- 36 | kind: PersistentVolumeClaim 37 | apiVersion: v1 38 | metadata: 39 | name: pvol1 40 | namespace: unity 41 | spec: 42 | accessModes: 43 | - ReadWriteOnce 44 | volumeMode: Filesystem 45 | resources: 46 | requests: 47 | storage: 12Gi 48 | storageClassName: unity-iscsi 49 | --- 50 | kind: PersistentVolumeClaim 51 | apiVersion: v1 52 | metadata: 53 | name: pvol2 54 | namespace: unity 55 | spec: 56 | accessModes: 57 | - ReadWriteMany 58 | volumeMode: Filesystem 59 | resources: 60 | requests: 61 | storage: 10Gi 62 | storageClassName: unity-nfs 63 | --- 64 | apiVersion: v1 65 | kind: ServiceAccount 66 | metadata: 67 | name: unitytest 68 | namespace: unity 69 | --- 70 | kind: StatefulSet 71 | apiVersion: apps/v1 72 | metadata: 73 | name: unitytest 74 | namespace: unity 75 | spec: 76 | serviceName: unitytest 77 | selector: 78 | matchLabels: 79 | app: unitytest 80 | template: 81 | metadata: 82 | labels: 83 | app: unitytest 84 | spec: 85 | serviceAccountName: unitytest 86 | hostNetwork: true 87 | containers: 88 | - name: test 89 | image: quay.io/centos/centos:latest 90 | command: [ "/bin/sleep", "3600" ] 91 | volumeMounts: 92 | - mountPath: "/data0" 93 | name: pvol0 94 | - mountPath: "/data1" 95 | name: pvol1 96 | - mountPath: "/data2" 97 | name: pvol2 98 | volumes: 99 | - name: pvol0 100 | persistentVolumeClaim: 101 | claimName: pvol0 102 | - name: pvol1 103 | persistentVolumeClaim: 104 | claimName: pvol1 105 | - name: pvol2 106 | persistentVolumeClaim: 107 | claimName: pvol2 108 | -------------------------------------------------------------------------------- /test/sanity/README.md: -------------------------------------------------------------------------------- 1 | 14 | # Kubernetes Sanity Script Test 15 | 16 | This test runs the Kubernetes sanity test at https://github.com/kubernetes-csi/csi-test. 17 | The test last qualified was v2.8.0. 18 | 19 | To run the test, follow these steps: 20 | 21 | 1. "go get github.com/kubernetes-csi/csi-test" 22 | 2. Build and install the executable, csi-sanity, in a directory in your $PATH. 23 | 3. Make sure your env.sh is up to date so the CSI driver can be run. 24 | 4. Edit the secrets.yaml to have the correct SYMID, ServiceLevel, SRP, and ApplicationPrefix. 25 | 5. Use the start_driver.sh to start the driver. 26 | 6. Wait until the driver has fully come up and completed node setup. If you remain attached the logs will print on the screen. 27 | 7. Use the script run.sh to start csi-sanity (best if you do this in a separate window.) 28 | 29 | ## Excluded Tests 30 | 31 | The following tests were excluded for the reasons specified: 32 | 1. GetCapacity -- the test does not support supplying the Parameters fields that are required (for things like SYMID). 33 | 2. An idempotent volume test that attempts to create a volume with a different size as the existing volume. It appears to have a problem; 34 | the new size is over the maximum capability, and we detect that error first and disqualify the request, which I think is valid. 35 | -------------------------------------------------------------------------------- /test/sanity/params.yaml: -------------------------------------------------------------------------------- 1 | arrayId: "" # update arrayId here 2 | storagePool: "pool_1" # update storagePool here 3 | -------------------------------------------------------------------------------- /test/sanity/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | rm -rf /tmp/csi-mount 16 | 17 | csi-sanity --ginkgo.v \ 18 | --csi.endpoint=unix_sock \ 19 | --csi.secrets=secrets.yaml \ 20 | --csi.testvolumeparameters=params.yaml \ 21 | --ginkgo.skip "GetCapacity|ListSnapshots|create a volume with already existing name and different capacity" \ 22 | -------------------------------------------------------------------------------- /test/sanity/secrets.yaml: -------------------------------------------------------------------------------- 1 | CreateVolumeSecret: 2 | storagePool: pool_1 3 | secretKey: secretval1 4 | ApplicationPrefix: sanity 5 | ListVolumesSecret: 6 | storagePool: pool_1 7 | ValidateVolumeCapabilitiesSecret: 8 | storagePool: pool_1 9 | CreateSnapshotSecret: 10 | storagePool: pool_1 11 | # ListSnapshotSecret: 12 | # storagePool: pool_1 13 | DeleteSnapshotSecret: 14 | storagePool: pool_1 15 | DeleteVolumeSecret: 16 | secretKey: secretval2 17 | ControllerPublishVolumeSecret: 18 | secretKey: secretval3 19 | # storagepool: pool_1 20 | ControllerUnpublishVolumeSecret: 21 | secretKey: secretval4 22 | # storagepool: pool_1 23 | NodeStageVolumeSecret: 24 | secretKey: secretval5 25 | NodePublishVolumeSecret: 26 | secretKey: secretval6 27 | ControllerValidateVolumeCapabilitiesSecret: 28 | secretKey: secretval7 29 | storagePool: pool_1 30 | GetCapacitySecret: 31 | storagepool: pool_1 32 | -------------------------------------------------------------------------------- /test/sanity/start_driver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright © 2019 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | # This will run coverage analysis using the integration testing. 15 | # The env.sh must point to a valid Unisphere deployment and the iscsi packages must be installed 16 | # on this system. This will make real calls to Unisphere 17 | 18 | rm -f unix_sock 19 | . ../../env.sh 20 | echo ENDPOINT $CSI_ENDPOINT 21 | echo "Starting the csi-unity driver. You should wait until the node setup is complete before running tests." 22 | ../../csi-unity --driver-name=$DRIVER_NAME --driver-config=$DRIVER_CONFIG --driver-secret=$DRIVER_SECRET 23 | -------------------------------------------------------------------------------- /test/scale-test/100volumes/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: 100volumes 2 | version: 1.0.0 3 | appVersion: 1.0.0 4 | description: | 5 | Tests CSI Unity deployments. 6 | keywords: 7 | - csi-unity 8 | - storage 9 | engine: gotpl 10 | -------------------------------------------------------------------------------- /test/scale-test/100volumes/values.yaml: -------------------------------------------------------------------------------- 1 | StorageClassName: unity-iscsi 2 | replicas: 3 3 | -------------------------------------------------------------------------------- /test/scale-test/10volumes/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: 10volumes 2 | version: 1.0.0 3 | appVersion: 1.0.0 4 | description: | 5 | Tests CSI Unity deployments. 6 | keywords: 7 | - csi-unity 8 | - storage 9 | engine: gotpl 10 | -------------------------------------------------------------------------------- /test/scale-test/10volumes/templates/test.yaml: -------------------------------------------------------------------------------- 1 | kind: StatefulSet 2 | apiVersion: apps/v1 3 | metadata: 4 | name: unitytest 5 | namespace: test 6 | spec: 7 | serviceName: unitytest-10vol 8 | replicas: 9 | '[object Object]': null 10 | selector: 11 | matchLabels: 12 | app: unitytest 13 | template: 14 | metadata: 15 | labels: 16 | app: unitytest 17 | spec: 18 | serviceAccount: unitytest 19 | hostNetwork: true 20 | containers: 21 | - name: test 22 | image: 'quay.io/centos/centos:latest' 23 | command: 24 | - /bin/sleep 25 | - '3600' 26 | volumeMounts: 27 | - mountPath: /data0 28 | name: unityvolx0 29 | - mountPath: /data1 30 | name: unityvolx1 31 | - mountPath: /data2 32 | name: unityvolx2 33 | - mountPath: /data3 34 | name: unityvolx3 35 | - mountPath: /data4 36 | name: unityvolx4 37 | - mountPath: /data5 38 | name: unityvolx5 39 | - mountPath: /data6 40 | name: unityvolx6 41 | - mountPath: /data7 42 | name: unityvolx7 43 | - mountPath: /data8 44 | name: unityvolx8 45 | - mountPath: /data9 46 | name: unityvolx9 47 | volumeClaimTemplates: 48 | - metadata: 49 | name: unityvolx0 50 | spec: 51 | accessModes: 52 | - ReadWriteOnce 53 | storageClassName: 54 | '[object Object]': null 55 | resources: 56 | requests: 57 | storage: 8Gi 58 | - metadata: 59 | name: unityvolx1 60 | spec: 61 | accessModes: 62 | - ReadWriteOnce 63 | storageClassName: 64 | '[object Object]': null 65 | resources: 66 | requests: 67 | storage: 8Gi 68 | - metadata: 69 | name: unityvolx2 70 | spec: 71 | accessModes: 72 | - ReadWriteOnce 73 | storageClassName: 74 | '[object Object]': null 75 | resources: 76 | requests: 77 | storage: 8Gi 78 | - metadata: 79 | name: unityvolx3 80 | spec: 81 | accessModes: 82 | - ReadWriteOnce 83 | storageClassName: 84 | '[object Object]': null 85 | resources: 86 | requests: 87 | storage: 8Gi 88 | - metadata: 89 | name: unityvolx4 90 | spec: 91 | accessModes: 92 | - ReadWriteOnce 93 | storageClassName: 94 | '[object Object]': null 95 | resources: 96 | requests: 97 | storage: 8Gi 98 | - metadata: 99 | name: unityvolx5 100 | spec: 101 | accessModes: 102 | - ReadWriteOnce 103 | storageClassName: 104 | '[object Object]': null 105 | resources: 106 | requests: 107 | storage: 8Gi 108 | - metadata: 109 | name: unityvolx6 110 | spec: 111 | accessModes: 112 | - ReadWriteOnce 113 | storageClassName: 114 | '[object Object]': null 115 | resources: 116 | requests: 117 | storage: 8Gi 118 | - metadata: 119 | name: unityvolx7 120 | spec: 121 | accessModes: 122 | - ReadWriteOnce 123 | storageClassName: 124 | '[object Object]': null 125 | resources: 126 | requests: 127 | storage: 8Gi 128 | - metadata: 129 | name: unityvolx8 130 | spec: 131 | accessModes: 132 | - ReadWriteOnce 133 | storageClassName: 134 | '[object Object]': null 135 | resources: 136 | requests: 137 | storage: 8Gi 138 | - metadata: 139 | name: unityvolx9 140 | spec: 141 | accessModes: 142 | - ReadWriteOnce 143 | storageClassName: 144 | '[object Object]': null 145 | resources: 146 | requests: 147 | storage: 8Gi 148 | -------------------------------------------------------------------------------- /test/scale-test/10volumes/values.yaml: -------------------------------------------------------------------------------- 1 | StorageClassName: unity-iscsi 2 | replicas: 3 3 | -------------------------------------------------------------------------------- /test/scale-test/20volumes/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: 20volumes 2 | version: 1.0.0 3 | appVersion: 1.0.0 4 | description: | 5 | Tests CSI Unity XT deployments. 6 | keywords: 7 | - csi-unity 8 | - storage 9 | engine: gotpl 10 | -------------------------------------------------------------------------------- /test/scale-test/20volumes/templates/test.yaml: -------------------------------------------------------------------------------- 1 | kind: StatefulSet 2 | apiVersion: apps/v1 3 | metadata: 4 | name: unitytest 5 | namespace: test 6 | spec: 7 | serviceName: unitytest-20vol 8 | replicas: 9 | '[object Object]': null 10 | selector: 11 | matchLabels: 12 | app: unitytest 13 | template: 14 | metadata: 15 | labels: 16 | app: unitytest 17 | spec: 18 | serviceAccount: unitytest 19 | hostNetwork: true 20 | containers: 21 | - name: test 22 | image: 'quay.io/centos/centos:latest' 23 | command: 24 | - /bin/sleep 25 | - '3600' 26 | volumeMounts: 27 | - mountPath: /data0 28 | name: unityvolx0 29 | - mountPath: /data1 30 | name: unityvolx1 31 | - mountPath: /data2 32 | name: unityvolx2 33 | - mountPath: /data3 34 | name: unityvolx3 35 | - mountPath: /data4 36 | name: unityvolx4 37 | - mountPath: /data5 38 | name: unityvolx5 39 | - mountPath: /data6 40 | name: unityvolx6 41 | - mountPath: /data7 42 | name: unityvolx7 43 | - mountPath: /data8 44 | name: unityvolx8 45 | - mountPath: /data9 46 | name: unityvolx9 47 | - mountPath: /data10 48 | name: unityvolx10 49 | - mountPath: /data11 50 | name: unityvolx11 51 | - mountPath: /data12 52 | name: unityvolx12 53 | - mountPath: /data13 54 | name: unityvolx13 55 | - mountPath: /data14 56 | name: unityvolx14 57 | - mountPath: /data15 58 | name: unityvolx15 59 | - mountPath: /data16 60 | name: unityvolx16 61 | - mountPath: /data17 62 | name: unityvolx17 63 | - mountPath: /data18 64 | name: unityvolx18 65 | - mountPath: /data19 66 | name: unityvolx19 67 | volumeClaimTemplates: 68 | - metadata: 69 | name: unityvolx0 70 | spec: 71 | accessModes: 72 | - ReadWriteOnce 73 | storageClassName: 74 | '[object Object]': null 75 | resources: 76 | requests: 77 | storage: 8Gi 78 | - metadata: 79 | name: unityvolx1 80 | spec: 81 | accessModes: 82 | - ReadWriteOnce 83 | storageClassName: 84 | '[object Object]': null 85 | resources: 86 | requests: 87 | storage: 8Gi 88 | - metadata: 89 | name: unityvolx2 90 | spec: 91 | accessModes: 92 | - ReadWriteOnce 93 | storageClassName: 94 | '[object Object]': null 95 | resources: 96 | requests: 97 | storage: 8Gi 98 | - metadata: 99 | name: unityvolx3 100 | spec: 101 | accessModes: 102 | - ReadWriteOnce 103 | storageClassName: 104 | '[object Object]': null 105 | resources: 106 | requests: 107 | storage: 8Gi 108 | - metadata: 109 | name: unityvolx4 110 | spec: 111 | accessModes: 112 | - ReadWriteOnce 113 | storageClassName: 114 | '[object Object]': null 115 | resources: 116 | requests: 117 | storage: 8Gi 118 | - metadata: 119 | name: unityvolx5 120 | spec: 121 | accessModes: 122 | - ReadWriteOnce 123 | storageClassName: 124 | '[object Object]': null 125 | resources: 126 | requests: 127 | storage: 8Gi 128 | - metadata: 129 | name: unityvolx6 130 | spec: 131 | accessModes: 132 | - ReadWriteOnce 133 | storageClassName: 134 | '[object Object]': null 135 | resources: 136 | requests: 137 | storage: 8Gi 138 | - metadata: 139 | name: unityvolx7 140 | spec: 141 | accessModes: 142 | - ReadWriteOnce 143 | storageClassName: 144 | '[object Object]': null 145 | resources: 146 | requests: 147 | storage: 8Gi 148 | - metadata: 149 | name: unityvolx8 150 | spec: 151 | accessModes: 152 | - ReadWriteOnce 153 | storageClassName: 154 | '[object Object]': null 155 | resources: 156 | requests: 157 | storage: 8Gi 158 | - metadata: 159 | name: unityvolx9 160 | spec: 161 | accessModes: 162 | - ReadWriteOnce 163 | storageClassName: 164 | '[object Object]': null 165 | resources: 166 | requests: 167 | storage: 8Gi 168 | - metadata: 169 | name: unityvolx10 170 | spec: 171 | accessModes: 172 | - ReadWriteOnce 173 | storageClassName: 174 | '[object Object]': null 175 | resources: 176 | requests: 177 | storage: 8Gi 178 | - metadata: 179 | name: unityvolx11 180 | spec: 181 | accessModes: 182 | - ReadWriteOnce 183 | storageClassName: 184 | '[object Object]': null 185 | resources: 186 | requests: 187 | storage: 8Gi 188 | - metadata: 189 | name: unityvolx12 190 | spec: 191 | accessModes: 192 | - ReadWriteOnce 193 | storageClassName: 194 | '[object Object]': null 195 | resources: 196 | requests: 197 | storage: 8Gi 198 | - metadata: 199 | name: unityvolx13 200 | spec: 201 | accessModes: 202 | - ReadWriteOnce 203 | storageClassName: 204 | '[object Object]': null 205 | resources: 206 | requests: 207 | storage: 8Gi 208 | - metadata: 209 | name: unityvolx14 210 | spec: 211 | accessModes: 212 | - ReadWriteOnce 213 | storageClassName: 214 | '[object Object]': null 215 | resources: 216 | requests: 217 | storage: 8Gi 218 | - metadata: 219 | name: unityvolx15 220 | spec: 221 | accessModes: 222 | - ReadWriteOnce 223 | storageClassName: 224 | '[object Object]': null 225 | resources: 226 | requests: 227 | storage: 8Gi 228 | - metadata: 229 | name: unityvolx16 230 | spec: 231 | accessModes: 232 | - ReadWriteOnce 233 | storageClassName: 234 | '[object Object]': null 235 | resources: 236 | requests: 237 | storage: 8Gi 238 | - metadata: 239 | name: unityvolx17 240 | spec: 241 | accessModes: 242 | - ReadWriteOnce 243 | storageClassName: 244 | '[object Object]': null 245 | resources: 246 | requests: 247 | storage: 8Gi 248 | - metadata: 249 | name: unityvolx18 250 | spec: 251 | accessModes: 252 | - ReadWriteOnce 253 | storageClassName: 254 | '[object Object]': null 255 | resources: 256 | requests: 257 | storage: 8Gi 258 | - metadata: 259 | name: unityvolx19 260 | spec: 261 | accessModes: 262 | - ReadWriteOnce 263 | storageClassName: 264 | '[object Object]': null 265 | resources: 266 | requests: 267 | storage: 8Gi 268 | -------------------------------------------------------------------------------- /test/scale-test/20volumes/values.yaml: -------------------------------------------------------------------------------- 1 | StorageClassName: unity-iscsi 2 | replicas: 3 3 | -------------------------------------------------------------------------------- /test/scale-test/2volumes/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: 2volumes 2 | version: 1.0.0 3 | appVersion: 1.0.0 4 | description: | 5 | Tests CSI Unity deployments. 6 | keywords: 7 | - csi-unity 8 | - storage 9 | engine: gotpl 10 | -------------------------------------------------------------------------------- /test/scale-test/2volumes/templates/test.yaml: -------------------------------------------------------------------------------- 1 | kind: StatefulSet 2 | apiVersion: apps/v1 3 | metadata: 4 | name: unitytest 5 | namespace: test 6 | spec: 7 | serviceName: unitytest-2vol 8 | replicas: 9 | '[object Object]': null 10 | selector: 11 | matchLabels: 12 | app: unitytest 13 | template: 14 | metadata: 15 | labels: 16 | app: unitytest 17 | spec: 18 | serviceAccount: unitytest 19 | hostNetwork: true 20 | containers: 21 | - name: test 22 | image: 'quay.io/centos/centos:latest' 23 | command: 24 | - /bin/sleep 25 | - '3600' 26 | volumeMounts: 27 | - mountPath: /data0 28 | name: unityvolx0 29 | - mountPath: /data1 30 | name: unityvolx1 31 | volumeClaimTemplates: 32 | - metadata: 33 | name: unityvolx0 34 | spec: 35 | accessModes: 36 | - ReadWriteOnce 37 | storageClassName: 38 | '[object Object]': null 39 | resources: 40 | requests: 41 | storage: 5Gi 42 | - metadata: 43 | name: unityvolx1 44 | spec: 45 | accessModes: 46 | - ReadWriteOnce 47 | storageClassName: 48 | '[object Object]': null 49 | resources: 50 | requests: 51 | storage: 3Gi 52 | -------------------------------------------------------------------------------- /test/scale-test/2volumes/values.yaml: -------------------------------------------------------------------------------- 1 | StorageClassName: unity-iscsi 2 | replicas: 3 3 | -------------------------------------------------------------------------------- /test/scale-test/30volumes/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: 30volumes 2 | version: 1.0.0 3 | appVersion: 1.0.0 4 | description: | 5 | Tests CSI Unity deployments. 6 | keywords: 7 | - csi-unity 8 | - storage 9 | engine: gotpl 10 | -------------------------------------------------------------------------------- /test/scale-test/30volumes/templates/test.yaml: -------------------------------------------------------------------------------- 1 | kind: StatefulSet 2 | apiVersion: apps/v1 3 | metadata: 4 | name: unitytest 5 | namespace: test 6 | spec: 7 | serviceName: unitytest-30vol 8 | replicas: 9 | '[object Object]': null 10 | selector: 11 | matchLabels: 12 | app: unitytest 13 | template: 14 | metadata: 15 | labels: 16 | app: unitytest 17 | spec: 18 | serviceAccount: unitytest 19 | hostNetwork: true 20 | containers: 21 | - name: test 22 | image: 'quay.io/centos/centos:latest' 23 | command: 24 | - /bin/sleep 25 | - '3600' 26 | volumeMounts: 27 | - mountPath: /data0 28 | name: unityvolx0 29 | - mountPath: /data1 30 | name: unityvolx1 31 | - mountPath: /data2 32 | name: unityvolx2 33 | - mountPath: /data3 34 | name: unityvolx3 35 | - mountPath: /data4 36 | name: unityvolx4 37 | - mountPath: /data5 38 | name: unityvolx5 39 | - mountPath: /data6 40 | name: unityvolx6 41 | - mountPath: /data7 42 | name: unityvolx7 43 | - mountPath: /data8 44 | name: unityvolx8 45 | - mountPath: /data9 46 | name: unityvolx9 47 | - mountPath: /data10 48 | name: unityvolx10 49 | - mountPath: /data11 50 | name: unityvolx11 51 | - mountPath: /data12 52 | name: unityvolx12 53 | - mountPath: /data13 54 | name: unityvolx13 55 | - mountPath: /data14 56 | name: unityvolx14 57 | - mountPath: /data15 58 | name: unityvolx15 59 | - mountPath: /data16 60 | name: unityvolx16 61 | - mountPath: /data17 62 | name: unityvolx17 63 | - mountPath: /data18 64 | name: unityvolx18 65 | - mountPath: /data19 66 | name: unityvolx19 67 | - mountPath: /data20 68 | name: unityvolx20 69 | - mountPath: /data21 70 | name: unityvolx21 71 | - mountPath: /data22 72 | name: unityvolx22 73 | - mountPath: /data23 74 | name: unityvolx23 75 | - mountPath: /data24 76 | name: unityvolx24 77 | - mountPath: /data25 78 | name: unityvolx25 79 | - mountPath: /data26 80 | name: unityvolx26 81 | - mountPath: /data27 82 | name: unityvolx27 83 | - mountPath: /data28 84 | name: unityvolx28 85 | - mountPath: /data29 86 | name: unityvolx29 87 | volumeClaimTemplates: 88 | - metadata: 89 | name: unityvolx0 90 | spec: 91 | accessModes: 92 | - ReadWriteOnce 93 | storageClassName: 94 | '[object Object]': null 95 | resources: 96 | requests: 97 | storage: 8Gi 98 | - metadata: 99 | name: unityvolx1 100 | spec: 101 | accessModes: 102 | - ReadWriteOnce 103 | storageClassName: 104 | '[object Object]': null 105 | resources: 106 | requests: 107 | storage: 8Gi 108 | - metadata: 109 | name: unityvolx2 110 | spec: 111 | accessModes: 112 | - ReadWriteOnce 113 | storageClassName: 114 | '[object Object]': null 115 | resources: 116 | requests: 117 | storage: 8Gi 118 | - metadata: 119 | name: unityvolx3 120 | spec: 121 | accessModes: 122 | - ReadWriteOnce 123 | storageClassName: 124 | '[object Object]': null 125 | resources: 126 | requests: 127 | storage: 8Gi 128 | - metadata: 129 | name: unityvolx4 130 | spec: 131 | accessModes: 132 | - ReadWriteOnce 133 | storageClassName: 134 | '[object Object]': null 135 | resources: 136 | requests: 137 | storage: 8Gi 138 | - metadata: 139 | name: unityvolx5 140 | spec: 141 | accessModes: 142 | - ReadWriteOnce 143 | storageClassName: 144 | '[object Object]': null 145 | resources: 146 | requests: 147 | storage: 8Gi 148 | - metadata: 149 | name: unityvolx6 150 | spec: 151 | accessModes: 152 | - ReadWriteOnce 153 | storageClassName: 154 | '[object Object]': null 155 | resources: 156 | requests: 157 | storage: 8Gi 158 | - metadata: 159 | name: unityvolx7 160 | spec: 161 | accessModes: 162 | - ReadWriteOnce 163 | storageClassName: 164 | '[object Object]': null 165 | resources: 166 | requests: 167 | storage: 8Gi 168 | - metadata: 169 | name: unityvolx8 170 | spec: 171 | accessModes: 172 | - ReadWriteOnce 173 | storageClassName: 174 | '[object Object]': null 175 | resources: 176 | requests: 177 | storage: 8Gi 178 | - metadata: 179 | name: unityvolx9 180 | spec: 181 | accessModes: 182 | - ReadWriteOnce 183 | storageClassName: 184 | '[object Object]': null 185 | resources: 186 | requests: 187 | storage: 8Gi 188 | - metadata: 189 | name: unityvolx10 190 | spec: 191 | accessModes: 192 | - ReadWriteOnce 193 | storageClassName: 194 | '[object Object]': null 195 | resources: 196 | requests: 197 | storage: 8Gi 198 | - metadata: 199 | name: unityvolx11 200 | spec: 201 | accessModes: 202 | - ReadWriteOnce 203 | storageClassName: 204 | '[object Object]': null 205 | resources: 206 | requests: 207 | storage: 8Gi 208 | - metadata: 209 | name: unityvolx12 210 | spec: 211 | accessModes: 212 | - ReadWriteOnce 213 | storageClassName: 214 | '[object Object]': null 215 | resources: 216 | requests: 217 | storage: 8Gi 218 | - metadata: 219 | name: unityvolx13 220 | spec: 221 | accessModes: 222 | - ReadWriteOnce 223 | storageClassName: 224 | '[object Object]': null 225 | resources: 226 | requests: 227 | storage: 8Gi 228 | - metadata: 229 | name: unityvolx14 230 | spec: 231 | accessModes: 232 | - ReadWriteOnce 233 | storageClassName: 234 | '[object Object]': null 235 | resources: 236 | requests: 237 | storage: 8Gi 238 | - metadata: 239 | name: unityvolx15 240 | spec: 241 | accessModes: 242 | - ReadWriteOnce 243 | storageClassName: 244 | '[object Object]': null 245 | resources: 246 | requests: 247 | storage: 8Gi 248 | - metadata: 249 | name: unityvolx16 250 | spec: 251 | accessModes: 252 | - ReadWriteOnce 253 | storageClassName: 254 | '[object Object]': null 255 | resources: 256 | requests: 257 | storage: 8Gi 258 | - metadata: 259 | name: unityvolx17 260 | spec: 261 | accessModes: 262 | - ReadWriteOnce 263 | storageClassName: 264 | '[object Object]': null 265 | resources: 266 | requests: 267 | storage: 8Gi 268 | - metadata: 269 | name: unityvolx18 270 | spec: 271 | accessModes: 272 | - ReadWriteOnce 273 | storageClassName: 274 | '[object Object]': null 275 | resources: 276 | requests: 277 | storage: 8Gi 278 | - metadata: 279 | name: unityvolx19 280 | spec: 281 | accessModes: 282 | - ReadWriteOnce 283 | storageClassName: 284 | '[object Object]': null 285 | resources: 286 | requests: 287 | storage: 8Gi 288 | - metadata: 289 | name: unityvolx20 290 | spec: 291 | accessModes: 292 | - ReadWriteOnce 293 | storageClassName: 294 | '[object Object]': null 295 | resources: 296 | requests: 297 | storage: 8Gi 298 | - metadata: 299 | name: unityvolx21 300 | spec: 301 | accessModes: 302 | - ReadWriteOnce 303 | storageClassName: 304 | '[object Object]': null 305 | resources: 306 | requests: 307 | storage: 8Gi 308 | - metadata: 309 | name: unityvolx22 310 | spec: 311 | accessModes: 312 | - ReadWriteOnce 313 | storageClassName: 314 | '[object Object]': null 315 | resources: 316 | requests: 317 | storage: 8Gi 318 | - metadata: 319 | name: unityvolx23 320 | spec: 321 | accessModes: 322 | - ReadWriteOnce 323 | storageClassName: 324 | '[object Object]': null 325 | resources: 326 | requests: 327 | storage: 8Gi 328 | - metadata: 329 | name: unityvolx24 330 | spec: 331 | accessModes: 332 | - ReadWriteOnce 333 | storageClassName: 334 | '[object Object]': null 335 | resources: 336 | requests: 337 | storage: 8Gi 338 | - metadata: 339 | name: unityvolx25 340 | spec: 341 | accessModes: 342 | - ReadWriteOnce 343 | storageClassName: 344 | '[object Object]': null 345 | resources: 346 | requests: 347 | storage: 8Gi 348 | - metadata: 349 | name: unityvolx26 350 | spec: 351 | accessModes: 352 | - ReadWriteOnce 353 | storageClassName: 354 | '[object Object]': null 355 | resources: 356 | requests: 357 | storage: 8Gi 358 | - metadata: 359 | name: unityvolx27 360 | spec: 361 | accessModes: 362 | - ReadWriteOnce 363 | storageClassName: 364 | '[object Object]': null 365 | resources: 366 | requests: 367 | storage: 8Gi 368 | - metadata: 369 | name: unityvolx28 370 | spec: 371 | accessModes: 372 | - ReadWriteOnce 373 | storageClassName: 374 | '[object Object]': null 375 | resources: 376 | requests: 377 | storage: 8Gi 378 | - metadata: 379 | name: unityvolx29 380 | spec: 381 | accessModes: 382 | - ReadWriteOnce 383 | storageClassName: 384 | '[object Object]': null 385 | resources: 386 | requests: 387 | storage: 8Gi 388 | -------------------------------------------------------------------------------- /test/scale-test/30volumes/values.yaml: -------------------------------------------------------------------------------- 1 | StorageClassName: unity-iscsi 2 | replicas: 3 3 | -------------------------------------------------------------------------------- /test/scale-test/50volumes/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: 50volumes 2 | version: 1.0.0 3 | appVersion: 1.0.0 4 | description: | 5 | Tests CSI Unity deployments. 6 | keywords: 7 | - csi-unity 8 | - storage 9 | engine: gotpl 10 | -------------------------------------------------------------------------------- /test/scale-test/50volumes/values.yaml: -------------------------------------------------------------------------------- 1 | StorageClassName: unity-iscsi 2 | replicas: 3 3 | -------------------------------------------------------------------------------- /test/scale-test/README.md: -------------------------------------------------------------------------------- 1 | 14 | # Helm charts and scripts for scalability tests 15 | 16 | ## Helm charts 17 | | Name | Usage | 18 | |------------|-------| 19 | | 2volumes | Creates 2 volumes per pod/replica 20 | | 10volumes | Creates 10 volumes per pod/replica 21 | | 20volumes | Creates 20 volumes per pod/replica 22 | | 30volumes | Creates 30 volumes per pod/replica 23 | | 50volumes | Creates 50 volumes per pod/replica 24 | | 100volumes | Creates 100 volumes per pod/replica 25 | 26 | 27 | ## Scripts 28 | | Name | Usage | 29 | |----------------|-------| 30 | | scaletest.sh | Script to start the scalability tests 31 | | rescaletest.sh | Script to rescale pods to a different number of replicas 32 | 33 | 34 | ## Scale test prerequisites 35 | * Works only on Helm v3 36 | * Make sure the driver is installed and running fine in the k8s cluster 37 | * The namespace that is to be used for the scale test should be pre-created in the k8s cluster and make sure no objects are present under the namespace. 38 | * The right storage class to be used for the test should be pre-created and specified as StorageClassName in values.yaml under the test folder that is to be run. 39 | 40 | 41 | ## scaletest.sh 42 | The test is run using the script file scaletest.sh in the directory test/scale-tests. The script takes the below arguments. 43 | -r : number of replicas for the statefulset 44 | -t : the test name to be run [default: 2volumes] 45 | -n : namespace to be used to run the test [default: test] 46 | 47 | Examples: 48 | 49 | To scale the statefulset to 3 resplicas and then re-scale it back to 0 replicas: 50 | 51 | > ./scaletest.sh -r 3 -t 10volumes -n test 52 | 53 | To rescale the statefulset to 2 replicas: 54 | 55 | > sh rescaletest.sh -r 0 -t 10volumes -n test 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /test/scale-test/rescaletest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright © 2020 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | # rescaletest 15 | # This script will rescale helm deployments created as part of the scaletest 16 | # Finally delete all the pvc that were created 17 | 18 | 19 | TEST="2volumes" 20 | NAMESPACE="test" 21 | REPLICAS=-1 22 | 23 | # Usage information 24 | function usage { 25 | echo 26 | echo "`basename ${0}`" 27 | echo " -n namespace - Namespace in which to place the test. Default is: ${NAMESPACE}" 28 | echo " -t test - Test to run. Default is: ${TEST}. The value must point to a Helm Chart" 29 | echo " -r replicas - Number of replicas to create" 30 | echo 31 | exit 1 32 | } 33 | 34 | # Parse the options passed on the command line 35 | while getopts "n:r:t:" opt; do 36 | case $opt in 37 | t) 38 | TEST="${OPTARG}" 39 | ;; 40 | n) 41 | NAMESPACE="${OPTARG}" 42 | ;; 43 | r) 44 | REPLICAS="${OPTARG}" 45 | ;; 46 | \?) 47 | echo "Invalid option: -$OPTARG" >&2 48 | usage 49 | ;; 50 | :) 51 | echo "Option -$OPTARG requires an argument." >&2 52 | usage 53 | ;; 54 | esac 55 | done 56 | 57 | if [ ${REPLICAS} -eq -1 ]; then 58 | echo "No value for number of replicas provided"; 59 | usage 60 | fi 61 | 62 | TARGET=$(expr $REPLICAS \* 1) 63 | echo "Targeting replicas: $REPLICAS" 64 | echo "Targeting pods: $TARGET" 65 | 66 | helm upgrade scalevoltest --install --set "name=scalevoltest,namespace=test,replicas=$REPLICAS,storageClass=unity" --namespace "${NAMESPACE}" "${TEST}" 67 | 68 | waitOnRunning() { 69 | if [ "$1" = "" ]; 70 | then echo "arg: target" ; 71 | exit 2; 72 | fi 73 | WAITINGFOR=$1 74 | 75 | RUNNING=$(kubectl get pods -n "${NAMESPACE}" | grep "Running" | wc -l) 76 | while [ $RUNNING -ne $WAITINGFOR ]; 77 | do 78 | sleep 15 79 | RUNNING=$(kubectl get pods -n "${NAMESPACE}" | grep "Running" | wc -l) 80 | CREATING=$(kubectl get pods -n "${NAMESPACE}" | grep "ContainerCreating" | wc -l) 81 | TERMINATING=$(kubectl get pods -n "${NAMESPACE}" | grep "Terminating" | wc -l) 82 | PVCS=$(kubectl get pvc -n "${NAMESPACE}" | wc -l) 83 | date 84 | date >>log.output 85 | echo running $RUNNING creating $CREATING terminating $TERMINATING pvcs $PVCS 86 | echo running $RUNNING creating $CREATING terminating $TERMINATING pvcs $PVCS >>log.output 87 | done 88 | } 89 | 90 | waitOnRunning $TARGET 91 | 92 | echo "Re scale to $REPLICAS completed successfully" 93 | -------------------------------------------------------------------------------- /test/scale-test/scaletest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright © 2020 Dell Inc. or its subsidiaries. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | # scaletest 15 | # This script will kick off a test designed to stress the limits of the driver as well as the array 16 | # It will install a user supplied helm chart with a user supplied number of replicas. 17 | # Each replica will contain a number of volumes 18 | # The test will continue to run until all replicas have been started, volumes created, and mapped 19 | # Then the replicas will be scaled down to 0 and then all the created pvc will be flushed out. 20 | 21 | TEST="2volumes" 22 | NAMESPACE="test" 23 | REPLICAS=-1 24 | 25 | # Usage information 26 | function usage { 27 | echo 28 | echo "`basename ${0}`" 29 | echo " -n namespace - Namespace in which to place the test. Default is: ${NAMESPACE}" 30 | echo " -t test - Test to run. Default is: ${TEST}. The value must point to a Helm Chart" 31 | echo " -r replicas - Number of replicas to create" 32 | echo " -p replicas - Protocol" 33 | echo 34 | exit 1 35 | } 36 | 37 | # Parse the options passed on the command line 38 | while getopts "n:r:t:" opt; do 39 | case $opt in 40 | t) 41 | TEST="${OPTARG}" 42 | ;; 43 | n) 44 | NAMESPACE="${OPTARG}" 45 | ;; 46 | r) 47 | REPLICAS="${OPTARG}" 48 | ;; 49 | \?) 50 | echo "Invalid option: -$OPTARG" >&2 51 | usage 52 | ;; 53 | :) 54 | echo "Option -$OPTARG requires an argument." >&2 55 | usage 56 | ;; 57 | esac 58 | done 59 | 60 | if [ ${REPLICAS} -eq -1 ]; then 61 | echo "No value for number of replicas provided"; 62 | usage 63 | fi 64 | 65 | validateServiceAccount() { 66 | # validate that the service account exists 67 | ACCOUNTS=$(kubectl describe serviceaccount -n "${NAMESPACE}" "unitytest") 68 | if [ $? -ne 0 ]; then 69 | echo "Creating Service Account" 70 | kubectl create -n ${NAMESPACE} -f serviceAccount.yaml 71 | fi 72 | } 73 | 74 | validateServiceAccount 75 | 76 | TARGET=$(expr $REPLICAS \* 1) 77 | echo "Targeting replicas: $REPLICAS" 78 | echo "Targeting pods: $TARGET" 79 | 80 | helm install scalevoltest --set "name=scalevoltest,replicas=$REPLICAS,storageClass=unity,namespace=${NAMESPACE}" -n ${NAMESPACE} ./${TEST} 81 | 82 | waitOnRunning() { 83 | if [ "$1" = "" ]; 84 | then echo "arg: target" ; 85 | exit 2; 86 | fi 87 | WAITINGFOR=$1 88 | 89 | RUNNING=$(kubectl get pods -n ${NAMESPACE} | grep "Running" | wc -l) 90 | 91 | while [ $RUNNING -ne $WAITINGFOR ]; 92 | do 93 | sleep 5 94 | RUNNING=$(kubectl get pods -n ${NAMESPACE} | grep "Running" | wc -l) 95 | CREATING=$(kubectl get pods -n ${NAMESPACE} | grep "ContainerCreating" | wc -l) 96 | PVCS=$(kubectl get pvc -n ${NAMESPACE} | wc -l) 97 | date 98 | date >>log.output 99 | echo running $RUNNING creating $CREATING pvcs $PVCS 100 | echo running $RUNNING creating $CREATING pvcs $PVCS >>log.output 101 | done 102 | } 103 | 104 | SECONDS=0 105 | waitOnRunning $TARGET 106 | END=$SECONDS 107 | echo "Time taken to create $REPLICAS replicas with $TEST: $(( (${END} / 60) % 60 ))m $(( ${END} % 60 ))s" 108 | sleep 30 109 | 110 | SECONDS=0 111 | # rescale the environment back to 0 replicas 112 | sh rescaletest.sh -n "${NAMESPACE}" -r 0 -t "${TEST}" 113 | END=$SECONDS 114 | echo "Time taken to rescale from $REPLICAS to 0 replicas on test $TEST: $(( (${END} / 60) % 60 ))m $(( ${END} % 60 ))s" 115 | 116 | echo "Deleting hem installation under namespace: ${NAMESPACE}" 117 | helm delete -n ${NAMESPACE} scalevoltest 118 | 119 | echo "Deleting all pvcs under namespace: ${NAMESPACE}" 120 | kubectl delete pvc --all -n ${NAMESPACE} 121 | -------------------------------------------------------------------------------- /test/scale-test/serviceAccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: unitytest 5 | --------------------------------------------------------------------------------