diff --git a/.dockerignore b/.dockerignore index 5a21efb..b483184 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,9 @@ -chart -dev -examples -contrib -node_modules -Dockerfile* -TODO.md -.git -/ci +** + +!/bin +!/csi_proto +!/csi_proxy_proto +!/docker +!/LICENSE +!/package*.json +!/src diff --git a/.github/bin/docker-release-windows.sh b/.github/bin/docker-release-windows.sh new file mode 100755 index 0000000..21e2e13 --- /dev/null +++ b/.github/bin/docker-release-windows.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -e + +echo "$DOCKER_PASSWORD" | docker login docker.io -u "$DOCKER_USERNAME" --password-stdin +echo "$GHCR_PASSWORD" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin + +export DOCKER_ORG="democraticcsi" +export DOCKER_PROJECT="democratic-csi" +export DOCKER_REPO="docker.io/${DOCKER_ORG}/${DOCKER_PROJECT}" + +export GHCR_ORG="democratic-csi" +export GHCR_PROJECT="democratic-csi" +export GHCR_REPO="ghcr.io/${GHCR_ORG}/${GHCR_PROJECT}" + +export MANIFEST_NAME="democratic-csi-combined:${IMAGE_TAG}" + +if [[ -n "${IMAGE_TAG}" ]]; then + # create local manifest to work with + buildah manifest rm "${MANIFEST_NAME}" || true + buildah manifest create "${MANIFEST_NAME}" + + # all all the existing linux data to the manifest + buildah manifest add "${MANIFEST_NAME}" --all "${DOCKER_REPO}:${IMAGE_TAG}" + buildah manifest inspect "${MANIFEST_NAME}" + + # import pre-built images + buildah pull docker-archive:democratic-csi-windows-ltsc2019.tar + buildah pull docker-archive:democratic-csi-windows-ltsc2022.tar + + # add pre-built images to manifest + buildah manifest add "${MANIFEST_NAME}" democratic-csi-windows:${GITHUB_RUN_ID}-ltsc2019 + buildah manifest add "${MANIFEST_NAME}" democratic-csi-windows:${GITHUB_RUN_ID}-ltsc2022 + buildah manifest inspect "${MANIFEST_NAME}" + + # push manifest + buildah manifest push --all "${MANIFEST_NAME}" docker://${DOCKER_REPO}:${IMAGE_TAG} + buildah manifest push --all "${MANIFEST_NAME}" docker://${GHCR_REPO}:${IMAGE_TAG} + + # cleanup + buildah manifest rm "${MANIFEST_NAME}" || true +else + : +fi \ No newline at end of file diff --git a/.github/bin/docker-release.sh b/.github/bin/docker-release.sh index f9686c1..f5fe0d4 100755 --- a/.github/bin/docker-release.sh +++ b/.github/bin/docker-release.sh @@ -11,20 +11,12 @@ export GHCR_ORG="democratic-csi" export GHCR_PROJECT="democratic-csi" export GHCR_REPO="ghcr.io/${GHCR_ORG}/${GHCR_PROJECT}" -if [[ $GITHUB_REF == refs/tags/* ]]; then - export GIT_TAG=${GITHUB_REF#refs/tags/} -else - export GIT_BRANCH=${GITHUB_REF#refs/heads/} -fi - -if [[ -n "${GIT_TAG}" ]]; then - docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${GIT_TAG} -t ${GHCR_REPO}:${GIT_TAG} . -elif [[ -n "${GIT_BRANCH}" ]]; then - if [[ "${GIT_BRANCH}" == "master" ]]; then - docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:latest -t ${GHCR_REPO}:latest . - else - docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${GIT_BRANCH} -t ${GHCR_REPO}:${GIT_BRANCH} . - fi +if [[ -n "${IMAGE_TAG}" ]]; then + # -t ${GHCR_REPO}:${IMAGE_TAG} + docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${IMAGE_TAG} \ + --label "org.opencontainers.image.created=$(date -u --iso-8601=seconds)" \ + --label "org.opencontainers.image.revision=${GITHUB_SHA}" \ + . else : fi diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5bff4fa..fafd564 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,12 +17,13 @@ jobs: with: access_token: ${{ github.token }} - build-npm: - name: build-npm - runs-on: - - self-hosted + build-npm-linux-amd64: + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 + - uses: actions/setup-node@v3 + with: + node-version: 16 - shell: bash name: npm install run: | @@ -30,35 +31,84 @@ jobs: - name: upload build uses: actions/upload-artifact@v2 with: - name: node-modules - #path: node_modules/ - path: node_modules.tar.gz - retention-days: 7 + name: node-modules-linux-amd64 + path: node_modules-linux-amd64.tar.gz + retention-days: 1 - csi-sanity-synology: + build-npm-windows-amd64: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v3 + with: + node-version: 16 + - shell: pwsh + name: npm install + run: | + ci\bin\build.ps1 + - name: upload build + uses: actions/upload-artifact@v2 + with: + name: node-modules-windows-amd64 + path: node_modules-windows-amd64.tar.gz + retention-days: 1 + + csi-sanity-synology-dsm6: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: config: - - synlogy/iscsi.yaml + - synlogy/dsm6/iscsi.yaml runs-on: - self-hosted + - Linux + - X64 - csi-sanity-synology steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests ci/bin/run.sh env: TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" - SYNOLOGY_HOST: ${{ secrets.SANITY_SYNOLOGY_HOST }} - SYNOLOGY_PORT: ${{ secrets.SANITY_SYNOLOGY_PORT }} + SYNOLOGY_HOST: ${{ secrets.SANITY_SYNOLOGY_DSM6_HOST }} + SYNOLOGY_PORT: ${{ secrets.SANITY_SYNOLOGY_DSM6_PORT }} + SYNOLOGY_USERNAME: ${{ secrets.SANITY_SYNOLOGY_USERNAME }} + SYNOLOGY_PASSWORD: ${{ secrets.SANITY_SYNOLOGY_PASSWORD }} + SYNOLOGY_VOLUME: ${{ secrets.SANITY_SYNOLOGY_VOLUME }} + + csi-sanity-synology-dsm7: + needs: + - build-npm-linux-amd64 + strategy: + fail-fast: false + matrix: + config: + - synlogy/dsm7/iscsi.yaml + runs-on: + - self-hosted + - Linux + - X64 + - csi-sanity-synology + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v2 + with: + name: node-modules-linux-amd64 + - name: csi-sanity + run: | + # run tests + ci/bin/run.sh + env: + TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" + SYNOLOGY_HOST: ${{ secrets.SANITY_SYNOLOGY_DSM7_HOST }} + SYNOLOGY_PORT: ${{ secrets.SANITY_SYNOLOGY_DSM7_PORT }} SYNOLOGY_USERNAME: ${{ secrets.SANITY_SYNOLOGY_USERNAME }} SYNOLOGY_PASSWORD: ${{ secrets.SANITY_SYNOLOGY_PASSWORD }} SYNOLOGY_VOLUME: ${{ secrets.SANITY_SYNOLOGY_VOLUME }} @@ -66,7 +116,7 @@ jobs: # api-based drivers csi-sanity-truenas-scale-22_02: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: @@ -74,16 +124,18 @@ jobs: - truenas/scale/22.02/scale-iscsi.yaml - truenas/scale/22.02/scale-nfs.yaml # 80 char limit - #- truenas/scale-smb.yaml + - truenas/scale/22.02/scale-smb.yaml runs-on: - self-hosted - - csi-sanity-zfs-local - #- csi-sanity-truenas-scale + - Linux + - X64 + - csi-sanity-truenas + #- csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -97,25 +149,27 @@ jobs: # ssh-based drivers csi-sanity-truenas-core-12_0: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: config: # 63 char limit - #- truenas/core-iscsi.yaml + - truenas/core/12.0/core-iscsi.yaml - truenas/core/12.0/core-nfs.yaml # 80 char limit - #- truenas/core-smb.yaml + - truenas/core/12.0/core-smb.yaml runs-on: - self-hosted - - csi-sanity-zfs-local - #- csi-sanity-truenas-core + - Linux + - X64 + #- csi-sanity-truenas + - csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -129,7 +183,7 @@ jobs: # ssh-based drivers csi-sanity-truenas-core-13_0: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: @@ -137,16 +191,18 @@ jobs: - truenas/core/13.0/core-iscsi.yaml - truenas/core/13.0/core-nfs.yaml # 80 char limit - #- truenas/core-smb.yaml + - truenas/core/13.0/core-smb.yaml runs-on: - self-hosted - - csi-sanity-zfs-local - #- csi-sanity-truenas-core + - Linux + - X64 + #- csi-sanity-truenas + - csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -160,21 +216,24 @@ jobs: # ssh-based drivers csi-sanity-zfs-generic: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: config: - zfs-generic/iscsi.yaml - zfs-generic/nfs.yaml + - zfs-generic/smb.yaml runs-on: - self-hosted + - Linux + - X64 - csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -185,10 +244,70 @@ jobs: SERVER_USERNAME: ${{ secrets.SANITY_ZFS_GENERIC_USERNAME }} SERVER_PASSWORD: ${{ secrets.SANITY_ZFS_GENERIC_PASSWORD }} + # client drivers + csi-sanity-client: + needs: + - build-npm-linux-amd64 + strategy: + fail-fast: false + matrix: + config: + - client/nfs.yaml + - client/smb.yaml + runs-on: + - self-hosted + - Linux + - X64 + - csi-sanity-client + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v2 + with: + name: node-modules-linux-amd64 + - name: csi-sanity + run: | + # run tests + ci/bin/run.sh + env: + TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" + SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_HOST }} + SHARE_NAME: tank_client_smb + CSI_SANITY_SKIP: "should fail when requesting to create a snapshot with already existing name and different source volume ID|should fail when requesting to create a volume with already existing name and different capacity" + + csi-sanity-client-windows: + needs: + - build-npm-windows-amd64 + strategy: + fail-fast: false + matrix: + config: + - client\smb.yaml + runs-on: + - self-hosted + - Windows + - X64 + - csi-sanity-client + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v2 + with: + name: node-modules-windows-amd64 + - name: csi-sanity + run: | + # run tests + ci\bin\run.ps1 + env: + TEMPLATE_CONFIG_FILE: ".\\ci\\configs\\${{ matrix.config }}" + SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_HOST }} + SHARE_NAME: tank_client_smb + CSI_SANITY_SKIP: "should fail when requesting to create a snapshot with already existing name and different source volume ID|should fail when requesting to create a volume with already existing name and different capacity" + + + # zfs-local drivers csi-sanity-zfs-local: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: @@ -197,12 +316,14 @@ jobs: - zfs-local/dataset.yaml runs-on: - self-hosted + - Linux + - X64 - csi-sanity-zfs-local steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -213,36 +334,108 @@ jobs: # local-hostpath driver csi-sanity-local-hostpath: needs: - - build-npm + - build-npm-linux-amd64 + - build-npm-windows-amd64 strategy: fail-fast: false matrix: - config: - - local-hostpath/basic.yaml + os: [Linux, Windows] + include: + - os: Linux + npmartifact: node-modules-linux-amd64 + template: "./ci/configs/local-hostpath/basic.yaml" + run: | + # run tests + ci/bin/run.sh + - os: Windows + npmartifact: node-modules-windows-amd64 + template: ".\\ci\\configs\\local-hostpath\\basic.yaml" + run: | + # run tests + ci\bin\run.ps1 runs-on: - self-hosted + - ${{ matrix.os }} + - X64 + - csi-sanity-local-hostpath steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: ${{ matrix.npmartifact }} + - name: csi-sanity + run: ${{ matrix.run }} + env: + TEMPLATE_CONFIG_FILE: "${{ matrix.template }}" + CSI_SANITY_SKIP: "should fail when requesting to create a snapshot with already existing name and different source volume ID|should fail when requesting to create a volume with already existing name and different capacity" + + csi-sanity-windows-node: + needs: + - build-npm-windows-amd64 + strategy: + fail-fast: false + matrix: + config: + - windows\iscsi.yaml + - windows\smb.yaml + runs-on: + - self-hosted + - Windows + - X64 + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v2 + with: + name: node-modules-windows-amd64 - name: csi-sanity run: | # run tests - ci/bin/run.sh + ci\bin\run.ps1 env: - TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" - CSI_SANITY_SKIP: "should fail when requesting to create a snapshot with already existing name and different source volume ID|should fail when requesting to create a volume with already existing name and different capacity" + TEMPLATE_CONFIG_FILE: ".\\ci\\configs\\${{ matrix.config }}" + SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_HOST }} + SERVER_USERNAME: ${{ secrets.SANITY_ZFS_GENERIC_USERNAME }} + SERVER_PASSWORD: ${{ secrets.SANITY_ZFS_GENERIC_PASSWORD }} + CSI_SANITY_FOCUS: "Node Service" - build-docker: + determine-image-tag: + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag.outputs.tag }} + steps: + - id: tag + run: | + if [[ $GITHUB_REF == refs/tags/* ]]; then + export GIT_TAG=${GITHUB_REF#refs/tags/} + else + export GIT_BRANCH=${GITHUB_REF#refs/heads/} + fi + if [[ -n "${GIT_TAG}" ]]; then + echo "::set-output name=tag::${GIT_TAG}" + elif [[ -n "${GIT_BRANCH}" ]]; then + if [[ "${GIT_BRANCH}" == "master" ]]; then + echo "::set-output name=tag::latest" + else + echo "::set-output name=tag::${GIT_BRANCH}" + fi + else + : + fi + + build-docker-linux: needs: - - csi-sanity-synology + - determine-image-tag + - csi-sanity-synology-dsm6 + - csi-sanity-synology-dsm7 - csi-sanity-truenas-scale-22_02 - csi-sanity-truenas-core-12_0 - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic + - csi-sanity-client + - csi-sanity-client-windows - csi-sanity-zfs-local - csi-sanity-local-hostpath + - csi-sanity-windows-node runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -250,7 +443,7 @@ jobs: run: | export ARCH=$([ $(uname -m) = "x86_64" ] && echo "amd64" || echo "arm64") mkdir -p ~/.docker/cli-plugins/ - wget -qO ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.5.1/buildx-v0.5.1.linux-${ARCH} + wget -qO ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.8.2/buildx-v0.8.2.linux-${ARCH} chmod a+x ~/.docker/cli-plugins/docker-buildx docker info docker buildx version @@ -267,3 +460,78 @@ jobs: GHCR_PASSWORD: ${{ secrets.GHCR_PASSWORD }} DOCKER_CLI_EXPERIMENTAL: enabled DOCKER_BUILD_PLATFORM: linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le + IMAGE_TAG: ${{needs.determine-image-tag.outputs.tag}} + + build-docker-windows: + needs: + - csi-sanity-synology-dsm6 + - csi-sanity-synology-dsm7 + - csi-sanity-truenas-scale-22_02 + - csi-sanity-truenas-core-12_0 + - csi-sanity-truenas-core-13_0 + - csi-sanity-zfs-generic + - csi-sanity-client + - csi-sanity-client-windows + - csi-sanity-zfs-local + - csi-sanity-local-hostpath + - csi-sanity-windows-node + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-2019, windows-2022] + include: + - os: windows-2019 + core_base_tag: ltsc2019 + nano_base_tag: "1809" + file: Dockerfile.Windows + - os: windows-2022 + core_base_tag: ltsc2022 + nano_base_tag: ltsc2022 + file: Dockerfile.Windows + steps: + - uses: actions/checkout@v2 + - name: docker build + shell: bash + run: | + docker info + docker build --pull -f ${{ matrix.file }} --build-arg NANO_BASE_TAG=${{ matrix.nano_base_tag }} --build-arg CORE_BASE_TAG=${{ matrix.core_base_tag }} -t democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} \ + --label "org.opencontainers.image.created=$(date -u --iso-8601=seconds)" \ + --label "org.opencontainers.image.revision=${GITHUB_SHA}" \ + . + docker inspect democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} + docker save democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} -o democratic-csi-windows-${{ matrix.core_base_tag }}.tar + - name: upload image tar + uses: actions/upload-artifact@v2 + with: + name: democratic-csi-windows-${{ matrix.core_base_tag }}.tar + path: democratic-csi-windows-${{ matrix.core_base_tag }}.tar + retention-days: 1 + + push-docker-windows: + needs: + - build-docker-linux + - build-docker-windows + - determine-image-tag + runs-on: + - self-hosted + - buildah + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v3 + with: + name: democratic-csi-windows-ltsc2019.tar + - uses: actions/download-artifact@v3 + with: + name: democratic-csi-windows-ltsc2022.tar + - name: push windows images with buildah + run: | + #.github/bin/install_latest_buildah.sh + buildah version + .github/bin/docker-release-windows.sh + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }} + GHCR_PASSWORD: ${{ secrets.GHCR_PASSWORD }} + DOCKER_CLI_EXPERIMENTAL: enabled + IMAGE_TAG: ${{needs.determine-image-tag.outputs.tag}} diff --git a/.gitignore b/.gitignore index 26ff709..2855d28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ +**~ node_modules dev +/ci/bin/*dev* diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f5444d..a0d3b85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +# v1.7.0 + +Released 2022-06-08 + +The windows release. + +- windows smb, iscsi, and local-hostpath support (requires chart `v0.13.0+`) +- ntfs, exfat, vfat fs support +- `zfs-generic-smb` driver +- synology improvements + - DSM7 support + - synology enhancements to allow templates to be configured at various + 'levels' +- testing improvements + - support (for testing) generating volume_id from name + - test all the smb variants + - test all nfs/smb client drivers +- misc fixes + - wait for chown/chmod jobs to complete (freenas) + - general improvement to smb behavior throughout + - better logging + - better sudo logic throughout + - minor fixes throughout +- more robust logic for connecting to iscsi devices with partition tables +- massive performance improvement for ssh-based drivers (reusing existing + connection instead of new connection per-command) +- dep bumps +- trimmed container images +- windows container images for 2019 and 2022 + # v1.6.3 Released 2022-04-08 diff --git a/Dockerfile b/Dockerfile index 82349b4..f5abf73 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y locales && rm -rf /var/lib/apt/lists/* && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 ENV LANG=en_US.utf8 -ENV NODE_VERSION=v16.14.2 +ENV NODE_VERSION=v16.15.1 ENV NODE_ENV=production # install build deps @@ -43,6 +43,8 @@ RUN rm -rf docker FROM debian:11-slim LABEL org.opencontainers.image.source https://github.com/democratic-csi/democratic-csi +LABEL org.opencontainers.image.url https://github.com/democratic-csi/democratic-csi +LABEL org.opencontainers.image.licenses MIT ENV DEBIAN_FRONTEND=noninteractive @@ -73,7 +75,7 @@ COPY --from=build /usr/local/lib/nodejs/bin/node /usr/local/bin/node # netbase is required by rpcbind/rpcinfo to work properly # /etc/{services,rpc} are required RUN apt-get update && \ - apt-get install -y netbase socat e2fsprogs xfsprogs btrfs-progs fatresize dosfstools nfs-common cifs-utils sudo rsync && \ + apt-get install -y netbase socat e2fsprogs exfatprogs xfsprogs btrfs-progs fatresize dosfstools ntfs-3g nfs-common cifs-utils fdisk gdisk cloud-guest-utils sudo rsync && \ rm -rf /var/lib/apt/lists/* # controller requirements diff --git a/Dockerfile.Windows b/Dockerfile.Windows new file mode 100644 index 0000000..2d7436c --- /dev/null +++ b/Dockerfile.Windows @@ -0,0 +1,100 @@ +# +# https://github.com/kubernetes/kubernetes/blob/master/test/images/windows/powershell-helper/Dockerfile_windows +# https://github.com/kubernetes/kubernetes/blob/master/test/images/busybox/Dockerfile_windows +# https://github.com/kubernetes/kubernetes/tree/master/test/images#windows-test-images-considerations +# https://stefanscherer.github.io/find-dependencies-in-windows-containers/ +# +# docker build --build-arg NANO_BASE_TAG=1809 --build-arg CORE_BASE_TAG=ltsc2019 -t foobar -f Dockerfile.Windows . +# docker run --rm -ti --entrypoint powershell foobar +# docker run --rm foobar +# docker save foobar -o foobar.tar +# buildah pull docker-archive:foobar.tar + +# mcr.microsoft.com/windows/servercore:ltsc2019 +# mcr.microsoft.com/windows/nanoserver:1809 + +ARG NANO_BASE_TAG +ARG CORE_BASE_TAG + +FROM mcr.microsoft.com/windows/servercore:${CORE_BASE_TAG} as powershell + +# install powershell +ENV PS_VERSION=6.2.7 +ADD https://github.com/PowerShell/PowerShell/releases/download/v$PS_VERSION/PowerShell-$PS_VERSION-win-x64.zip /PowerShell/powershell.zip + +RUN cd C:\PowerShell &\ + tar.exe -xf powershell.zip &\ + del powershell.zip &\ + mklink powershell.exe pwsh.exe + + +FROM mcr.microsoft.com/windows/servercore:${CORE_BASE_TAG} as build + +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +#ENV GPG_VERSION 4.0.2 +ENV GPG_VERSION 2.3.4 + +RUN Invoke-WebRequest $('https://files.gpg4win.org/gpg4win-vanilla-{0}.exe' -f $env:GPG_VERSION) -OutFile 'gpg4win.exe' -UseBasicParsing ; \ + Start-Process .\gpg4win.exe -ArgumentList '/S' -NoNewWindow -Wait + +# https://github.com/nodejs/node#release-keys +RUN @( \ + '4ED778F539E3634C779C87C6D7062848A1AB005C', \ + '141F07595B7B3FFE74309A937405533BE57C7D57', \ + '94AE36675C464D64BAFA68DD7434390BDBE9B9C5', \ + '74F12602B6F1C4E913FAA37AD3A89613643B6201', \ + '71DCFD284A79C3B38668286BC97EC7A07EDE3FC1', \ + '61FC681DFB92A079F1685E77973F295594EC4689', \ + '8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600', \ + 'C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8', \ + 'C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C', \ + 'DD8F2338BAE7501E3DD5AC78C273792F7D83545D', \ + 'A48C2BEE680E841632CD4E44F07496B3EB3C1762', \ + '108F52B48DB57BB0CC439B2997B01419BD92F80A', \ + 'B9E2F5981AA6E0CD28160D9FF13993A75599653C' \ + ) | foreach { \ + gpg --keyserver hkps://keys.openpgp.org --recv-keys $_ ; \ + } + +ENV NODE_VERSION 16.15.1 + +RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/SHASUMS256.txt.asc' -f $env:NODE_VERSION) -OutFile 'SHASUMS256.txt.asc' -UseBasicParsing ; +#RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/SHASUMS256.txt.asc' -f $env:NODE_VERSION) -OutFile 'SHASUMS256.txt.asc' -UseBasicParsing ; \ +# gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc +#gpg --verify SHASUMS256.txt.sig SHASUMS256.txt + +RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f $env:NODE_VERSION) -OutFile 'node.zip' -UseBasicParsing ; \ + $sum = $(cat SHASUMS256.txt.asc | sls $(' node-v{0}-win-x64.zip' -f $env:NODE_VERSION)) -Split ' ' ; \ + if ((Get-FileHash node.zip -Algorithm sha256).Hash -ne $sum[0]) { Write-Error 'SHA256 mismatch' } ; \ + Expand-Archive node.zip -DestinationPath C:\ ; \ + Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs' + +#RUN setx /M PATH "%PATH%;C:\nodejs" +RUN setx /M PATH $(${Env:PATH} + \";C:\nodejs\") + +RUN node --version; npm --version; + +RUN mkdir /app +WORKDIR /app + +COPY package*.json ./ +RUN npm install --only=production; ls / +COPY . . + +FROM mcr.microsoft.com/windows/nanoserver:${NANO_BASE_TAG} + +LABEL org.opencontainers.image.source https://github.com/democratic-csi/democratic-csi +LABEL org.opencontainers.image.url https://github.com/democratic-csi/democratic-csi +LABEL org.opencontainers.image.licenses MIT + +# if additional dlls are required can copy like this +#COPY --from=build /Windows/System32/nltest.exe /Windows/System32/nltest.exe + +COPY --from=build /app /app +WORKDIR /app + +# this works for both host-process and non-host-process container semantics +COPY --from=build /nodejs/node.exe ./bin + +ENTRYPOINT [ "bin/node.exe", "--expose-gc", "bin/democratic-csi" ] diff --git a/README.md b/README.md index f934689..ae5d8db 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,16 @@ Predominantly 3 things are needed: You should install/configure the requirements for both nfs and iscsi. +### cifs + +``` +RHEL / CentOS +sudo yum install -y cifs-utils + +Ubuntu / Debian +sudo apt-get install -y cifs-utils +``` + ### nfs ``` @@ -176,6 +186,35 @@ volume is/was provisioned. The nature of this `driver` also prevents the enforcement of quotas. In short the requested volume size is generally ignored. +### windows + +Support for Windows was introduced in `v1.7.0`. Currently support is limited +to kubernetes nodes capabale of running `HostProcess` containers. Support was +tested against `Windows Server 2019` using `rke2-v1.24`. Currently any of the +`-smb` and `-iscsi` drivers will work. Support for `ntfs` was added to the +linux nodes as well (using the `ntfs3` driver) so volumes created can be +utilized by nodes with either operating system (in the case of `cifs` by both +simultaneously). + +Due to current limits in the kubernetes tooling it is not possible to use the +`local-hostpath` driver but support is implemented in this project and will +work as soon as kubernetes support is available. + +``` +# ensure all updates are installed + +# enable the container feature +Enable-WindowsOptionalFeature -Online -FeatureName Containers –All + +# create symbolic link due to current limitations in the driver-registrar container +New-Item -ItemType SymbolicLink -Path "C:\registration\" -Target "C:\var\lib\kubelet\plugins_registry\" + +# install a HostProcess compatible kubernetes +``` + +- https://kubernetes.io/blog/2021/08/16/windows-hostprocess-containers/ +- https://kubernetes.io/docs/tasks/configure-pod-container/create-hostprocess-pod/ + ## Server Prep Server preparation depends slightly on which `driver` you are using. @@ -201,6 +240,7 @@ Ensure the following services are configurged and running: - ensure `zsh`, `bash`, or `sh` is set as the root shell, `csh` gives false errors due to quoting - nfs - iscsi + - (fixed in 12.0-U2+) when using the FreeNAS API concurrently the `/etc/ctl.conf` file on the server can become invalid, some sample scripts are provided in the `contrib` directory to clean things up ie: copy the @@ -216,7 +256,7 @@ Ensure the following services are configurged and running: - `curl --header "Accept: application/json" --user root: 'http(s):///api/v2.0/iscsi/initiator'` - `curl --header "Accept: application/json" --user root: 'http(s):///api/v2.0/iscsi/auth'` - The maximum number of volumes is limited to 255 by default on FreeBSD (physical devices such as disks and CD-ROM drives count against this value). - Be sure to properly adjust both [tunables](https://www.freebsd.org/cgi/man.cgi?query=ctl&sektion=4#end) `kern.cam.ctl.max_ports` and `kern.cam.ctl.max_luns` to avoid running out of resources when dynamically provisioning iSCSI volumes on FreeNAS or TrueNAS Core. + Be sure to properly adjust both [tunables](https://www.freebsd.org/cgi/man.cgi?query=ctl&sektion=4#end) `kern.cam.ctl.max_ports` and `kern.cam.ctl.max_luns` to avoid running out of resources when dynamically provisioning iSCSI volumes on FreeNAS or TrueNAS Core. - smb @@ -273,17 +313,38 @@ Issues to review: - https://jira.ixsystems.com/browse/NAS-108522 - https://jira.ixsystems.com/browse/NAS-107219 -### ZoL (zfs-generic-nfs, zfs-generic-iscsi) +### ZoL (zfs-generic-nfs, zfs-generic-iscsi, zfs-generic-smb) Ensure ssh and zfs is installed on the nfs/iscsi server and that you have installed `targetcli`. -- `sudo yum install targetcli -y` -- `sudo apt-get -y install targetcli-fb` +The driver executes many commands over an ssh connection. You may consider +disabling all the `motd` details for the ssh user as it can spike the cpu +unecessarily: + +- https://askubuntu.com/questions/318592/how-can-i-remove-the-landscape-canonical-com-greeting-from-motd +- https://linuxconfig.org/disable-dynamic-motd-and-news-on-ubuntu-20-04-focal-fossa-linux + +``` +####### iscsi +yum install targetcli -y +apt-get -y install targetcli-fb + +####### smb +apt-get install -y samba smbclient + +# create posix user +groupadd -g 1001 smbroot +useradd -u 1001 -g 1001 -M -N -s /sbin/nologin smbroot +passwd smbroot (optional) + +# create smb user and set password +smbpasswd -L -a smbroot +``` ### Synology (synology-iscsi) -Ensure iscsi manager has been installed and is generally setup/configured. +Ensure iscsi manager has been installed and is generally setup/configured. DSM 6.3+ is supported. ## Helm Installation diff --git a/bin/democratic-csi b/bin/democratic-csi index 2a2148e..52e0a34 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -1,10 +1,18 @@ -#!/usr/bin/env -S node --expose-gc ${NODE_OPTIONS_CSI_1} ${NODE_OPTIONS_CSI_2} ${NODE_OPTIONS_CSI_3} ${NODE_OPTIONS_CSI_4} ${NODE_OPTIONS_CSI_5} +#!/usr/bin/env -S node --expose-gc ${NODE_OPTIONS_CSI_1} ${NODE_OPTIONS_CSI_2} ${NODE_OPTIONS_CSI_3} ${NODE_OPTIONS_CSI_4} +/** + * keep the shebang line length under 128 + * https://github.com/democratic-csi/democratic-csi/issues/171 + */ + +// polyfills +require("../src/utils/polyfills"); const yaml = require("js-yaml"); const fs = require("fs"); const { grpc } = require("../src/utils/grpc"); -const { stringify } = require("../src/utils/general"); +const { stringify, stripWindowsDriveLetter } = require("../src/utils/general"); +let driverConfigFile; let options; const args = require("yargs") .env("DEMOCRATIC_CSI") @@ -14,17 +22,29 @@ const args = require("yargs") describe: "provide a path to driver config file", config: true, configParser: (path) => { - try { - options = JSON.parse(fs.readFileSync(path, "utf-8")); - return true; - } catch (e) {} + // normalize path for host-process containers + // CONTAINER_SANDBOX_MOUNT_POINT C:\C\0eac9a8da76f6d7119c5d9f86c8b3106d67dbbf01dbeb22fdc0192476b7e31cb\ + // path is injected as C:\config\driver-config-file.yaml + if (process.env.CONTAINER_SANDBOX_MOUNT_POINT) { + path = `${ + process.env.CONTAINER_SANDBOX_MOUNT_POINT + }${stripWindowsDriveLetter(path)}`; + } try { options = yaml.load(fs.readFileSync(path, "utf8")); - return true; - } catch (e) {} + try { + driverConfigFile = fs.realpathSync(path); + } catch (e) { + console.log("failed finding config file realpath: " + e.toString()); + driverConfigFile = path; + } - throw new Error("failed parsing config file: " + path); + return true; + } catch (e) { + console.log("failed parsing config file: " + path); + throw e; + } }, }) .demandOption(["driver-config-file"], "driver-config-file is required") @@ -136,6 +156,14 @@ let operationLock = new Set(); async function requestHandlerProxy(call, callback, serviceMethodName) { const cleansedCall = JSON.parse(stringify(call)); + + delete cleansedCall.call; + delete cleansedCall.canceled; + for (const key in cleansedCall) { + if (key.startsWith("_")) { + delete cleansedCall[key]; + } + } for (const key in cleansedCall.request) { if (key.includes("secret")) { cleansedCall.request[key] = "redacted"; @@ -165,6 +193,18 @@ async function requestHandlerProxy(call, callback, serviceMethodName) { }); } + // for testing purposes + //await GeneralUtils.sleep(10000); + //throw new Error("fake error"); + + // for CI/testing purposes + if (["NodePublishVolume", "NodeStageVolume"].includes(serviceMethodName)) { + await driver.setVolumeContextCache( + call.request.volume_id, + call.request.volume_context + ); + } + let response; let responseError; try { @@ -190,12 +230,21 @@ async function requestHandlerProxy(call, callback, serviceMethodName) { throw responseError; } + // for CI/testing purposes + if (serviceMethodName == "CreateVolume") { + await driver.setVolumeContextCache( + response.volume.volume_id, + response.volume.volume_context + ); + } + logger.info( "new response - driver: %s method: %s response: %j", driver.constructor.name, serviceMethodName, response ); + callback(null, response); } catch (e) { let message; @@ -205,7 +254,7 @@ async function requestHandlerProxy(call, callback, serviceMethodName) { message += ` ${e.stack}`; } } else { - message = JSON.stringify(e); + message = stringify(e); } logger.error( @@ -336,9 +385,11 @@ if (args.serverSocket) { } logger.info( - "starting csi server - name: %s, version: %s, driver: %s, mode: %s, csi version: %s, address: %s, socket: %s", - args.csiName, + "starting csi server - node version: %s, package version: %s, config file: %s, csi-name: %s, csi-driver: %s, csi-mode: %s, csi-version: %s, address: %s, socket: %s", + process.version, args.version, + driverConfigFile, + args.csiName, options.driver, args.csiMode.join(","), args.csiVersion, diff --git a/ci/bin/build.ps1 b/ci/bin/build.ps1 new file mode 100644 index 0000000..5b7deb9 --- /dev/null +++ b/ci/bin/build.ps1 @@ -0,0 +1,19 @@ +Write-Output "current user" +whoami +Write-Output "current working directory" +(Get-Location).Path +Write-Output "current PATH" +$Env:PATH + +Write-Output "node version" +node --version +Write-Output "npm version" +npm --version + +# install deps +Write-Output "running npm i" +npm i + +Write-Output "creating tar.gz" +# tar node_modules to keep the number of files low to upload +tar -zcf node_modules-windows-amd64.tar.gz node_modules diff --git a/ci/bin/build.sh b/ci/bin/build.sh index a2d08d3..0e8afbf 100755 --- a/ci/bin/build.sh +++ b/ci/bin/build.sh @@ -12,4 +12,4 @@ npm --version npm i # tar node_modules to keep the number of files low to upload -tar -zcf node_modules.tar.gz node_modules +tar -zcf node_modules-linux-amd64.tar.gz node_modules diff --git a/ci/bin/helper.ps1 b/ci/bin/helper.ps1 new file mode 100644 index 0000000..6e8a357 --- /dev/null +++ b/ci/bin/helper.ps1 @@ -0,0 +1,16 @@ +#Set-StrictMode -Version Latest +#$ErrorActionPreference = "Stop" +#$PSDefaultParameterValues['*:ErrorAction'] = "Stop" +function ThrowOnNativeFailure { + if (-not $?) { + throw 'Native Failure' + } +} + +function psenvsubstr($data) { + foreach($v in Get-ChildItem env:) { + $key = '${' + $v.Name + '}' + $data = $data.Replace($key, $v.Value) + } + return $data +} \ No newline at end of file diff --git a/ci/bin/launch-csi-grpc-proxy.ps1 b/ci/bin/launch-csi-grpc-proxy.ps1 new file mode 100644 index 0000000..8dbf6b7 --- /dev/null +++ b/ci/bin/launch-csi-grpc-proxy.ps1 @@ -0,0 +1,15 @@ +if (! $PSScriptRoot) { + $PSScriptRoot = $args[0] +} + +. "${PSScriptRoot}\helper.ps1" + +Set-Location $env:PWD + +Write-Output "launching csi-grpc-proxy" + +$env:PROXY_TO = "npipe://" + $env:NPIPE_ENDPOINT +$env:BIND_TO = "unix://" + $env:CSI_ENDPOINT + +# https://stackoverflow.com/questions/2095088/error-when-calling-3rd-party-executable-from-powershell-when-using-an-ide +csi-grpc-proxy.exe 2>&1 | % { "$_" } diff --git a/ci/bin/launch-csi-sanity.ps1 b/ci/bin/launch-csi-sanity.ps1 new file mode 100644 index 0000000..ba99971 --- /dev/null +++ b/ci/bin/launch-csi-sanity.ps1 @@ -0,0 +1,69 @@ +if (! $PSScriptRoot) { + $PSScriptRoot = $args[0] +} + +. "${PSScriptRoot}\helper.ps1" + +Set-Location $env:PWD + +$exit_code = 0 +$tmpdir = New-Item -ItemType Directory -Path ([System.IO.Path]::GetTempPath()) -Name ([System.IO.Path]::GetRandomFileName()) +$env:CSI_SANITY_TEMP_DIR = $tmpdir.FullName + +# cleanse endpoint to something csi-sanity plays nicely with +$endpoint = ${env:CSI_ENDPOINT} +$endpoint = $endpoint.replace("C:\", "/") +$endpoint = $endpoint.replace("\", "/") + +if (! $env:CSI_SANITY_FAILFAST) { + $env:CSI_SANITY_FAILFAST = "false" +} + +$failfast = "" + +if ($env:CSI_SANITY_FAILFAST -eq "true") { + $failfast = "-ginkgo.failFast" +} + +Write-Output "launching csi-sanity" +Write-Output "connecting to: ${endpoint}" +Write-Output "failfast: ${env:CSI_SANITY_FAILFAST}" +Write-Output "skip: ${env:CSI_SANITY_SKIP}" +Write-Output "focus: ${env:CSI_SANITY_FOCUS}" + +$skip = '"' + ${env:CSI_SANITY_SKIP} + '"' +$focus = '"' + ${env:CSI_SANITY_FOCUS} + '"' + +csi-sanity.exe -"csi.endpoint" "unix://${endpoint}" ` + $failfast ` + -"csi.mountdir" "${env:CSI_SANITY_TEMP_DIR}\mnt" ` + -"csi.stagingdir" "${env:CSI_SANITY_TEMP_DIR}\stage" ` + -"csi.testvolumeexpandsize" 2147483648 ` + -"csi.testvolumesize" 1073741824 ` + -"ginkgo.skip" $skip ` + -"ginkgo.focus" $focus + +# does not work the same as linux for some reason +# -"ginkgo.skip" "'" + ${env:CSI_SANITY_SKIP} + "'" ` + +if (-not $?) { + $exit_code = $LASTEXITCODE + Write-Output "csi-sanity exit code: ${exit_code}" + if ($exit_code -gt 0) { + $exit_code = 1 + } +} + +# remove tmp dir +Remove-Item -Path "$env:CSI_SANITY_TEMP_DIR" -Force -Recurse + +#Exit $exit_code +Write-Output "exiting with exit code: ${exit_code}" + +if ($exit_code -gt 0) { + throw "csi-sanity failed" +} + +# these do not work for whatever reason +#Exit $exit_code +#[System.Environment]::Exit($exit_code) diff --git a/ci/bin/launch-server.ps1 b/ci/bin/launch-server.ps1 new file mode 100644 index 0000000..3cf17a7 --- /dev/null +++ b/ci/bin/launch-server.ps1 @@ -0,0 +1,29 @@ +if (! $PSScriptRoot) { + $PSScriptRoot = $args[0] +} + +. "${PSScriptRoot}\helper.ps1" + +Set-Location $env:PWD +Write-Output "launching server" + +$env:LOG_LEVEL = "debug" +$env:CSI_VERSION = "1.5.0" +$env:CSI_NAME = "driver-test" +$env:CSI_SANITY = "1" + +if (! ${env:CONFIG_FILE}) { + $env:CONFIG_FILE = $env:TEMP + "\csi-config-" + $env:CI_BUILD_KEY + ".yaml" + if ($env:TEMPLATE_CONFIG_FILE) { + $config_data = Get-Content "${env:TEMPLATE_CONFIG_FILE}" -Raw + $config_data = psenvsubstr($config_data) + $config_data | Set-Content "${env:CONFIG_FILE}" + } +} + +node "${PSScriptRoot}\..\..\bin\democratic-csi" ` + --log-level "$env:LOG_LEVEL" ` + --driver-config-file "$env:CONFIG_FILE" ` + --csi-version "$env:CSI_VERSION" ` + --csi-name "$env:CSI_NAME" ` + --server-socket "${env:NPIPE_ENDPOINT}" 2>&1 | % { "$_" } diff --git a/ci/bin/launch-server.sh b/ci/bin/launch-server.sh index 93274a5..5d3b387 100755 --- a/ci/bin/launch-server.sh +++ b/ci/bin/launch-server.sh @@ -3,6 +3,9 @@ set -e set -x +export PATH="/usr/local/lib/nodejs/bin:${PATH}" +echo "current launch-server PATH: ${PATH}" + : ${CI_BUILD_KEY:="local"} : ${TEMPLATE_CONFIG_FILE:=${1}} : ${CSI_MODE:=""} diff --git a/ci/bin/run.ps1 b/ci/bin/run.ps1 new file mode 100644 index 0000000..e3d30aa --- /dev/null +++ b/ci/bin/run.ps1 @@ -0,0 +1,133 @@ +# https://stackoverflow.com/questions/2095088/error-when-calling-3rd-party-executable-from-powershell-when-using-an-ide +# +# Examples: +# +# $mypath = $MyInvocation.MyCommand.Path +# Get-ChildItem env:\ +# Get-Job | Where-Object -Property State -eq “Running” +# Get-Location (like pwd) +# if ($null -eq $env:FOO) { $env:FOO = 'bar' } + +. "${PSScriptRoot}\helper.ps1" + +#Set-PSDebug -Trace 2 + +Write-Output "current user" +whoami +Write-Output "current working directory" +(Get-Location).Path +Write-Output "current PATH" +$Env:PATH + +function Job-Cleanup() { + Get-Job | Stop-Job + Get-Job | Remove-Job +} + +# start clean +Job-Cleanup + +# install from artifacts +if ((Test-Path "node_modules-windows-amd64.tar.gz") -and !(Test-Path "node_modules")) { + Write-Output "extracting node_modules-windows-amd64.tar.gz" + tar -zxf node_modules-windows-amd64.tar.gz +} + +# setup env +$env:PWD = (Get-Location).Path +$env:CI_BUILD_KEY = ([guid]::NewGuid() -Split "-")[0] +$env:CSI_ENDPOINT = $env:TEMP + "\csi-sanity-" + $env:CI_BUILD_KEY + ".sock" +$env:NPIPE_ENDPOINT = "//./pipe/csi-sanity-" + $env:CI_BUILD_KEY + "csi.sock" + +# testing values +if (Test-Path "${PSScriptRoot}\run-dev.ps1") { + . "${PSScriptRoot}\run-dev.ps1" +} + +# launch server +$server_job = Start-Job -FilePath .\ci\bin\launch-server.ps1 -InitializationScript {} -ArgumentList $PSScriptRoot + +# launch csi-grpc-proxy +$csi_grpc_proxy_job = Start-Job -FilePath .\ci\bin\launch-csi-grpc-proxy.ps1 -InitializationScript {} -ArgumentList $PSScriptRoot + +# wait for socket to appear +$iter = 0 +$max_iter = 60 +$started = 1 +while (!(Test-Path "${env:CSI_ENDPOINT}")) { + $iter++ + Write-Output "Waiting for ${env:CSI_ENDPOINT} to appear" + Start-Sleep 1 + try { + Get-Job | Receive-Job + } catch {} + if ($iter -gt $max_iter) { + Write-Output "${env:CSI_ENDPOINT} failed to appear" + $started = 0 + break + } +} + +# launch csi-sanity +if ($started -eq 1) { + $csi_sanity_job = Start-Job -FilePath .\ci\bin\launch-csi-sanity.ps1 -InitializationScript {} -ArgumentList $PSScriptRoot +} + +# https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-job?view=powershell-7.2 +# -ChildJobState +$iter = 0 +while ($csi_sanity_job -and ($csi_sanity_job.State -eq "Running" -or $csi_sanity_job.State -eq "NotStarted")) { + $iter++ + foreach ($job in Get-Job) { + if (($job -eq $csi_grpc_proxy_job) -and ($iter -gt 20)) { + continue + } + if (!$job.HasMoreData) { + continue + } + try { + $job | Receive-Job + } + catch { + if ($job.State -ne "Failed") { + Write-Output "failure receiving job data: ${_}" + # just swallow the errors as it seems there are various reasons errors + # may show up (perhaps no data currently, etc) + #$job | fl + #throw $_ + } + } + } +} + +# spew any remaining job output to the console +foreach ($job in Get-Job) { + if ($job -eq $csi_grpc_proxy_job) { + continue + } + try { + $job | Receive-Job + } + catch {} +} + +# wait for good measure +if ($csi_sanity_job) { + Wait-Job -Job $csi_sanity_job +} + +#Get-Job | fl + +$exit_code = 0 + +if (! $csi_sanity_job) { + $exit_code = 1 +} + +if ($csi_sanity_job -and $csi_sanity_job.State -eq "Failed") { + $exit_code = 1 +} + +# cleanup after ourselves +Job-Cleanup +Exit $exit_code diff --git a/ci/bin/run.sh b/ci/bin/run.sh index 92b18d6..1d4eaf8 100755 --- a/ci/bin/run.sh +++ b/ci/bin/run.sh @@ -15,8 +15,8 @@ export PATH="/usr/local/lib/nodejs/bin:${PATH}" # install deps #npm i # install from artifacts -if [[ -f "node_modules.tar.gz" ]];then - tar -zxf node_modules.tar.gz +if [[ -f "node_modules-linux-amd64.tar.gz" && ! -d "node_modules" ]];then + tar -zxf node_modules-linux-amd64.tar.gz fi # generate key for paths etc diff --git a/ci/configs/client/nfs.yaml b/ci/configs/client/nfs.yaml new file mode 100644 index 0000000..b277805 --- /dev/null +++ b/ci/configs/client/nfs.yaml @@ -0,0 +1,10 @@ +driver: nfs-client +instance_id: +nfs: + shareHost: ${SERVER_HOST} + shareBasePath: "/mnt/tank/client/nfs/${CI_BUILD_KEY}" + # shareHost:shareBasePath should be mounted at this location in the controller container + controllerBasePath: "/mnt/client/nfs/${CI_BUILD_KEY}" + dirPermissionsMode: "0777" + dirPermissionsUser: 0 + dirPermissionsGroup: 0 diff --git a/ci/configs/client/smb.yaml b/ci/configs/client/smb.yaml new file mode 100644 index 0000000..485a782 --- /dev/null +++ b/ci/configs/client/smb.yaml @@ -0,0 +1,14 @@ +driver: smb-client +instance_id: +smb: + shareHost: ${SERVER_HOST} + shareBasePath: "${SHARE_NAME}/${CI_BUILD_KEY}" + # shareHost:shareBasePath should be mounted at this location in the controller container + controllerBasePath: "/mnt/client/smb/${CI_BUILD_KEY}" + dirPermissionsMode: "0777" + dirPermissionsUser: 0 + dirPermissionsGroup: 0 + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" diff --git a/ci/configs/synlogy/iscsi.yaml b/ci/configs/synlogy/dsm6/iscsi.yaml similarity index 96% rename from ci/configs/synlogy/iscsi.yaml rename to ci/configs/synlogy/dsm6/iscsi.yaml index 8b58913..7c0ca87 100644 --- a/ci/configs/synlogy/iscsi.yaml +++ b/ci/configs/synlogy/dsm6/iscsi.yaml @@ -16,8 +16,8 @@ iscsi: targetPortal: ${SYNOLOGY_HOST} targetPortals: [] baseiqn: "iqn.2000-01.com.synology:XpenoDsm62x." - namePrefix: "csi-${CI_BUILD_KEY}-" - nameSuffix: "-ci" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" + nameSuffix: "" lunTemplate: # btrfs thin provisioning diff --git a/ci/configs/synlogy/dsm7/iscsi.yaml b/ci/configs/synlogy/dsm7/iscsi.yaml new file mode 100644 index 0000000..7c0ca87 --- /dev/null +++ b/ci/configs/synlogy/dsm7/iscsi.yaml @@ -0,0 +1,77 @@ +driver: synology-iscsi +httpConnection: + protocol: http + host: ${SYNOLOGY_HOST} + port: ${SYNOLOGY_PORT} + username: ${SYNOLOGY_USERNAME} + password: ${SYNOLOGY_PASSWORD} + allowInsecure: true + session: "democratic-csi-${CI_BUILD_KEY}" + serialize: true + +synology: + volume: ${SYNOLOGY_VOLUME} + +iscsi: + targetPortal: ${SYNOLOGY_HOST} + targetPortals: [] + baseiqn: "iqn.2000-01.com.synology:XpenoDsm62x." + namePrefix: "csi-ci-${CI_BUILD_KEY}-" + nameSuffix: "" + + lunTemplate: + # btrfs thin provisioning + type: "BLUN" + # tpws = Hardware-assisted zeroing + # caw = Hardware-assisted locking + # 3pc = Hardware-assisted data transfer + # tpu = Space reclamation + # can_snapshot = Snapshot + #dev_attribs: + #- dev_attrib: emulate_tpws + # enable: 1 + #- dev_attrib: emulate_caw + # enable: 1 + #- dev_attrib: emulate_3pc + # enable: 1 + #- dev_attrib: emulate_tpu + # enable: 0 + #- dev_attrib: can_snapshot + # enable: 1 + + # btfs thick provisioning + # only zeroing and locking supported + #type: "BLUN_THICK" + # tpws = Hardware-assisted zeroing + # caw = Hardware-assisted locking + #dev_attribs: + #- dev_attrib: emulate_tpws + # enable: 1 + #- dev_attrib: emulate_caw + # enable: 1 + + # ext4 thinn provisioning UI sends everything with enabled=0 + #type: "THIN" + + # ext4 thin with advanced legacy features set + # can only alter tpu (all others are set as enabled=1) + #type: "ADV" + #dev_attribs: + #- dev_attrib: emulate_tpu + # enable: 1 + + # ext4 thick + # can only alter caw + #type: "FILE" + #dev_attribs: + #- dev_attrib: emulate_caw + # enable: 1 + + lunSnapshotTemplate: + is_locked: true + # https://kb.synology.com/en-me/DSM/tutorial/What_is_file_system_consistent_snapshot + is_app_consistent: true + + targetTemplate: + auth_type: 0 + max_sessions: 0 diff --git a/ci/configs/truenas/core/12.0/core-iscsi.yaml b/ci/configs/truenas/core/12.0/core-iscsi.yaml index 2cf1d84..aa32a78 100644 --- a/ci/configs/truenas/core/12.0/core-iscsi.yaml +++ b/ci/configs/truenas/core/12.0/core-iscsi.yaml @@ -26,7 +26,7 @@ zfs: iscsi: targetPortal: ${TRUENAS_HOST} interface: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" targetGroups: - targetGroupPortalGroup: 1 @@ -35,3 +35,10 @@ iscsi: targetGroupAuthGroup: # 0-100 (0 == ignore) extentAvailThreshold: 0 + +# overcome the 63 char limit for testing purposes only +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/ci/configs/truenas/core/12.0/core-nfs.yaml b/ci/configs/truenas/core/12.0/core-nfs.yaml index 04f2744..0205cb0 100644 --- a/ci/configs/truenas/core/12.0/core-nfs.yaml +++ b/ci/configs/truenas/core/12.0/core-nfs.yaml @@ -19,7 +19,7 @@ zfs: detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true + datasetEnableReservation: false datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 diff --git a/ci/configs/truenas/core/12.0/core-smb.yaml b/ci/configs/truenas/core/12.0/core-smb.yaml index 9460255..fe6856e 100644 --- a/ci/configs/truenas/core/12.0/core-smb.yaml +++ b/ci/configs/truenas/core/12.0/core-smb.yaml @@ -17,33 +17,29 @@ sshConnection: zfs: datasetProperties: # smb options - #aclmode: restricted - #casesensitivity: mixed + aclmode: restricted + aclinherit: passthrough + acltype: nfsv4 + casesensitivity: insensitive datasetParentName: tank/ci/${CI_BUILD_KEY}/v detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true - datasetPermissionsMode: "0777" - datasetPermissionsUser: 0 - datasetPermissionsGroup: 0 + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: 1001 + datasetPermissionsGroup: 1001 - # for smb with guest - #datasetPermissionsUser: nobody - #datasetPermissionsGroup: nobody - - #datasetPermissionsGroup: root - #datasetPermissionsAcls: - #- "-m everyone@:full_set:allow" - - #datasetPermissionsAcls: - #- "-m u:kube:full_set:allow" + datasetPermissionsAcls: + - "-m g:builtin_users:full_set:fd:allow" + - "-m group@:modify_set:fd:allow" + - "-m owner@:full_set:fd:allow" smb: shareHost: ${TRUENAS_HOST} #nameTemplate: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" shareAuxiliaryConfigurationTemplate: | #guest ok = yes @@ -52,11 +48,21 @@ smb: shareAllowedHosts: [] shareDeniedHosts: [] #shareDefaultPermissions: true - shareGuestOk: true + shareGuestOk: false #shareGuestOnly: true #shareShowHiddenFiles: true - shareRecycleBin: true + shareRecycleBin: false shareBrowsable: false shareAccessBasedEnumeration: true shareTimeMachine: false #shareStorageTask: + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" + +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/ci/configs/truenas/core/13.0/core-iscsi.yaml b/ci/configs/truenas/core/13.0/core-iscsi.yaml index 2cf1d84..3a8619b 100644 --- a/ci/configs/truenas/core/13.0/core-iscsi.yaml +++ b/ci/configs/truenas/core/13.0/core-iscsi.yaml @@ -26,7 +26,7 @@ zfs: iscsi: targetPortal: ${TRUENAS_HOST} interface: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" targetGroups: - targetGroupPortalGroup: 1 diff --git a/ci/configs/truenas/core/13.0/core-nfs.yaml b/ci/configs/truenas/core/13.0/core-nfs.yaml index 04f2744..0205cb0 100644 --- a/ci/configs/truenas/core/13.0/core-nfs.yaml +++ b/ci/configs/truenas/core/13.0/core-nfs.yaml @@ -19,7 +19,7 @@ zfs: detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true + datasetEnableReservation: false datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 diff --git a/ci/configs/truenas/core/13.0/core-smb.yaml b/ci/configs/truenas/core/13.0/core-smb.yaml index 9460255..fe6856e 100644 --- a/ci/configs/truenas/core/13.0/core-smb.yaml +++ b/ci/configs/truenas/core/13.0/core-smb.yaml @@ -17,33 +17,29 @@ sshConnection: zfs: datasetProperties: # smb options - #aclmode: restricted - #casesensitivity: mixed + aclmode: restricted + aclinherit: passthrough + acltype: nfsv4 + casesensitivity: insensitive datasetParentName: tank/ci/${CI_BUILD_KEY}/v detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true - datasetPermissionsMode: "0777" - datasetPermissionsUser: 0 - datasetPermissionsGroup: 0 + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: 1001 + datasetPermissionsGroup: 1001 - # for smb with guest - #datasetPermissionsUser: nobody - #datasetPermissionsGroup: nobody - - #datasetPermissionsGroup: root - #datasetPermissionsAcls: - #- "-m everyone@:full_set:allow" - - #datasetPermissionsAcls: - #- "-m u:kube:full_set:allow" + datasetPermissionsAcls: + - "-m g:builtin_users:full_set:fd:allow" + - "-m group@:modify_set:fd:allow" + - "-m owner@:full_set:fd:allow" smb: shareHost: ${TRUENAS_HOST} #nameTemplate: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" shareAuxiliaryConfigurationTemplate: | #guest ok = yes @@ -52,11 +48,21 @@ smb: shareAllowedHosts: [] shareDeniedHosts: [] #shareDefaultPermissions: true - shareGuestOk: true + shareGuestOk: false #shareGuestOnly: true #shareShowHiddenFiles: true - shareRecycleBin: true + shareRecycleBin: false shareBrowsable: false shareAccessBasedEnumeration: true shareTimeMachine: false #shareStorageTask: + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" + +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/ci/configs/truenas/scale/22.02/scale-iscsi.yaml b/ci/configs/truenas/scale/22.02/scale-iscsi.yaml index 05d0359..b6b6f43 100644 --- a/ci/configs/truenas/scale/22.02/scale-iscsi.yaml +++ b/ci/configs/truenas/scale/22.02/scale-iscsi.yaml @@ -20,7 +20,7 @@ zfs: iscsi: targetPortal: ${TRUENAS_HOST} interface: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" targetGroups: - targetGroupPortalGroup: 1 diff --git a/ci/configs/truenas/scale/22.02/scale-nfs.yaml b/ci/configs/truenas/scale/22.02/scale-nfs.yaml index 0e817ce..42818ae 100644 --- a/ci/configs/truenas/scale/22.02/scale-nfs.yaml +++ b/ci/configs/truenas/scale/22.02/scale-nfs.yaml @@ -13,7 +13,7 @@ zfs: detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true + datasetEnableReservation: false datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 diff --git a/ci/configs/truenas/scale/22.02/scale-smb.yaml b/ci/configs/truenas/scale/22.02/scale-smb.yaml index 74964ea..2a8861e 100644 --- a/ci/configs/truenas/scale/22.02/scale-smb.yaml +++ b/ci/configs/truenas/scale/22.02/scale-smb.yaml @@ -9,35 +9,19 @@ httpConnection: password: ${TRUENAS_PASSWORD} zfs: - datasetProperties: - # smb options - #aclmode: restricted - #casesensitivity: mixed - datasetParentName: tank/ci/${CI_BUILD_KEY}/v detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true - datasetPermissionsMode: "0777" - datasetPermissionsUser: 0 - datasetPermissionsGroup: 0 + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: 1001 + datasetPermissionsGroup: 1001 - # for smb with guest - #datasetPermissionsUser: nobody - #datasetPermissionsGroup: nobody - - #datasetPermissionsGroup: root - #datasetPermissionsAcls: - #- "-m everyone@:full_set:allow" - - #datasetPermissionsAcls: - #- "-m u:kube:full_set:allow" - smb: shareHost: ${TRUENAS_HOST} #nameTemplate: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" shareAuxiliaryConfigurationTemplate: | #guest ok = yes @@ -46,11 +30,21 @@ smb: shareAllowedHosts: [] shareDeniedHosts: [] #shareDefaultPermissions: true - shareGuestOk: true + shareGuestOk: false #shareGuestOnly: true #shareShowHiddenFiles: true - shareRecycleBin: true + shareRecycleBin: false shareBrowsable: false shareAccessBasedEnumeration: true shareTimeMachine: false #shareStorageTask: + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" + +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/ci/configs/windows/iscsi.yaml b/ci/configs/windows/iscsi.yaml new file mode 100644 index 0000000..38dc594 --- /dev/null +++ b/ci/configs/windows/iscsi.yaml @@ -0,0 +1,31 @@ +driver: zfs-generic-iscsi + +sshConnection: + host: ${SERVER_HOST} + port: 22 + username: ${SERVER_USERNAME} + password: ${SERVER_PASSWORD} + +zfs: + datasetParentName: tank/ci/${CI_BUILD_KEY}/v + detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s + + zvolCompression: + zvolDedup: + zvolEnableReservation: false + zvolBlocksize: + +iscsi: + targetPortal: ${SERVER_HOST} + interface: "" + namePrefix: "csi-ci-${CI_BUILD_KEY}" + nameSuffix: "" + shareStrategy: "targetCli" + shareStrategyTargetCli: + basename: "iqn.2003-01.org.linux-iscsi.ubuntu-19.x8664" + tpg: + attributes: + authentication: 0 + generate_node_acls: 1 + cache_dynamic_acls: 1 + demo_mode_write_protect: 0 diff --git a/ci/configs/windows/smb.yaml b/ci/configs/windows/smb.yaml new file mode 100644 index 0000000..272a590 --- /dev/null +++ b/ci/configs/windows/smb.yaml @@ -0,0 +1,40 @@ +driver: zfs-generic-smb + +sshConnection: + host: ${SERVER_HOST} + port: 22 + username: ${SERVER_USERNAME} + password: ${SERVER_PASSWORD} + +zfs: + datasetParentName: tank/ci/${CI_BUILD_KEY}/v + detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s + + datasetProperties: + #aclmode: restricted + #aclinherit: passthrough + #acltype: nfsv4 + casesensitivity: insensitive + + datasetEnableQuotas: true + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: smbroot + datasetPermissionsGroup: smbroot + +smb: + shareHost: ${SERVER_HOST} + shareStrategy: "setDatasetProperties" + shareStrategySetDatasetProperties: + properties: + sharesmb: "on" + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" + +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/ci/configs/zfs-generic/iscsi.yaml b/ci/configs/zfs-generic/iscsi.yaml index 38dc594..e5bc3e2 100644 --- a/ci/configs/zfs-generic/iscsi.yaml +++ b/ci/configs/zfs-generic/iscsi.yaml @@ -18,7 +18,7 @@ zfs: iscsi: targetPortal: ${SERVER_HOST} interface: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" shareStrategy: "targetCli" shareStrategyTargetCli: diff --git a/ci/configs/zfs-generic/nfs.yaml b/ci/configs/zfs-generic/nfs.yaml index a3a8b88..46acf25 100644 --- a/ci/configs/zfs-generic/nfs.yaml +++ b/ci/configs/zfs-generic/nfs.yaml @@ -11,7 +11,7 @@ zfs: detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true + datasetEnableReservation: false datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 @@ -21,4 +21,5 @@ nfs: shareStrategy: "setDatasetProperties" shareStrategySetDatasetProperties: properties: - sharenfs: "on" + #sharenfs: "on" + sharenfs: "rw,no_subtree_check,no_root_squash" diff --git a/ci/configs/zfs-generic/smb.yaml b/ci/configs/zfs-generic/smb.yaml new file mode 100644 index 0000000..272a590 --- /dev/null +++ b/ci/configs/zfs-generic/smb.yaml @@ -0,0 +1,40 @@ +driver: zfs-generic-smb + +sshConnection: + host: ${SERVER_HOST} + port: 22 + username: ${SERVER_USERNAME} + password: ${SERVER_PASSWORD} + +zfs: + datasetParentName: tank/ci/${CI_BUILD_KEY}/v + detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s + + datasetProperties: + #aclmode: restricted + #aclinherit: passthrough + #acltype: nfsv4 + casesensitivity: insensitive + + datasetEnableQuotas: true + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: smbroot + datasetPermissionsGroup: smbroot + +smb: + shareHost: ${SERVER_HOST} + shareStrategy: "setDatasetProperties" + shareStrategySetDatasetProperties: + properties: + sharesmb: "on" + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" + +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/csi_proxy_proto/disk/v1/api.proto b/csi_proxy_proto/disk/v1/api.proto new file mode 100644 index 0000000..f73f412 --- /dev/null +++ b/csi_proxy_proto/disk/v1/api.proto @@ -0,0 +1,111 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/disk/v1"; + +service Disk { + // ListDiskLocations returns locations of all + // disk devices enumerated by the host. + rpc ListDiskLocations(ListDiskLocationsRequest) returns (ListDiskLocationsResponse) {} + + // PartitionDisk initializes and partitions a disk device with the GPT partition style + // (if the disk has not been partitioned already) and returns the resulting volume device ID. + rpc PartitionDisk(PartitionDiskRequest) returns (PartitionDiskResponse) {} + + // Rescan refreshes the host's storage cache. + rpc Rescan(RescanRequest) returns (RescanResponse) {} + + // ListDiskIDs returns a map of DiskID objects where the key is the disk number. + rpc ListDiskIDs(ListDiskIDsRequest) returns (ListDiskIDsResponse) {} + + // GetDiskStats returns the stats of a disk (currently it returns the disk size). + rpc GetDiskStats(GetDiskStatsRequest) returns (GetDiskStatsResponse) {} + + // SetDiskState sets the offline/online state of a disk. + rpc SetDiskState(SetDiskStateRequest) returns (SetDiskStateResponse) {} + + // GetDiskState gets the offline/online state of a disk. + rpc GetDiskState(GetDiskStateRequest) returns (GetDiskStateResponse) {} +} + +message ListDiskLocationsRequest { + // Intentionally empty. +} + +message DiskLocation { + string Adapter = 1; + string Bus = 2; + string Target = 3; + string LUNID = 4; +} + +message ListDiskLocationsResponse { + // Map of disk number and associated with each disk device. + map disk_locations = 1; +} + +message PartitionDiskRequest { + // Disk device number of the disk to partition. + uint32 disk_number = 1; +} + +message PartitionDiskResponse { + // Intentionally empty. +} + +message RescanRequest { + // Intentionally empty. +} + +message RescanResponse { + // Intentionally empty. +} + +message ListDiskIDsRequest { + // Intentionally empty. +} + +message DiskIDs { + // The disk page83 id. + string page83 = 1; + // The disk serial number. + string serial_number = 2; +} + +message ListDiskIDsResponse { + // Map of disk numbers and disk identifiers associated with each disk device. + map diskIDs = 1; // the case is intentional for protoc to generate the field as DiskIDs +} + +message GetDiskStatsRequest { + // Disk device number of the disk to get the stats from. + uint32 disk_number = 1; +} + +message GetDiskStatsResponse { + // Total size of the volume. + int64 total_bytes = 1; +} + +message SetDiskStateRequest { + // Disk device number of the disk. + uint32 disk_number = 1; + + // Online state to set for the disk. true for online, false for offline. + bool is_online = 2; +} + +message SetDiskStateResponse { + // Intentionally empty. +} + +message GetDiskStateRequest { + // Disk device number of the disk. + uint32 disk_number = 1; +} + +message GetDiskStateResponse { + // Online state of the disk. true for online, false for offline. + bool is_online = 1; +} diff --git a/csi_proxy_proto/disk/v1alpha1/api.proto b/csi_proxy_proto/disk/v1alpha1/api.proto new file mode 100644 index 0000000..03ef612 --- /dev/null +++ b/csi_proxy_proto/disk/v1alpha1/api.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +package v1alpha1; + +service Disk { + // ListDiskLocations returns locations of all + // disk devices enumerated by the host + rpc ListDiskLocations(ListDiskLocationsRequest) returns (ListDiskLocationsResponse) {} + + // PartitionDisk initializes and partitions a disk device (if the disk has not + // been partitioned already) and returns the resulting volume device ID + rpc PartitionDisk(PartitionDiskRequest) returns (PartitionDiskResponse) {} + + // Rescan refreshes the host's storage cache + rpc Rescan(RescanRequest) returns (RescanResponse) {} + + // GetDiskNumberByName returns disk number based on the passing disk name information + rpc GetDiskNumberByName(GetDiskNumberByNameRequest) returns (GetDiskNumberByNameResponse) {} +} + +message ListDiskLocationsRequest { + // Intentionally empty +} + +message DiskLocation { + string Adapter = 1; + string Bus = 2; + string Target = 3; + string LUNID = 4; +} + +message ListDiskLocationsResponse { + // Map of disk device IDs and associated with each disk device + map disk_locations = 1; +} + +message PartitionDiskRequest { + // Disk device ID of the disk to partition + string diskID = 1; +} + +message PartitionDiskResponse { + // Intentionally empty +} + +message RescanRequest { + // Intentionally empty +} + +message RescanResponse { + // Intentionally empty +} + +message GetDiskNumberByNameRequest { + // Disk ID + string disk_name = 1; +} + +message GetDiskNumberByNameResponse { + // Disk number + string disk_number = 1; +} diff --git a/csi_proxy_proto/disk/v1beta1/api.proto b/csi_proxy_proto/disk/v1beta1/api.proto new file mode 100644 index 0000000..4673b2c --- /dev/null +++ b/csi_proxy_proto/disk/v1beta1/api.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +package v1beta1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/disk/v1beta1"; + +service Disk { + // ListDiskLocations returns locations of all + // disk devices enumerated by the host + rpc ListDiskLocations(ListDiskLocationsRequest) returns (ListDiskLocationsResponse) {} + + // PartitionDisk initializes and partitions a disk device (if the disk has not + // been partitioned already) and returns the resulting volume device ID + rpc PartitionDisk(PartitionDiskRequest) returns (PartitionDiskResponse) {} + + // Rescan refreshes the host's storage cache + rpc Rescan(RescanRequest) returns (RescanResponse) {} + + // ListDiskIDs returns a map of DiskID objects where the key is the disk number + rpc ListDiskIDs(ListDiskIDsRequest) returns (ListDiskIDsResponse) {} + + // DiskStats returns the stats for the disk + rpc DiskStats(DiskStatsRequest) returns (DiskStatsResponse) {} +} + +message ListDiskLocationsRequest { + // Intentionally empty +} + +message DiskLocation { + string Adapter = 1; + string Bus = 2; + string Target = 3; + string LUNID = 4; +} + +message ListDiskLocationsResponse { + // Map of disk device IDs and associated with each disk device + map disk_locations = 1; +} + +message PartitionDiskRequest { + // Disk device ID of the disk to partition + string diskID = 1; +} + +message PartitionDiskResponse { + // Intentionally empty +} + +message RescanRequest { + // Intentionally empty +} + +message RescanResponse { + // Intentionally empty +} + +message ListDiskIDsRequest { + // Intentionally empty +} + +message DiskIDs { + // Map of Disk ID types and Disk ID values + map identifiers = 1; +} + +message ListDiskIDsResponse { + // Map of disk device numbers and IDs associated with each disk device + map diskIDs = 1; +} + +message DiskStatsRequest { + // Disk device ID of the disk to get the size from + string diskID = 1; +} + +message DiskStatsResponse { + //Total size of the volume + int64 diskSize = 1; +} diff --git a/csi_proxy_proto/disk/v1beta2/api.proto b/csi_proxy_proto/disk/v1beta2/api.proto new file mode 100644 index 0000000..c9f6c8f --- /dev/null +++ b/csi_proxy_proto/disk/v1beta2/api.proto @@ -0,0 +1,109 @@ +syntax = "proto3"; + +package v1beta2; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/disk/v1beta2"; + +service Disk { + // ListDiskLocations returns locations of all + // disk devices enumerated by the host + rpc ListDiskLocations(ListDiskLocationsRequest) returns (ListDiskLocationsResponse) {} + + // PartitionDisk initializes and partitions a disk device (if the disk has not + // been partitioned already) and returns the resulting volume device ID + rpc PartitionDisk(PartitionDiskRequest) returns (PartitionDiskResponse) {} + + // Rescan refreshes the host's storage cache + rpc Rescan(RescanRequest) returns (RescanResponse) {} + + // ListDiskIDs returns a map of DiskID objects where the key is the disk number + rpc ListDiskIDs(ListDiskIDsRequest) returns (ListDiskIDsResponse) {} + + // DiskStats returns the stats for the disk + rpc DiskStats(DiskStatsRequest) returns (DiskStatsResponse) {} + + // SetAttachState sets the offline/online state of a disk + rpc SetAttachState(SetAttachStateRequest) returns (SetAttachStateResponse) {} + + // GetAttachState gets the offline/online state of a disk + rpc GetAttachState(GetAttachStateRequest) returns (GetAttachStateResponse) {} +} + +message ListDiskLocationsRequest { + // Intentionally empty +} + +message DiskLocation { + string Adapter = 1; + string Bus = 2; + string Target = 3; + string LUNID = 4; +} + +message ListDiskLocationsResponse { + // Map of disk device IDs and associated with each disk device + map disk_locations = 1; +} + +message PartitionDiskRequest { + // Disk device ID of the disk to partition + string diskID = 1; +} + +message PartitionDiskResponse { + // Intentionally empty +} + +message RescanRequest { + // Intentionally empty +} + +message RescanResponse { + // Intentionally empty +} + +message ListDiskIDsRequest { + // Intentionally empty +} + +message DiskIDs { + // Map of Disk ID types and Disk ID values + map identifiers = 1; +} + +message ListDiskIDsResponse { + // Map of disk device numbers and IDs associated with each disk device + map diskIDs = 1; +} + +message DiskStatsRequest { + // Disk device ID of the disk to get the size from + string diskID = 1; +} + +message DiskStatsResponse { + //Total size of the volume + int64 diskSize = 1; +} + +message SetAttachStateRequest { + // Disk device ID (number) of the disk which state will change + string diskID = 1; + + // Online state to set for the disk. true for online, false for offline + bool isOnline = 2; +} + +message SetAttachStateResponse { +} + +message GetAttachStateRequest { + // Disk device ID (number) of the disk + string diskID = 1; +} + +message GetAttachStateResponse { + // Online state of the disk. true for online, false for offline + bool isOnline = 1; +} + diff --git a/csi_proxy_proto/disk/v1beta3/api.proto b/csi_proxy_proto/disk/v1beta3/api.proto new file mode 100644 index 0000000..8f8283e --- /dev/null +++ b/csi_proxy_proto/disk/v1beta3/api.proto @@ -0,0 +1,111 @@ +syntax = "proto3"; + +package v1beta3; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/disk/v1beta3"; + +service Disk { + // ListDiskLocations returns locations of all + // disk devices enumerated by the host. + rpc ListDiskLocations(ListDiskLocationsRequest) returns (ListDiskLocationsResponse) {} + + // PartitionDisk initializes and partitions a disk device with the GPT partition style + // (if the disk has not been partitioned already) and returns the resulting volume device ID. + rpc PartitionDisk(PartitionDiskRequest) returns (PartitionDiskResponse) {} + + // Rescan refreshes the host's storage cache. + rpc Rescan(RescanRequest) returns (RescanResponse) {} + + // ListDiskIDs returns a map of DiskID objects where the key is the disk number. + rpc ListDiskIDs(ListDiskIDsRequest) returns (ListDiskIDsResponse) {} + + // GetDiskStats returns the stats of a disk (currently it returns the disk size). + rpc GetDiskStats(GetDiskStatsRequest) returns (GetDiskStatsResponse) {} + + // SetDiskState sets the offline/online state of a disk. + rpc SetDiskState(SetDiskStateRequest) returns (SetDiskStateResponse) {} + + // GetDiskState gets the offline/online state of a disk. + rpc GetDiskState(GetDiskStateRequest) returns (GetDiskStateResponse) {} +} + +message ListDiskLocationsRequest { + // Intentionally empty. +} + +message DiskLocation { + string Adapter = 1; + string Bus = 2; + string Target = 3; + string LUNID = 4; +} + +message ListDiskLocationsResponse { + // Map of disk number and associated with each disk device. + map disk_locations = 1; +} + +message PartitionDiskRequest { + // Disk device number of the disk to partition. + uint32 disk_number = 1; +} + +message PartitionDiskResponse { + // Intentionally empty. +} + +message RescanRequest { + // Intentionally empty. +} + +message RescanResponse { + // Intentionally empty. +} + +message ListDiskIDsRequest { + // Intentionally empty. +} + +message DiskIDs { + // The disk page83 id. + string page83 = 1; + // The disk serial number. + string serial_number = 2; +} + +message ListDiskIDsResponse { + // Map of disk numbers and disk identifiers associated with each disk device. + map diskIDs = 1; // the case is intentional for protoc to generate the field as DiskIDs +} + +message GetDiskStatsRequest { + // Disk device number of the disk to get the stats from. + uint32 disk_number = 1; +} + +message GetDiskStatsResponse { + // Total size of the volume. + int64 total_bytes = 1; +} + +message SetDiskStateRequest { + // Disk device number of the disk. + uint32 disk_number = 1; + + // Online state to set for the disk. true for online, false for offline. + bool is_online = 2; +} + +message SetDiskStateResponse { + // Intentionally empty. +} + +message GetDiskStateRequest { + // Disk device number of the disk. + uint32 disk_number = 1; +} + +message GetDiskStateResponse { + // Online state of the disk. true for online, false for offline. + bool is_online = 1; +} diff --git a/csi_proxy_proto/errors.proto b/csi_proxy_proto/errors.proto new file mode 100644 index 0000000..60c74cd --- /dev/null +++ b/csi_proxy_proto/errors.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package api; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api"; + +// CommandError details errors yielded by cmdlet calls. +message CmdletError { + // Name of the cmdlet that errored out. + string cmdlet_name = 1; + + // Error code that got returned. + uint32 code = 2; + + // Human-readable error message - can be empty. + string message = 3; +} diff --git a/csi_proxy_proto/filesystem/v1/api.proto b/csi_proxy_proto/filesystem/v1/api.proto new file mode 100644 index 0000000..151a1ff --- /dev/null +++ b/csi_proxy_proto/filesystem/v1/api.proto @@ -0,0 +1,136 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/filesystem/v1"; + +service Filesystem { + // PathExists checks if the requested path exists in the host filesystem. + rpc PathExists(PathExistsRequest) returns (PathExistsResponse) {} + + // Mkdir creates a directory at the requested path in the host filesystem. + rpc Mkdir(MkdirRequest) returns (MkdirResponse) {} + + // Rmdir removes the directory at the requested path in the host filesystem. + // This may be used for unlinking a symlink created through CreateSymlink. + rpc Rmdir(RmdirRequest) returns (RmdirResponse) {} + + // CreateSymlink creates a symbolic link called target_path that points to source_path + // in the host filesystem (target_path is the name of the symbolic link created, + // source_path is the existing path). + rpc CreateSymlink(CreateSymlinkRequest) returns (CreateSymlinkResponse) {} + + // IsSymlink checks if a given path is a symlink. + rpc IsSymlink(IsSymlinkRequest) returns (IsSymlinkResponse) {} +} + +message PathExistsRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; +} + +message PathExistsResponse { + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool exists = 1; +} + +message MkdirRequest { + // The path to create in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // Non-existent parent directories in the path will be automatically created. + // Directories will be created with Read and Write privileges of the Windows + // User account under which csi-proxy is started (typically LocalSystem). + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // The path parameter cannot already exist in the host's filesystem. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Maximum path length will be capped to 260 characters. + string path = 1; +} + +message MkdirResponse { + // Intentionally empty. +} + +message RmdirRequest { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Force remove all contents under path (if any). + bool force = 2; +} + +message RmdirResponse { + // Intentionally empty. +} + +message CreateSymlinkRequest { + // The path of the existing directory to be linked. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs needs to match the paths specified as + // kubelet-csi-plugins-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + string source_path = 1; + + // Target path is the location of the new directory entry to be created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs to match the paths specified as + // kubelet-pod-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + string target_path = 2; +} + +message CreateSymlinkResponse { + // Intentionally empty. +} + +message IsSymlinkRequest { + // The path whose existence as a symlink we want to check in the host's filesystem. + string path = 1; +} + +message IsSymlinkResponse { + // Indicates whether the path in IsSymlinkRequest is a symlink. + bool is_symlink = 1; +} diff --git a/csi_proxy_proto/filesystem/v1alpha1/api.proto b/csi_proxy_proto/filesystem/v1alpha1/api.proto new file mode 100644 index 0000000..8856da5 --- /dev/null +++ b/csi_proxy_proto/filesystem/v1alpha1/api.proto @@ -0,0 +1,168 @@ +syntax = "proto3"; + +package v1alpha1; + +service Filesystem { + // PathExists checks if the requested path exists in the host's filesystem + rpc PathExists(PathExistsRequest) returns (PathExistsResponse) {} + + // Mkdir creates a directory at the requested path in the host's filesystem + rpc Mkdir(MkdirRequest) returns (MkdirResponse) {} + + // Rmdir removes the directory at the requested path in the host's filesystem. + // This may be used for unlinking a symlink created through LinkPath + rpc Rmdir(RmdirRequest) returns (RmdirResponse) {} + + // LinkPath creates a local directory symbolic link between a source path + // and target path in the host's filesystem + rpc LinkPath(LinkPathRequest) returns (LinkPathResponse) {} + + //IsMountPoint checks if a given path is mount or not + rpc IsMountPoint(IsMountPointRequest) returns (IsMountPointResponse) {} +} + +// Context of the paths used for path prefix validation +enum PathContext { + // Indicates the kubelet-csi-plugins-path parameter of csi-proxy be used as + // the path context. This may be used while handling NodeStageVolume where + // a volume may need to be mounted at a plugin-specific path like: + // kubelet\plugins\kubernetes.io\csi\pv\\globalmount + PLUGIN = 0; + // Indicates the kubelet-pod-path parameter of csi-proxy be used as the path + // context. This may be used while handling NodePublishVolume where a staged + // volume may be need to be symlinked to a pod-specific path like: + // kubelet\pods\\volumes\kubernetes.io~csi\\mount + POD = 1; +} + +message PathExistsRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; +} + +message PathExistsResponse { + // Error message if any. Empty string indicates success + string error = 1; + + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool exists = 2; +} + +message MkdirRequest { + // The path to create in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // Non-existent parent directories in the path will be automatically created. + // Directories will be created with Read and Write privileges of the Windows + // User account under which csi-proxy is started (typically LocalSystem). + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // The path parameter cannot already exist in the host's filesystem. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; +} + +message MkdirResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message RmdirRequest { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; + + // Force remove all contents under path (if any). + bool force = 3; +} + +message RmdirResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message LinkPathRequest { + // The path where the symlink is created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs needs to match the paths specified as + // kubelet-csi-plugins-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + string source_path = 1; + + // Target path in the host's filesystem used for the symlink creation. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs to match the paths specified as + // kubelet-pod-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + string target_path = 2; +} + +message LinkPathResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message IsMountPointRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; +} + +message IsMountPointResponse { + // Error message if any. Empty string indicates success + string error = 1; + + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool is_mount_point = 2; +} diff --git a/csi_proxy_proto/filesystem/v1beta1/api.proto b/csi_proxy_proto/filesystem/v1beta1/api.proto new file mode 100644 index 0000000..3b4c0ab --- /dev/null +++ b/csi_proxy_proto/filesystem/v1beta1/api.proto @@ -0,0 +1,168 @@ +syntax = "proto3"; + +package v1beta1; + +service Filesystem { + // PathExists checks if the requested path exists in the host's filesystem + rpc PathExists(PathExistsRequest) returns (PathExistsResponse) {} + + // Mkdir creates a directory at the requested path in the host's filesystem + rpc Mkdir(MkdirRequest) returns (MkdirResponse) {} + + // Rmdir removes the directory at the requested path in the host's filesystem. + // This may be used for unlinking a symlink created through LinkPath + rpc Rmdir(RmdirRequest) returns (RmdirResponse) {} + + // LinkPath creates a local directory symbolic link between a source path + // and target path in the host's filesystem + rpc LinkPath(LinkPathRequest) returns (LinkPathResponse) {} + + //IsMountPoint checks if a given path is mount or not + rpc IsMountPoint(IsMountPointRequest) returns (IsMountPointResponse) {} +} + +// Context of the paths used for path prefix validation +enum PathContext { + // Indicates the kubelet-csi-plugins-path parameter of csi-proxy be used as + // the path context. This may be used while handling NodeStageVolume where + // a volume may need to be mounted at a plugin-specific path like: + // kubelet\plugins\kubernetes.io\csi\pv\\globalmount + PLUGIN = 0; + // Indicates the kubelet-pod-path parameter of csi-proxy be used as the path + // context. This may be used while handling NodePublishVolume where a staged + // volume may be need to be symlinked to a pod-specific path like: + // kubelet\pods\\volumes\kubernetes.io~csi\\mount + POD = 1; +} + +message PathExistsRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; +} + +message PathExistsResponse { + // Error message if any. Empty string indicates success + string error = 1; + + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool exists = 2; +} + +message MkdirRequest { + // The path to create in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // Non-existent parent directories in the path will be automatically created. + // Directories will be created with Read and Write privileges of the Windows + // User account under which csi-proxy is started (typically LocalSystem). + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // The path parameter cannot already exist in the host's filesystem. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; +} + +message MkdirResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message RmdirRequest { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; + + // Force remove all contents under path (if any). + bool force = 3; +} + +message RmdirResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message LinkPathRequest { + // The path where the symlink is created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs needs to match the paths specified as + // kubelet-csi-plugins-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + string source_path = 1; + + // Target path in the host's filesystem used for the symlink creation. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs to match the paths specified as + // kubelet-pod-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + string target_path = 2; +} + +message LinkPathResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message IsMountPointRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; +} + +message IsMountPointResponse { + // Error message if any. Empty string indicates success + string error = 1; + + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool is_mount_point = 2; +} diff --git a/csi_proxy_proto/filesystem/v1beta2/api.proto b/csi_proxy_proto/filesystem/v1beta2/api.proto new file mode 100644 index 0000000..fdaf391 --- /dev/null +++ b/csi_proxy_proto/filesystem/v1beta2/api.proto @@ -0,0 +1,136 @@ +syntax = "proto3"; + +package v1beta2; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/filesystem/v1beta2"; + +service Filesystem { + // PathExists checks if the requested path exists in the host filesystem. + rpc PathExists(PathExistsRequest) returns (PathExistsResponse) {} + + // Mkdir creates a directory at the requested path in the host filesystem. + rpc Mkdir(MkdirRequest) returns (MkdirResponse) {} + + // Rmdir removes the directory at the requested path in the host filesystem. + // This may be used for unlinking a symlink created through CreateSymlink. + rpc Rmdir(RmdirRequest) returns (RmdirResponse) {} + + // CreateSymlink creates a symbolic link called target_path that points to source_path + // in the host filesystem (target_path is the name of the symbolic link created, + // source_path is the existing path). + rpc CreateSymlink(CreateSymlinkRequest) returns (CreateSymlinkResponse) {} + + // IsSymlink checks if a given path is a symlink. + rpc IsSymlink(IsSymlinkRequest) returns (IsSymlinkResponse) {} +} + +message PathExistsRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; +} + +message PathExistsResponse { + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool exists = 1; +} + +message MkdirRequest { + // The path to create in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // Non-existent parent directories in the path will be automatically created. + // Directories will be created with Read and Write privileges of the Windows + // User account under which csi-proxy is started (typically LocalSystem). + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // The path parameter cannot already exist in the host's filesystem. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Maximum path length will be capped to 260 characters. + string path = 1; +} + +message MkdirResponse { + // Intentionally empty. +} + +message RmdirRequest { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Force remove all contents under path (if any). + bool force = 2; +} + +message RmdirResponse { + // Intentionally empty. +} + +message CreateSymlinkRequest { + // The path of the existing directory to be linked. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs needs to match the paths specified as + // kubelet-csi-plugins-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + string source_path = 1; + + // Target path is the location of the new directory entry to be created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs to match the paths specified as + // kubelet-pod-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + string target_path = 2; +} + +message CreateSymlinkResponse { + // Intentionally empty. +} + +message IsSymlinkRequest { + // The path whose existence as a symlink we want to check in the host's filesystem. + string path = 1; +} + +message IsSymlinkResponse { + // Indicates whether the path in IsSymlinkRequest is a symlink. + bool is_symlink = 1; +} diff --git a/csi_proxy_proto/filesystem/v2alpha1/api.proto b/csi_proxy_proto/filesystem/v2alpha1/api.proto new file mode 100644 index 0000000..b46d736 --- /dev/null +++ b/csi_proxy_proto/filesystem/v2alpha1/api.proto @@ -0,0 +1,163 @@ +syntax = "proto3"; + +package v2alpha1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/filesystem/v2alpha1"; + +service Filesystem { + // PathExists checks if the requested path exists in the host filesystem. + rpc PathExists(PathExistsRequest) returns (PathExistsResponse) {} + + // Mkdir creates a directory at the requested path in the host filesystem. + rpc Mkdir(MkdirRequest) returns (MkdirResponse) {} + + // Rmdir removes the directory at the requested path in the host filesystem. + // This may be used for unlinking a symlink created through CreateSymlink. + rpc Rmdir(RmdirRequest) returns (RmdirResponse) {} + + // RmdirContents removes the contents of a directory in the host filesystem. + // Unlike Rmdir it won't delete the requested path, it'll only delete its contents. + rpc RmdirContents(RmdirContentsRequest) returns (RmdirContentsResponse) {} + + // CreateSymlink creates a symbolic link called target_path that points to source_path + // in the host filesystem (target_path is the name of the symbolic link created, + // source_path is the existing path). + rpc CreateSymlink(CreateSymlinkRequest) returns (CreateSymlinkResponse) {} + + // IsSymlink checks if a given path is a symlink. + rpc IsSymlink(IsSymlinkRequest) returns (IsSymlinkResponse) {} +} + +message PathExistsRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; +} + +message PathExistsResponse { + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool exists = 1; +} + +message MkdirRequest { + // The path to create in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // Non-existent parent directories in the path will be automatically created. + // Directories will be created with Read and Write privileges of the Windows + // User account under which csi-proxy is started (typically LocalSystem). + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // The path parameter cannot already exist in the host's filesystem. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Maximum path length will be capped to 260 characters. + string path = 1; +} + +message MkdirResponse { + // Intentionally empty. +} + +message RmdirRequest { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Force remove all contents under path (if any). + bool force = 2; +} + +message RmdirResponse { + // Intentionally empty. +} + +message RmdirContentsRequest { + // The path whose contents will be removed in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; +} + +message RmdirContentsResponse { + // Intentionally empty. +} + +message CreateSymlinkRequest { + // The path of the existing directory to be linked. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs needs to match the paths specified as + // kubelet-csi-plugins-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + string source_path = 1; + + // Target path is the location of the new directory entry to be created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs to match the paths specified as + // kubelet-pod-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + string target_path = 2; +} + +message CreateSymlinkResponse { + // Intentionally empty. +} + +message IsSymlinkRequest { + // The path whose existence as a symlink we want to check in the host's filesystem. + string path = 1; +} + +message IsSymlinkResponse { + // Indicates whether the path in IsSymlinkRequest is a symlink. + bool is_symlink = 1; +} diff --git a/csi_proxy_proto/iscsi/v1alpha1/api.proto b/csi_proxy_proto/iscsi/v1alpha1/api.proto new file mode 100644 index 0000000..b667f8d --- /dev/null +++ b/csi_proxy_proto/iscsi/v1alpha1/api.proto @@ -0,0 +1,153 @@ +syntax = "proto3"; + +package v1alpha1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/iscsi/v1alpha1"; + +service Iscsi { + // AddTargetPortal registers an iSCSI target network address for later + // discovery. + // AddTargetPortal currently does not support selecting different NICs or + // a different iSCSI initiator (e.g a hardware initiator). This means that + // Windows will select the initiator NIC and instance on its own. + rpc AddTargetPortal(AddTargetPortalRequest) + returns (AddTargetPortalResponse) {} + + // DiscoverTargetPortal initiates discovery on an iSCSI target network address + // and returns discovered IQNs. + rpc DiscoverTargetPortal(DiscoverTargetPortalRequest) + returns (DiscoverTargetPortalResponse) {} + + // RemoveTargetPortal removes an iSCSI target network address registration. + rpc RemoveTargetPortal(RemoveTargetPortalRequest) + returns (RemoveTargetPortalResponse) {} + + // ListTargetPortal lists all currently registered iSCSI target network + // addresses. + rpc ListTargetPortals(ListTargetPortalsRequest) + returns (ListTargetPortalsResponse) {} + + // ConnectTarget connects to an iSCSI Target + rpc ConnectTarget(ConnectTargetRequest) returns (ConnectTargetResponse) {} + + // DisconnectTarget disconnects from an iSCSI Target + rpc DisconnectTarget(DisconnectTargetRequest) + returns (DisconnectTargetResponse) {} + + // GetTargetDisks returns the disk addresses that correspond to an iSCSI + // target + rpc GetTargetDisks(GetTargetDisksRequest) returns (GetTargetDisksResponse) {} +} + +// TargetPortal is an address and port pair for a specific iSCSI storage +// target. +message TargetPortal { + // iSCSI Target (server) address + string target_address = 1; + + // iSCSI Target port (default iSCSI port is 3260) + uint32 target_port = 2; +} + +message AddTargetPortalRequest { + // iSCSI Target Portal to register in the initiator + TargetPortal target_portal = 1; +} + +message AddTargetPortalResponse { + // Intentionally empty +} + +message DiscoverTargetPortalRequest { + // iSCSI Target Portal on which to initiate discovery + TargetPortal target_portal = 1; +} + +message DiscoverTargetPortalResponse { + // List of discovered IQN addresses + // follows IQN format: iqn.yyyy-mm.naming-authority:unique-name + repeated string iqns = 1; +} + +message RemoveTargetPortalRequest { + // iSCSI Target Portal + TargetPortal target_portal = 1; +} + +message RemoveTargetPortalResponse { + // Intentionally empty +} + +message ListTargetPortalsRequest { + // Intentionally empty +} + +message ListTargetPortalsResponse { + // A list of Target Portals currently registered in the initiator + repeated TargetPortal target_portals = 1; +} + +enum AuthenticationType { + // No authentication is used + NONE = 0; + + // One way CHAP authentication. The target authenticates the initiator. + ONE_WAY_CHAP = 1; + + // Mutual CHAP authentication. The target and initiator authenticate each + // other. + MUTUAL_CHAP = 2; +} + +message ConnectTargetRequest { + // Target portal to which the initiator will connect + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; + + // Connection authentication type, None by default + // + // One Way Chap uses the chap_username and chap_secret + // fields mentioned below to authenticate the initiator. + // + // Mutual Chap uses both the user/secret mentioned below + // and the Initiator Chap Secret to authenticate the target and initiator. + AuthenticationType auth_type = 3; + + // CHAP Username used to authenticate the initiator + string chap_username = 4; + + // CHAP password used to authenticate the initiator + string chap_secret = 5; +} + +message ConnectTargetResponse { + // Intentionally empty +} + +message GetTargetDisksRequest { + // Target portal whose disks will be queried + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; +} + +message GetTargetDisksResponse { + // List composed of disk ids (numbers) that are associated with the + // iSCSI target + repeated string diskIDs = 1; +} + +message DisconnectTargetRequest { + // Target portal from which initiator will disconnect + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; +} + +message DisconnectTargetResponse { + // Intentionally empty +} diff --git a/csi_proxy_proto/iscsi/v1alpha2/api.proto b/csi_proxy_proto/iscsi/v1alpha2/api.proto new file mode 100644 index 0000000..baa5bee --- /dev/null +++ b/csi_proxy_proto/iscsi/v1alpha2/api.proto @@ -0,0 +1,175 @@ +syntax = "proto3"; + +package v1alpha2; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/iscsi/v1alpha2"; + +service Iscsi { + // AddTargetPortal registers an iSCSI target network address for later + // discovery. + // AddTargetPortal currently does not support selecting different NICs or + // a different iSCSI initiator (e.g a hardware initiator). This means that + // Windows will select the initiator NIC and instance on its own. + rpc AddTargetPortal(AddTargetPortalRequest) + returns (AddTargetPortalResponse) {} + + // DiscoverTargetPortal initiates discovery on an iSCSI target network address + // and returns discovered IQNs. + rpc DiscoverTargetPortal(DiscoverTargetPortalRequest) + returns (DiscoverTargetPortalResponse) {} + + // RemoveTargetPortal removes an iSCSI target network address registration. + rpc RemoveTargetPortal(RemoveTargetPortalRequest) + returns (RemoveTargetPortalResponse) {} + + // ListTargetPortal lists all currently registered iSCSI target network + // addresses. + rpc ListTargetPortals(ListTargetPortalsRequest) + returns (ListTargetPortalsResponse) {} + + // ConnectTarget connects to an iSCSI Target + rpc ConnectTarget(ConnectTargetRequest) returns (ConnectTargetResponse) {} + + // DisconnectTarget disconnects from an iSCSI Target + rpc DisconnectTarget(DisconnectTargetRequest) + returns (DisconnectTargetResponse) {} + + // GetTargetDisks returns the disk addresses that correspond to an iSCSI + // target + rpc GetTargetDisks(GetTargetDisksRequest) returns (GetTargetDisksResponse) {} + + // SetMutualChapSecret sets the default CHAP secret that all initiators on + // this machine (node) use to authenticate the target on mutual CHAP + // authentication. + // NOTE: This method affects global node state and should only be used + // with consideration to other CSI drivers that run concurrently. + rpc SetMutualChapSecret(SetMutualChapSecretRequest) + returns (SetMutualChapSecretResponse) {} +} + +// TargetPortal is an address and port pair for a specific iSCSI storage +// target. +message TargetPortal { + // iSCSI Target (server) address + string target_address = 1; + + // iSCSI Target port (default iSCSI port is 3260) + uint32 target_port = 2; +} + +message AddTargetPortalRequest { + // iSCSI Target Portal to register in the initiator + TargetPortal target_portal = 1; +} + +message AddTargetPortalResponse { + // Intentionally empty +} + +message DiscoverTargetPortalRequest { + // iSCSI Target Portal on which to initiate discovery + TargetPortal target_portal = 1; +} + +message DiscoverTargetPortalResponse { + // List of discovered IQN addresses + // follows IQN format: iqn.yyyy-mm.naming-authority:unique-name + repeated string iqns = 1; +} + +message RemoveTargetPortalRequest { + // iSCSI Target Portal + TargetPortal target_portal = 1; +} + +message RemoveTargetPortalResponse { + // Intentionally empty +} + +message ListTargetPortalsRequest { + // Intentionally empty +} + +message ListTargetPortalsResponse { + // A list of Target Portals currently registered in the initiator + repeated TargetPortal target_portals = 1; +} + +// iSCSI logon authentication type +enum AuthenticationType { + // No authentication is used + NONE = 0; + + // One way CHAP authentication. The target authenticates the initiator. + ONE_WAY_CHAP = 1; + + // Mutual CHAP authentication. The target and initiator authenticate each + // other. + MUTUAL_CHAP = 2; +} + +message ConnectTargetRequest { + // Target portal to which the initiator will connect + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; + + // Connection authentication type, None by default + // + // One Way Chap uses the chap_username and chap_secret + // fields mentioned below to authenticate the initiator. + // + // Mutual Chap uses both the user/secret mentioned below + // and the Initiator Chap Secret (See `SetMutualChapSecret`) + // to authenticate the target and initiator. + AuthenticationType auth_type = 3; + + // CHAP Username used to authenticate the initiator + string chap_username = 4; + + // CHAP password used to authenticate the initiator + string chap_secret = 5; +} + +message ConnectTargetResponse { + // Intentionally empty +} + +message GetTargetDisksRequest { + // Target portal whose disks will be queried + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; +} + +message GetTargetDisksResponse { + // List composed of disk ids (numbers) that are associated with the + // iSCSI target + repeated string diskIDs = 1; +} + +message DisconnectTargetRequest { + // Target portal from which initiator will disconnect + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; +} + +message DisconnectTargetResponse { + // Intentionally empty +} + +message SetMutualChapSecretRequest { + // the default CHAP secret that all initiators on this machine (node) use to + // authenticate the target on mutual CHAP authentication. + // Must be at least 12 byte long for non-Ipsec connections, at least one + // byte long for Ipsec connections, and at most 16 bytes long. + string MutualChapSecret = 1; +} + +message SetMutualChapSecretResponse { + // Intentionally empty +} diff --git a/csi_proxy_proto/smb/v1/api.proto b/csi_proxy_proto/smb/v1/api.proto new file mode 100644 index 0000000..6f10635 --- /dev/null +++ b/csi_proxy_proto/smb/v1/api.proto @@ -0,0 +1,58 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/smb/v1"; + +service Smb { + // NewSmbGlobalMapping creates an SMB mapping on the SMB client to an SMB share. + rpc NewSmbGlobalMapping(NewSmbGlobalMappingRequest) returns (NewSmbGlobalMappingResponse) {} + + // RemoveSmbGlobalMapping removes the SMB mapping to an SMB share. + rpc RemoveSmbGlobalMapping(RemoveSmbGlobalMappingRequest) returns (RemoveSmbGlobalMappingResponse) {} +} + + +message NewSmbGlobalMappingRequest { + // A remote SMB share to mount + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB remote path specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; + + // Optional local path to mount the smb on + string local_path = 2; + + // Username credential associated with the share + string username = 3; + + // Password credential associated with the share + string password = 4; +} + +message NewSmbGlobalMappingResponse { + // Intentionally empty. +} + + +message RemoveSmbGlobalMappingRequest { + // A remote SMB share mapping to remove + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB share specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; +} + +message RemoveSmbGlobalMappingResponse { + // Intentionally empty. +} diff --git a/csi_proxy_proto/smb/v1alpha1/api.proto b/csi_proxy_proto/smb/v1alpha1/api.proto new file mode 100644 index 0000000..f4d96e9 --- /dev/null +++ b/csi_proxy_proto/smb/v1alpha1/api.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +package v1alpha1; + +service Smb { + // NewSmbGlobalMapping creates an SMB mapping on the SMB client to an SMB share. + rpc NewSmbGlobalMapping(NewSmbGlobalMappingRequest) returns (NewSmbGlobalMappingResponse) {} + + // RemoveSmbGlobalMapping removes the SMB mapping to an SMB share. + rpc RemoveSmbGlobalMapping(RemoveSmbGlobalMappingRequest) returns (RemoveSmbGlobalMappingResponse) {} +} + + +message NewSmbGlobalMappingRequest { + // A remote SMB share to mount + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB remote path specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; + // Optional local path to mount the smb on + string local_path = 2; + + // Username credential associated with the share + string username = 3; + + // Password credential associated with the share + string password = 4; +} + +message NewSmbGlobalMappingResponse { + // Windows error code + // Success is represented as 0 + string error = 1; +} + + +message RemoveSmbGlobalMappingRequest { + // A remote SMB share mapping to remove + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB share specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; +} + +message RemoveSmbGlobalMappingResponse { + // Windows error code + // Success is represented as 0 + string error = 1; +} \ No newline at end of file diff --git a/csi_proxy_proto/smb/v1beta1/api.proto b/csi_proxy_proto/smb/v1beta1/api.proto new file mode 100644 index 0000000..8a2b515 --- /dev/null +++ b/csi_proxy_proto/smb/v1beta1/api.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +package v1beta1; + +service Smb { + // NewSmbGlobalMapping creates an SMB mapping on the SMB client to an SMB share. + rpc NewSmbGlobalMapping(NewSmbGlobalMappingRequest) returns (NewSmbGlobalMappingResponse) {} + + // RemoveSmbGlobalMapping removes the SMB mapping to an SMB share. + rpc RemoveSmbGlobalMapping(RemoveSmbGlobalMappingRequest) returns (RemoveSmbGlobalMappingResponse) {} +} + + +message NewSmbGlobalMappingRequest { + // A remote SMB share to mount + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB remote path specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; + // Optional local path to mount the smb on + string local_path = 2; + + // Username credential associated with the share + string username = 3; + + // Password credential associated with the share + string password = 4; +} + +message NewSmbGlobalMappingResponse { + // Windows error code + // Success is represented as 0 + string error = 1; +} + + +message RemoveSmbGlobalMappingRequest { + // A remote SMB share mapping to remove + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB share specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; +} + +message RemoveSmbGlobalMappingResponse { + // Windows error code + // Success is represented as 0 + string error = 1; +} diff --git a/csi_proxy_proto/smb/v1beta2/api.proto b/csi_proxy_proto/smb/v1beta2/api.proto new file mode 100644 index 0000000..ff28b11 --- /dev/null +++ b/csi_proxy_proto/smb/v1beta2/api.proto @@ -0,0 +1,58 @@ +syntax = "proto3"; + +package v1beta2; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/smb/v1beta2"; + +service Smb { + // NewSmbGlobalMapping creates an SMB mapping on the SMB client to an SMB share. + rpc NewSmbGlobalMapping(NewSmbGlobalMappingRequest) returns (NewSmbGlobalMappingResponse) {} + + // RemoveSmbGlobalMapping removes the SMB mapping to an SMB share. + rpc RemoveSmbGlobalMapping(RemoveSmbGlobalMappingRequest) returns (RemoveSmbGlobalMappingResponse) {} +} + + +message NewSmbGlobalMappingRequest { + // A remote SMB share to mount + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB remote path specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; + + // Optional local path to mount the smb on + string local_path = 2; + + // Username credential associated with the share + string username = 3; + + // Password credential associated with the share + string password = 4; +} + +message NewSmbGlobalMappingResponse { + // Intentionally empty. +} + + +message RemoveSmbGlobalMappingRequest { + // A remote SMB share mapping to remove + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB share specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; +} + +message RemoveSmbGlobalMappingResponse { + // Intentionally empty. +} diff --git a/csi_proxy_proto/system/v1alpha1/api.proto b/csi_proxy_proto/system/v1alpha1/api.proto new file mode 100644 index 0000000..d04923e --- /dev/null +++ b/csi_proxy_proto/system/v1alpha1/api.proto @@ -0,0 +1,93 @@ +syntax = "proto3"; + +package v1alpha1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/system/v1alpha1"; + +service System { + // GetBIOSSerialNumber returns the device's serial number + rpc GetBIOSSerialNumber(GetBIOSSerialNumberRequest) + returns (GetBIOSSerialNumberResponse) {} + + // StartService starts a Windows service + // NOTE: This method affects global node state and should only be used + // with consideration to other CSI drivers that run concurrently. + rpc StartService(StartServiceRequest) returns (StartServiceResponse) {} + + // StopService stops a Windows service + // NOTE: This method affects global node state and should only be used + // with consideration to other CSI drivers that run concurrently. + rpc StopService(StopServiceRequest) returns (StopServiceResponse) {} + + // GetService queries a Windows service state + rpc GetService(GetServiceRequest) returns (GetServiceResponse) {} +} + +message GetBIOSSerialNumberRequest { + // Intentionally empty +} + +message GetBIOSSerialNumberResponse { + // Serial number + string serial_number = 1; +} + +message StartServiceRequest { + // Service name (as listed in System\CCS\Services keys) + string name = 1; +} + +message StartServiceResponse { + // Intentionally empty +} + +message StopServiceRequest { + // Service name (as listed in System\CCS\Services keys) + string name = 1; + + // Forces stopping of services that has dependent services + bool force = 2; +} + +message StopServiceResponse { + // Intentionally empty +} + +// https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_status#members +enum ServiceStatus { + UNKNOWN = 0; + STOPPED = 1; + START_PENDING = 2; + STOP_PENDING = 3; + RUNNING = 4; + CONTINUE_PENDING = 5; + PAUSE_PENDING = 6; + PAUSED = 7; +} + +// https://docs.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-changeserviceconfiga +enum StartType { + BOOT = 0; + SYSTEM = 1; + AUTOMATIC = 2; + MANUAL = 3; + DISABLED = 4; +} + +message GetServiceRequest { + // Service name (as listed in System\CCS\Services keys) + string name = 1; +} + +message GetServiceResponse { + // Service display name + string display_name = 1; + + // Service start type. + // Used to control whether a service will start on boot, and if so on which + // boot phase. + StartType start_type = 2; + + // Service status, e.g. stopped, running, paused + ServiceStatus status = 3; +} diff --git a/csi_proxy_proto/volume/v1/api.proto b/csi_proxy_proto/volume/v1/api.proto new file mode 100644 index 0000000..a065381 --- /dev/null +++ b/csi_proxy_proto/volume/v1/api.proto @@ -0,0 +1,143 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/volume/v1"; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for all volumes from a + // given disk number and partition number (optional) + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + + // MountVolume mounts the volume at the requested global staging path. + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + + // UnmountVolume flushes data cache to disk and removes the global staging path. + rpc UnmountVolume(UnmountVolumeRequest) returns (UnmountVolumeResponse) {} + + // IsVolumeFormatted checks if a volume is formatted. + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + + // FormatVolume formats a volume with NTFS. + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + + // ResizeVolume performs resizing of the partition and file system for a block based volume. + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} + + // GetVolumeStats gathers total bytes and used bytes for a volume. + rpc GetVolumeStats(GetVolumeStatsRequest) returns (GetVolumeStatsResponse) {} + + // GetDiskNumberFromVolumeID gets the disk number of the disk where the volume is located. + rpc GetDiskNumberFromVolumeID(GetDiskNumberFromVolumeIDRequest) returns (GetDiskNumberFromVolumeIDResponse ) {} + + // GetVolumeIDFromTargetPath gets the volume id for a given target path. + rpc GetVolumeIDFromTargetPath(GetVolumeIDFromTargetPathRequest) returns (GetVolumeIDFromTargetPathResponse) {} + + // WriteVolumeCache write volume cache to disk. + rpc WriteVolumeCache(WriteVolumeCacheRequest) returns (WriteVolumeCacheResponse) {} +} + +message ListVolumesOnDiskRequest { + // Disk device number of the disk to query for volumes. + uint32 disk_number = 1; + // The partition number (optional), by default it uses the first partition of the disk. + uint32 partition_number = 2; +} + +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk. + repeated string volume_ids = 1; +} + +message MountVolumeRequest { + // Volume device ID of the volume to mount. + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted. + string target_path = 2; +} + +message MountVolumeResponse { + // Intentionally empty. +} + +message UnmountVolumeRequest { + // Volume device ID of the volume to dismount. + string volume_id = 1; + // Path where the volume has been mounted. + string target_path = 2; +} + +message UnmountVolumeResponse { + // Intentionally empty. +} + +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check. + string volume_id = 1; +} + +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS. + bool formatted = 1; +} + +message FormatVolumeRequest { + // Volume device ID of the volume to format. + string volume_id = 1; +} + +message FormatVolumeResponse { + // Intentionally empty. +} + +message ResizeVolumeRequest { + // Volume device ID of the volume to resize. + string volume_id = 1; + // New size in bytes of the volume. + int64 size_bytes = 2; +} + +message ResizeVolumeResponse { + // Intentionally empty. +} + +message GetVolumeStatsRequest{ + // Volume device Id of the volume to get the stats for. + string volume_id = 1; +} + +message GetVolumeStatsResponse{ + // Total bytes + int64 total_bytes = 1; + // Used bytes + int64 used_bytes = 2; +} + +message GetDiskNumberFromVolumeIDRequest { + // Volume device ID of the volume to get the disk number for. + string volume_id = 1; +} + +message GetDiskNumberFromVolumeIDResponse { + // Corresponding disk number. + uint32 disk_number = 1; +} + +message GetVolumeIDFromTargetPathRequest { + // The target path. + string target_path = 1; +} + +message GetVolumeIDFromTargetPathResponse { + // The volume device ID. + string volume_id = 1; +} + +message WriteVolumeCacheRequest { + // Volume device ID of the volume to flush the cache. + string volume_id = 1; +} + +message WriteVolumeCacheResponse { + // Intentionally empty. +} diff --git a/csi_proxy_proto/volume/v1alpha1/api.proto b/csi_proxy_proto/volume/v1alpha1/api.proto new file mode 100644 index 0000000..5037859 --- /dev/null +++ b/csi_proxy_proto/volume/v1alpha1/api.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; + +package v1alpha1; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for + // all volumes on a Disk device + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + // MountVolume mounts the volume at the requested global staging path + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + // DismountVolume gracefully dismounts a volume + rpc DismountVolume(DismountVolumeRequest) returns (DismountVolumeResponse) {} + // IsVolumeFormatted checks if a volume is formatted with NTFS + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + // FormatVolume formats a volume with the provided file system + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + // ResizeVolume performs resizing of the partition and file system for a block based volume + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} +} +message ListVolumesOnDiskRequest { + // Disk device ID of the disk to query for volumes + string disk_id = 1; +} +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk + repeated string volume_ids = 1; +} +message MountVolumeRequest { + // Volume device ID of the volume to mount + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted + string path = 2; +} +message MountVolumeResponse { + // Intentionally empty +} +message DismountVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // Path where the volume has been mounted. + string path = 2; +} +message DismountVolumeResponse { + // Intentionally empty +} +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check + string volume_id = 1; +} +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS + bool formatted = 1; +} +message FormatVolumeRequest { + // Volume device ID of the volume to format + string volume_id = 1; +} +message FormatVolumeResponse { + // Intentionally empty +} +message ResizeVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // New size of the volume + int64 size = 2; +} +message ResizeVolumeResponse { + // Intentionally empty +} \ No newline at end of file diff --git a/csi_proxy_proto/volume/v1beta1/api.proto b/csi_proxy_proto/volume/v1beta1/api.proto new file mode 100644 index 0000000..07eb7b2 --- /dev/null +++ b/csi_proxy_proto/volume/v1beta1/api.proto @@ -0,0 +1,121 @@ +syntax = "proto3"; + +package v1beta1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/volume/v1beta1"; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for + // all volumes on a Disk device + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + // MountVolume mounts the volume at the requested global staging path + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + // DismountVolume gracefully dismounts a volume + rpc DismountVolume(DismountVolumeRequest) returns (DismountVolumeResponse) {} + // IsVolumeFormatted checks if a volume is formatted with NTFS + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + // FormatVolume formats a volume with the provided file system + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + // ResizeVolume performs resizing of the partition and file system for a block based volume + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} + // VolumeStats gathers DiskSize, VolumeSize and VolumeUsedSize for a volume + rpc VolumeStats(VolumeStatsRequest) returns (VolumeStatsResponse) {} + // GetVolumeDiskNumber gets the disk number of the disk where the volume is located + rpc GetVolumeDiskNumber(VolumeDiskNumberRequest) returns (VolumeDiskNumberResponse) {} + // GetVolumeIDFromMount gets the volume id for a given mount + rpc GetVolumeIDFromMount(VolumeIDFromMountRequest) returns (VolumeIDFromMountResponse) {} +} + +message ListVolumesOnDiskRequest { + // Disk device ID of the disk to query for volumes + string disk_id = 1; +} + +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk + repeated string volume_ids = 1; +} + +message MountVolumeRequest { + // Volume device ID of the volume to mount + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted + string path = 2; +} + +message MountVolumeResponse { + // Intentionally empty +} + +message DismountVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // Path where the volume has been mounted. + string path = 2; +} + +message DismountVolumeResponse { + // Intentionally empty +} + +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check + string volume_id = 1; +} + +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS + bool formatted = 1; +} + +message FormatVolumeRequest { + // Volume device ID of the volume to format + string volume_id = 1; +} + +message FormatVolumeResponse { + // Intentionally empty +} + +message ResizeVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // New size of the volume + int64 size = 2; +} + +message ResizeVolumeResponse { + // Intentionally empty +} + +message VolumeStatsRequest{ + // Volume device Id of the volume to get the stats for + string volume_id = 1; +} + +message VolumeStatsResponse{ + // Capacity of the volume + int64 volumeSize = 1; + // Used bytes + int64 volumeUsedSize = 2; +} + +message VolumeDiskNumberRequest{ + // Volume device Id of the volume to get the disk number for + string volume_id = 1; +} + +message VolumeDiskNumberResponse{ + // Corresponding disk number + int64 diskNumber = 1; +} + +message VolumeIDFromMountRequest { + // Mount + string mount = 1; +} + +message VolumeIDFromMountResponse { + // Mount + string volume_id = 1; +} diff --git a/csi_proxy_proto/volume/v1beta2/api.proto b/csi_proxy_proto/volume/v1beta2/api.proto new file mode 100644 index 0000000..c88e1f5 --- /dev/null +++ b/csi_proxy_proto/volume/v1beta2/api.proto @@ -0,0 +1,132 @@ +syntax = "proto3"; + +package v1beta2; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/volume/v1beta2"; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for + // all volumes on a Disk device + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + // MountVolume mounts the volume at the requested global staging path + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + // DismountVolume gracefully dismounts a volume + rpc DismountVolume(DismountVolumeRequest) returns (DismountVolumeResponse) {} + // IsVolumeFormatted checks if a volume is formatted with NTFS + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + // FormatVolume formats a volume with the provided file system + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + // ResizeVolume performs resizing of the partition and file system for a block based volume + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} + // VolumeStats gathers DiskSize, VolumeSize and VolumeUsedSize for a volume + rpc VolumeStats(VolumeStatsRequest) returns (VolumeStatsResponse) {} + // GetVolumeDiskNumber gets the disk number of the disk where the volume is located + rpc GetVolumeDiskNumber(VolumeDiskNumberRequest) returns (VolumeDiskNumberResponse) {} + // GetVolumeIDFromMount gets the volume id for a given mount + rpc GetVolumeIDFromMount(VolumeIDFromMountRequest) returns (VolumeIDFromMountResponse) {} + // WriteVolumeCache write volume cache to disk + rpc WriteVolumeCache(WriteVolumeCacheRequest) returns (WriteVolumeCacheResponse) {} +} + +message ListVolumesOnDiskRequest { + // Disk device ID of the disk to query for volumes + string disk_id = 1; +} + +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk + repeated string volume_ids = 1; +} + +message MountVolumeRequest { + // Volume device ID of the volume to mount + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted + string path = 2; +} + +message MountVolumeResponse { + // Intentionally empty +} + +message DismountVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // Path where the volume has been mounted. + string path = 2; +} + +message DismountVolumeResponse { + // Intentionally empty +} + +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check + string volume_id = 1; +} + +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS + bool formatted = 1; +} + +message FormatVolumeRequest { + // Volume device ID of the volume to format + string volume_id = 1; +} + +message FormatVolumeResponse { + // Intentionally empty +} + +message ResizeVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // New size of the volume + int64 size = 2; +} + +message ResizeVolumeResponse { + // Intentionally empty +} + +message VolumeStatsRequest{ + // Volume device Id of the volume to get the stats for + string volume_id = 1; +} + +message VolumeStatsResponse{ + // Capacity of the volume + int64 volumeSize = 1; + // Used bytes + int64 volumeUsedSize = 2; +} + +message VolumeDiskNumberRequest{ + // Volume device Id of the volume to get the disk number for + string volume_id = 1; +} + +message VolumeDiskNumberResponse{ + // Corresponding disk number + int64 diskNumber = 1; +} + +message VolumeIDFromMountRequest { + // Mount + string mount = 1; +} + +message VolumeIDFromMountResponse { + // Mount + string volume_id = 1; +} + +message WriteVolumeCacheRequest { + // Volume device ID of the volume to flush the cache + string volume_id = 1; +} + +message WriteVolumeCacheResponse { + // Intentionally empty +} \ No newline at end of file diff --git a/csi_proxy_proto/volume/v1beta3/api.proto b/csi_proxy_proto/volume/v1beta3/api.proto new file mode 100644 index 0000000..2340b3c --- /dev/null +++ b/csi_proxy_proto/volume/v1beta3/api.proto @@ -0,0 +1,143 @@ +syntax = "proto3"; + +package v1beta3; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/volume/v1beta3"; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for all volumes from a + // given disk number and partition number (optional) + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + + // MountVolume mounts the volume at the requested global staging path. + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + + // UnmountVolume flushes data cache to disk and removes the global staging path. + rpc UnmountVolume(UnmountVolumeRequest) returns (UnmountVolumeResponse) {} + + // IsVolumeFormatted checks if a volume is formatted. + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + + // FormatVolume formats a volume with NTFS. + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + + // ResizeVolume performs resizing of the partition and file system for a block based volume. + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} + + // GetVolumeStats gathers total bytes and used bytes for a volume. + rpc GetVolumeStats(GetVolumeStatsRequest) returns (GetVolumeStatsResponse) {} + + // GetDiskNumberFromVolumeID gets the disk number of the disk where the volume is located. + rpc GetDiskNumberFromVolumeID(GetDiskNumberFromVolumeIDRequest) returns (GetDiskNumberFromVolumeIDResponse ) {} + + // GetVolumeIDFromTargetPath gets the volume id for a given target path. + rpc GetVolumeIDFromTargetPath(GetVolumeIDFromTargetPathRequest) returns (GetVolumeIDFromTargetPathResponse) {} + + // WriteVolumeCache write volume cache to disk. + rpc WriteVolumeCache(WriteVolumeCacheRequest) returns (WriteVolumeCacheResponse) {} +} + +message ListVolumesOnDiskRequest { + // Disk device number of the disk to query for volumes. + uint32 disk_number = 1; + // The partition number (optional), by default it uses the first partition of the disk. + uint32 partition_number = 2; +} + +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk. + repeated string volume_ids = 1; +} + +message MountVolumeRequest { + // Volume device ID of the volume to mount. + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted. + string target_path = 2; +} + +message MountVolumeResponse { + // Intentionally empty. +} + +message UnmountVolumeRequest { + // Volume device ID of the volume to dismount. + string volume_id = 1; + // Path where the volume has been mounted. + string target_path = 2; +} + +message UnmountVolumeResponse { + // Intentionally empty. +} + +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check. + string volume_id = 1; +} + +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS. + bool formatted = 1; +} + +message FormatVolumeRequest { + // Volume device ID of the volume to format. + string volume_id = 1; +} + +message FormatVolumeResponse { + // Intentionally empty. +} + +message ResizeVolumeRequest { + // Volume device ID of the volume to resize. + string volume_id = 1; + // New size in bytes of the volume. + int64 size_bytes = 2; +} + +message ResizeVolumeResponse { + // Intentionally empty. +} + +message GetVolumeStatsRequest{ + // Volume device Id of the volume to get the stats for. + string volume_id = 1; +} + +message GetVolumeStatsResponse{ + // Total bytes + int64 total_bytes = 1; + // Used bytes + int64 used_bytes = 2; +} + +message GetDiskNumberFromVolumeIDRequest { + // Volume device ID of the volume to get the disk number for. + string volume_id = 1; +} + +message GetDiskNumberFromVolumeIDResponse { + // Corresponding disk number. + uint32 disk_number = 1; +} + +message GetVolumeIDFromTargetPathRequest { + // The target path. + string target_path = 1; +} + +message GetVolumeIDFromTargetPathResponse { + // The volume device ID. + string volume_id = 1; +} + +message WriteVolumeCacheRequest { + // Volume device ID of the volume to flush the cache. + string volume_id = 1; +} + +message WriteVolumeCacheResponse { + // Intentionally empty. +} diff --git a/csi_proxy_proto/volume/v2alpha1/api.proto b/csi_proxy_proto/volume/v2alpha1/api.proto new file mode 100644 index 0000000..611d4ab --- /dev/null +++ b/csi_proxy_proto/volume/v2alpha1/api.proto @@ -0,0 +1,158 @@ +syntax = "proto3"; + +package v2alpha1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/volume/v2alpha1"; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for all volumes from a + // given disk number and partition number (optional) + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + + // MountVolume mounts the volume at the requested global staging path. + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + + // UnmountVolume flushes data cache to disk and removes the global staging path. + rpc UnmountVolume(UnmountVolumeRequest) returns (UnmountVolumeResponse) {} + + // IsVolumeFormatted checks if a volume is formatted. + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + + // FormatVolume formats a volume with NTFS. + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + + // ResizeVolume performs resizing of the partition and file system for a block based volume. + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} + + // GetVolumeStats gathers total bytes and used bytes for a volume. + rpc GetVolumeStats(GetVolumeStatsRequest) returns (GetVolumeStatsResponse) {} + + // GetDiskNumberFromVolumeID gets the disk number of the disk where the volume is located. + rpc GetDiskNumberFromVolumeID(GetDiskNumberFromVolumeIDRequest) returns (GetDiskNumberFromVolumeIDResponse ) {} + + // GetVolumeIDFromTargetPath gets the volume id for a given target path. + rpc GetVolumeIDFromTargetPath(GetVolumeIDFromTargetPathRequest) returns (GetVolumeIDFromTargetPathResponse) {} + + // GetClosestVolumeIDFromTargetPath gets the closest volume id for a given target path + // by following symlinks and moving up in the filesystem, if after moving up in the filesystem + // we get to a DriveLetter then the volume corresponding to this drive letter is returned instead. + rpc GetClosestVolumeIDFromTargetPath(GetClosestVolumeIDFromTargetPathRequest) returns (GetClosestVolumeIDFromTargetPathResponse) {} + + // WriteVolumeCache write volume cache to disk. + rpc WriteVolumeCache(WriteVolumeCacheRequest) returns (WriteVolumeCacheResponse) {} +} + +message ListVolumesOnDiskRequest { + // Disk device number of the disk to query for volumes. + uint32 disk_number = 1; + // The partition number (optional), by default it uses the first partition of the disk. + uint32 partition_number = 2; +} + +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk. + repeated string volume_ids = 1; +} + +message MountVolumeRequest { + // Volume device ID of the volume to mount. + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted. + string target_path = 2; +} + +message MountVolumeResponse { + // Intentionally empty. +} + +message UnmountVolumeRequest { + // Volume device ID of the volume to dismount. + string volume_id = 1; + // Path where the volume has been mounted. + string target_path = 2; +} + +message UnmountVolumeResponse { + // Intentionally empty. +} + +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check. + string volume_id = 1; +} + +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS. + bool formatted = 1; +} + +message FormatVolumeRequest { + // Volume device ID of the volume to format. + string volume_id = 1; +} + +message FormatVolumeResponse { + // Intentionally empty. +} + +message ResizeVolumeRequest { + // Volume device ID of the volume to resize. + string volume_id = 1; + // New size in bytes of the volume. + int64 size_bytes = 2; +} + +message ResizeVolumeResponse { + // Intentionally empty. +} + +message GetVolumeStatsRequest{ + // Volume device Id of the volume to get the stats for. + string volume_id = 1; +} + +message GetVolumeStatsResponse{ + // Total bytes + int64 total_bytes = 1; + // Used bytes + int64 used_bytes = 2; +} + +message GetDiskNumberFromVolumeIDRequest { + // Volume device ID of the volume to get the disk number for. + string volume_id = 1; +} + +message GetDiskNumberFromVolumeIDResponse { + // Corresponding disk number. + uint32 disk_number = 1; +} + +message GetVolumeIDFromTargetPathRequest { + // The target path. + string target_path = 1; +} + +message GetVolumeIDFromTargetPathResponse { + // The volume device ID. + string volume_id = 1; +} + +message GetClosestVolumeIDFromTargetPathRequest { + // The target path. + string target_path = 1; +} + +message GetClosestVolumeIDFromTargetPathResponse { + // The volume device ID. + string volume_id = 1; +} + +message WriteVolumeCacheRequest { + // Volume device ID of the volume to flush the cache. + string volume_id = 1; +} + +message WriteVolumeCacheResponse { + // Intentionally empty. +} diff --git a/docker/iscsiadm b/docker/iscsiadm index cce2b01..56623d7 100755 --- a/docker/iscsiadm +++ b/docker/iscsiadm @@ -2,4 +2,4 @@ # https://engineering.docker.com/2019/07/road-to-containing-iscsi/ -chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin" iscsiadm "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" iscsiadm "${@:1}" diff --git a/docker/mount b/docker/mount index 0f7c17a..ac3281b 100755 --- a/docker/mount +++ b/docker/mount @@ -20,16 +20,18 @@ container_supported_filesystems=( while getopts "t:" opt; do case "$opt" in t) - [[ "${OPTARG,,}" == "zfs" ]] && USE_HOST_MOUNT_TOOLS=1 - [[ "${OPTARG,,}" == "lustre" ]] && USE_HOST_MOUNT_TOOLS=1 - [[ "${OPTARG,,}" == "onedata" ]] && USE_HOST_MOUNT_TOOLS=1 - #(printf '%s\0' "${container_supported_filesystems[@]}" | grep -Fqxz -- "${OPTARG}") || USE_HOST_MOUNT_TOOLS=1 + if [[ "x${USE_HOST_MOUNT_TOOLS}" == "x" ]]; then + [[ "${OPTARG,,}" == "zfs" ]] && USE_HOST_MOUNT_TOOLS=1 + [[ "${OPTARG,,}" == "lustre" ]] && USE_HOST_MOUNT_TOOLS=1 + [[ "${OPTARG,,}" == "onedata" ]] && USE_HOST_MOUNT_TOOLS=1 + #(printf '%s\0' "${container_supported_filesystems[@]}" | grep -Fqxz -- "${OPTARG}") || USE_HOST_MOUNT_TOOLS=1 + fi ;; esac done -if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]];then - chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" mount "${@:1}" +if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then + chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" mount "${@:1}" else - /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" mount "${@:1}" + /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" mount "${@:1}" fi diff --git a/docker/multipath b/docker/multipath index 0a95bc8..3d1d6ee 100755 --- a/docker/multipath +++ b/docker/multipath @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/sbin:/usr/bin" multipath "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" multipath "${@:1}" diff --git a/docker/oneclient b/docker/oneclient index 7b5ef21..0815dee 100755 --- a/docker/oneclient +++ b/docker/oneclient @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" oneclient "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" oneclient "${@:1}" diff --git a/docker/umount b/docker/umount index 9a4b184..fe08061 100755 --- a/docker/umount +++ b/docker/umount @@ -20,16 +20,18 @@ container_supported_filesystems=( while getopts "t:" opt; do case "$opt" in t) - [[ "${OPTARG,,}" == "zfs" ]] && USE_HOST_MOUNT_TOOLS=1 - [[ "${OPTARG,,}" == "lustre" ]] && USE_HOST_MOUNT_TOOLS=1 - [[ "${OPTARG,,}" == "onedata" ]] && USE_HOST_MOUNT_TOOLS=1 - #(printf '%s\0' "${container_supported_filesystems[@]}" | grep -Fqxz -- "${OPTARG}") || USE_HOST_MOUNT_TOOLS=1 + if [[ "x${USE_HOST_MOUNT_TOOLS}" == "x" ]]; then + [[ "${OPTARG,,}" == "zfs" ]] && USE_HOST_MOUNT_TOOLS=1 + [[ "${OPTARG,,}" == "lustre" ]] && USE_HOST_MOUNT_TOOLS=1 + [[ "${OPTARG,,}" == "onedata" ]] && USE_HOST_MOUNT_TOOLS=1 + #(printf '%s\0' "${container_supported_filesystems[@]}" | grep -Fqxz -- "${OPTARG}") || USE_HOST_MOUNT_TOOLS=1 + fi ;; esac done -if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]];then - chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" umount "${@:1}" +if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then + chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" umount "${@:1}" else - /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" umount "${@:1}" + /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" umount "${@:1}" fi diff --git a/docker/zfs b/docker/zfs index bc49b65..4a1f79e 100755 --- a/docker/zfs +++ b/docker/zfs @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" zfs "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" zfs "${@:1}" diff --git a/docker/zpool b/docker/zpool index c68eb0a..07241b2 100755 --- a/docker/zpool +++ b/docker/zpool @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" zpool "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" zpool "${@:1}" diff --git a/docs/storage-class-parameters.md b/docs/storage-class-parameters.md new file mode 100644 index 0000000..d71d47f --- /dev/null +++ b/docs/storage-class-parameters.md @@ -0,0 +1,138 @@ +# Storage Class Parameters + +Some drivers support different settings for volumes. These can be configured via the driver configuration and/or storage +classes. + +## `synology-iscsi` +The `synology-iscsi` driver supports several storage class parameters. Note however that not all parameters/values are +supported for all backing file systems and LUN type. The following options are available: + +### Configure Storage Classes +```yaml +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: synology-iscsi +parameters: + fsType: ext4 + # The following options affect the LUN representing the volume. These options are passed directly to the Synology API. + # The following options are known. + lunTemplate: | + type: BLUN # Btrfs thin provisioning + type: BLUN_THICK # Btrfs thick provisioning + type: THIN # Ext4 thin provisioning + type: ADV # Ext4 thin provisioning with legacy advanced feature set + type: FILE # Ext4 thick provisioning + description: Some Description + + # Only for thick provisioned volumes. Known values: + # 0: Buffered Writes + # 3: Direct Write + direct_io_pattern: 0 + + # Device Attributes. See below for more info + dev_attribs: + - dev_attrib: emulate_tpws + enable: 1 + - ... + + # The following options affect the iSCSI target. These options will be passed directly to the Synology API. + # The following options are known. + targetTemplate: | + has_header_checksum: false + has_data_checksum: false + + # Note that this option requires a compatible filesystem. Use 0 for unlimited sessions. + max_sessions: 0 + multi_sessions: true + max_recv_seg_bytes: 262144 + max_send_seg_bytes: 262144 + + # Use this to disable authentication. To configure authentication see below + auth_type: 0 +``` + +#### About LUN Types +The availability of the different types of LUNs depends on the filesystem used on your Synology volume. For Btrfs volumes +you can use `BLUN` and `BLUN_THICK` volumes. For Ext4 volumes you can use `THIN`, `ADV` or `FILE` volumes. These +correspond to the options available in the UI. + +#### About `dev_attribs` +Most of the LUN options are configured via the `dev_attribs` list. This list can be specified both in the `lunTemplate` +of the global configuration and in the `lunTemplate` of the `StorageClass`. If both lists are present they will be merged +(with the `StorageClass` taking precedence). The following `dev_attribs` are known to work: + +- `emulate_tpws`: Hardware-assisted zeroing +- `emulate_caw`: Hardware-assisted locking +- `emulate_3pc`: Hardware-assisted data transfer +- `emulate_tpu`: Space Reclamation +- `emulate_fua_write`: Enable the FUA iSCSI command (DSM 7+) +- `emulate_sync_cache`: Enable the Sync Cache iSCSI command (DSM 7+) +- `can_snapshot`: Enable snapshots for this volume. Only works for thin provisioned volumes. + +### Configure Snapshot Classes +`synology-iscsi` can also configure different parameters on snapshot classes: + +```yaml +apiVersion: snapshot.storage.k8s.io/v1 +kind: VolumeSnapshotClass +metadata: + name: synology-iscsi-snapshot +parameters: + # This inline yaml object will be passed to the Synology API when creating the snapshot. + lunSnapshotTemplate: | + is_locked: true + + # https://kb.synology.com/en-me/DSM/tutorial/What_is_file_system_consistent_snapshot + # Note that app consistent snapshots require a working Synology Storage Console. Otherwise both values will have + # equivalent behavior. + is_app_consistent: true +... +``` + +Note that it is currently not supported by Synology devices to restore a snapshot onto a different volume. You can +create volumes from snapshots, but you should use the same `StorageClass` as the original volume of the snapshot did. + +### Enabling CHAP Authentication +You can enable CHAP Authentication for `StorageClass`es by supplying an appropriate `StorageClass` secret (see the +[documentation](https://kubernetes-csi.github.io/docs/secrets-and-credentials-storage-class.html) for more details). You +can use the same password for alle volumes of a `StorageClass` or use different passwords per volume. + +```yaml +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: synology-iscsi-chap +parameters: + fsType: ext4 + lunTemplate: | + type: BLUN + description: iSCSI volumes with CHAP Authentication +secrets: + # Use this to configure a single set of credentials for all volumes of this StorageClass + csi.storage.k8s.io/provisioner-secret-name: chap-secret + csi.storage.k8s.io/provisioner-secret-namespace: default + # Use substitutions to use different credentials for volumes based on the PVC + csi.storage.k8s.io/provisioner-secret-name: "${pvc.name}-chap-secret" + csi.storage.k8s.io/provisioner-secret-namespace: "${pvc.namespace}" +... +--- +# Use a secret like this to supply CHAP credentials. +apiVersion: v1 +kind: Secret +metadata: + name: chap-secret +stringData: + # Client Credentials + user: client + password: MySecretPassword + # Mutual CHAP Credentials. If these are specified mutual CHAP will be enabled. + mutualUser: server + mutualPassword: MyOtherPassword +``` + +Note that CHAP authentication will only be enabled if the secret contains a username and password. If e.g. a password is +missing CHAP authentication will not be enabled (but the volume will still be created). You cannot automatically +enable/disable CHAP or change the password after the volume has been created. + +If the secret itself is referenced but not present, the volume will not be created. diff --git a/examples/freenas-api-nfs.yaml b/examples/freenas-api-nfs.yaml index 68084cb..97b8a53 100644 --- a/examples/freenas-api-nfs.yaml +++ b/examples/freenas-api-nfs.yaml @@ -43,6 +43,8 @@ zfs: datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 + + # not supported yet #datasetPermissionsAcls: #- "-m everyone@:full_set:allow" #- "-m u:kube:full_set:allow" diff --git a/examples/freenas-api-smb.yaml b/examples/freenas-api-smb.yaml index 7b45f4c..a8e0a84 100644 --- a/examples/freenas-api-smb.yaml +++ b/examples/freenas-api-smb.yaml @@ -34,9 +34,10 @@ zfs: # "org.freenas:test": "{{ parameters.foo }}" # "org.freenas:test2": "some value" - datasetProperties: - aclmode: restricted - casesensitivity: mixed + # these are managed automatically via the volume creation process when flagged as an smb volume + #datasetProperties: + # aclmode: restricted + # casesensitivity: mixed datasetParentName: tank/k8s/a/vols # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap @@ -47,8 +48,10 @@ zfs: datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 - datasetPermissionsAcls: - - "-m everyone@:full_set:allow" + + # not supported yet in api + #datasetPermissionsAcls: + #- "-m everyone@:full_set:allow" #- "-m u:kube:full_set:allow" smb: diff --git a/examples/freenas-smb.yaml b/examples/freenas-smb.yaml index 6d08b7e..8a2ed4d 100644 --- a/examples/freenas-smb.yaml +++ b/examples/freenas-smb.yaml @@ -46,7 +46,9 @@ zfs: datasetProperties: aclmode: restricted - casesensitivity: mixed + aclinherit: passthrough + acltype: nfsv4 + casesensitivity: insensitive datasetParentName: tank/k8s/a/vols # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap @@ -54,12 +56,41 @@ zfs: detachedSnapshotsDatasetParentName: tank/k8s/a/snaps datasetEnableQuotas: true datasetEnableReservation: false - datasetPermissionsMode: "0777" - datasetPermissionsUser: nobody - datasetPermissionsGroup: nobody + datasetPermissionsMode: "0770" + + # as appropriate create a dedicated user for smb connections + # and set this + datasetPermissionsUser: 65534 + datasetPermissionsGroup: 65534 + + # CORE + #datasetPermissionsAclsBinary: setfacl + + # SCALE + #datasetPermissionsAclsBinary: nfs4xdr_setfacl + + # if using a user other than guest/nobody comment the 'everyone@' acl + # and uncomment the appropriate block below datasetPermissionsAcls: - - "-m everyone@:full_set:allow" - #- "-m u:kube:full_set:allow" + - "-m everyone@:full_set:fd:allow" + + # CORE + # in CORE you cannot have multiple entries for the same principle + # or said differently, they are declarative so using -m will replace + # whatever the current value is for the principle rather than adding a + # entry in the acl list + #- "-m g:builtin_users:full_set:fd:allow" + #- "-m group@:modify_set:fd:allow" + #- "-m owner@:full_set:fd:allow" + + # SCALE + # https://www.truenas.com/community/threads/get-setfacl-on-scale-with-nfsv4-acls.95231/ + # -s replaces everything + # so we put this in specific order to mimic the defaults of SCALE when using the api + #- -s group:builtin_users:full_set:fd:allow + #- -a group:builtin_users:modify_set:fd:allow + #- -a group@:modify_set:fd:allow + #- -a owner@:full_set:fd:allow smb: shareHost: server address @@ -77,7 +108,7 @@ smb: shareAllowedHosts: [] shareDeniedHosts: [] #shareDefaultPermissions: true - shareGuestOk: true + shareGuestOk: false #shareGuestOnly: true #shareShowHiddenFiles: true shareRecycleBin: true diff --git a/examples/local-hostpath.yaml b/examples/local-hostpath.yaml index 7470f95..4c4a9ed 100644 --- a/examples/local-hostpath.yaml +++ b/examples/local-hostpath.yaml @@ -6,5 +6,5 @@ local-hostpath: shareBasePath: "/var/lib/csi-local-hostpath" controllerBasePath: "/var/lib/csi-local-hostpath" dirPermissionsMode: "0777" - dirPermissionsUser: root - dirPermissionsGroup: root + dirPermissionsUser: 0 + dirPermissionsGroup: 0 diff --git a/examples/node-common.yaml b/examples/node-common.yaml index 0fb61e7..1cda789 100644 --- a/examples/node-common.yaml +++ b/examples/node-common.yaml @@ -2,6 +2,9 @@ node: mount: + # predominantly used to facilitate testing + # mount_flags should generally be defined in storage classes, etc + mount_flags: "" # should fsck be executed before mounting the fs checkFilesystem: xfs: @@ -27,3 +30,30 @@ node: # ... btrfs: customOptions: [] + + csiProxy: + # should be left unset in most situation, will be auto-detected + #enabled: true + + # connection attributes can be set to grpc endpoint + # ie: hostname:port, or /some/path, or \\.\pipe\foo + # connection and version will use internal defaults and should generally be left unset + services: + filesystem: + #version: v1 + #connection: + disk: + #version: v1 + #connection: + volume: + #version: v1 + #connection: + smb: + #version: v1 + #connection: + system: + #version: v1alpha1 + #connection: + iscsi: + #version: v1alpha2 + #connection: diff --git a/examples/private.yaml b/examples/private.yaml new file mode 100644 index 0000000..8d2d969 --- /dev/null +++ b/examples/private.yaml @@ -0,0 +1,18 @@ +# +# these SHOULD NOT be used +# they are here for documentation purposes only and are likely to: +# - be removed +# - break things +# + +_private: + csi: + volume: + derivedContext: + # driver left blank is used to auto select + driver: memory # strictly to facilitate testing + #driver: kubernetes + idHash: + strategy: crc16 + #strategy: crc32 + #strategy: md5 diff --git a/examples/synology-iscsi.yaml b/examples/synology-iscsi.yaml index b8cd825..e0515b2 100644 --- a/examples/synology-iscsi.yaml +++ b/examples/synology-iscsi.yaml @@ -10,9 +10,9 @@ httpConnection: session: "democratic-csi" serialize: true -synology: - # choose the proper volume for your system - volume: /volume1 +# Choose the DSM volume this driver operates on. The default value is /volume1. +# synology: +# volume: /volume1 iscsi: targetPortal: "server[:port]" @@ -31,7 +31,12 @@ iscsi: # documented below are several blocks # pick the option appropriate for you based on what your backing fs is and desired features # you do not need to alter dev_attribs under normal circumstances but they may be altered in advanced use-cases + # These options can also be configured per storage-class: + # See https://github.com/democratic-csi/democratic-csi/blob/master/docs/storage-class-parameters.md lunTemplate: + # can be static value or handlebars template + #description: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}-{{ parameters.[csi.storage.k8s.io/pvc/name] }}" + # btrfs thin provisioning type: "BLUN" # tpws = Hardware-assisted zeroing diff --git a/examples/zfs-generic-nfs.yaml b/examples/zfs-generic-nfs.yaml index 54cc8d9..e068c29 100644 --- a/examples/zfs-generic-nfs.yaml +++ b/examples/zfs-generic-nfs.yaml @@ -36,8 +36,8 @@ zfs: datasetEnableQuotas: true datasetEnableReservation: false datasetPermissionsMode: "0777" - datasetPermissionsUser: root - datasetPermissionsGroup: root + datasetPermissionsUser: 0 + datasetPermissionsGroup: 0 #datasetPermissionsAcls: #- "-m everyone@:full_set:allow" #- "-m u:kube:full_set:allow" @@ -48,6 +48,7 @@ nfs: shareStrategy: "setDatasetProperties" shareStrategySetDatasetProperties: properties: + #sharenfs: "rw,no_subtree_check,no_root_squash" sharenfs: "on" # share: "" shareHost: "server address" diff --git a/examples/zfs-generic-smb.yaml b/examples/zfs-generic-smb.yaml new file mode 100644 index 0000000..db60cf3 --- /dev/null +++ b/examples/zfs-generic-smb.yaml @@ -0,0 +1,57 @@ +driver: zfs-generic-smb +sshConnection: + host: server address + port: 22 + username: root + # use either password or key + password: "" + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + ... + -----END RSA PRIVATE KEY----- + +zfs: + # can be used to override defaults if necessary + # the example below is useful for TrueNAS 12 + #cli: + # sudoEnabled: true + # paths: + # zfs: /usr/local/sbin/zfs + # zpool: /usr/local/sbin/zpool + # sudo: /usr/local/bin/sudo + # chroot: /usr/sbin/chroot + + # can be used to set arbitrary values on the dataset/zvol + # can use handlebars templates with the parameters from the storage class/CO + datasetProperties: + #aclmode: restricted + #aclinherit: passthrough + #acltype: nfsv4 + casesensitivity: insensitive + + datasetParentName: tank/k8s/test + # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap + # they may be siblings, but neither should be nested in the other + detachedSnapshotsDatasetParentName: tanks/k8s/test-snapshots + + datasetEnableQuotas: true + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: smbroot + datasetPermissionsGroup: smbroot + + #datasetPermissionsAclsBinary: nfs4_setfacl + #datasetPermissionsAcls: + #- "-m everyone@:full_set:allow" + #- -s group@:modify_set:fd:allow + #- -a owner@:full_set:fd:allow + +smb: + # https://docs.oracle.com/cd/E23824_01/html/821-1448/gayne.html + # https://www.hiroom2.com/2016/05/18/ubuntu-16-04-share-zfs-storage-via-nfs-smb/ + shareStrategy: "setDatasetProperties" + shareStrategySetDatasetProperties: + properties: + sharesmb: "on" + # share: "" + shareHost: "server address" diff --git a/package-lock.json b/package-lock.json index d0ba2ca..a6d0980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,21 @@ { "name": "democratic-csi", - "version": "1.6.3", + "version": "1.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "democratic-csi", - "version": "1.6.3", + "version": "1.7.0", "license": "MIT", "dependencies": { "@grpc/grpc-js": "^1.5.7", "@grpc/proto-loader": "^0.6.0", "@kubernetes/client-node": "^0.16.3", "async-mutex": "^0.3.1", - "axios": "^0.26.1", + "axios": "^0.27.2", "bunyan": "^1.8.15", + "fs-extra": "^10.1.0", "handlebars": "^4.7.7", "js-yaml": "^4.0.0", "lodash": "^4.17.21", @@ -50,19 +51,19 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", - "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.1", - "globals": "^13.9.0", + "espree": "^9.3.2", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { @@ -70,9 +71,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.2.tgz", - "integrity": "sha512-9+89Ne1K8F9u86T+l1yIV2DS+dWHYVK61SsDZN4MFTFehOOaJ4rHxa1cW8Lwdn2/6tOx7N3+SY/vfcjztOHopA==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.7.tgz", + "integrity": "sha512-eBM03pu9hd3VqDQG+kHahiG1x80RGkkqqRb1Pchcwqej/KkAH95gAvKs6laqaHCycYaPK+TKuNQnOz9UXYA8qw==", "dependencies": { "@grpc/proto-loader": "^0.6.4", "@types/node": ">=12.12.47" @@ -82,14 +83,14 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", - "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", "dependencies": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", "long": "^4.0.0", - "protobufjs": "^6.10.0", + "protobufjs": "^6.11.3", "yargs": "^16.2.0" }, "bin": { @@ -189,7 +190,7 @@ "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", @@ -204,12 +205,12 @@ "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -218,27 +219,27 @@ "node_modules/@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@sindresorhus/is": { "version": "4.6.0", @@ -302,9 +303,9 @@ } }, "node_modules/@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" }, "node_modules/@types/minipass": { "version": "3.1.2", @@ -315,9 +316,9 @@ } }, "node_modules/@types/node": { - "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" + "version": "17.0.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz", + "integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw==" }, "node_modules/@types/request": { "version": "2.48.8", @@ -356,9 +357,9 @@ } }, "node_modules/@types/tough-cookie": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", - "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" }, "node_modules/@types/underscore": { "version": "1.11.4", @@ -374,9 +375,9 @@ } }, "node_modules/acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -459,15 +460,15 @@ "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "engines": { "node": ">=0.8" } }, "node_modules/async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" }, "node_modules/async-mutex": { "version": "0.3.2", @@ -478,19 +479,19 @@ } }, "node_modules/async-mutex/node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "engines": { "node": "*" } @@ -501,11 +502,25 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", "dependencies": { - "follow-redirects": "^1.14.8" + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, "node_modules/balanced-match": { @@ -516,7 +531,7 @@ "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dependencies": { "tweetnacl": "^0.14.3" } @@ -559,7 +574,7 @@ "node_modules/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", "engines": { "node": ">=0.10.0" } @@ -615,7 +630,7 @@ "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, "node_modules/chalk": { "version": "4.1.2", @@ -662,7 +677,7 @@ "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "dependencies": { "mimic-response": "^1.0.0" } @@ -693,9 +708,9 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/color-string": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", - "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -712,12 +727,12 @@ "node_modules/color/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/colors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", "engines": { "node": ">=0.1.90" } @@ -743,9 +758,9 @@ } }, "node_modules/compress-brotli": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.6.tgz", - "integrity": "sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.8.tgz", + "integrity": "sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==", "dependencies": { "@types/json-buffer": "~3.0.0", "json-buffer": "~3.0.1" @@ -757,12 +772,12 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/cpu-features": { "version": "0.0.4", @@ -794,7 +809,7 @@ "node_modules/cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", "engines": { "node": ">=0.4.0" } @@ -802,7 +817,7 @@ "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dependencies": { "assert-plus": "^1.0.0" }, @@ -869,7 +884,7 @@ "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "engines": { "node": ">=0.4.0" } @@ -902,7 +917,7 @@ "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -947,12 +962,12 @@ } }, "node_modules/eslint": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz", - "integrity": "sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", + "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.2.1", + "@eslint/eslintrc": "^1.3.0", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -963,14 +978,14 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", + "espree": "^9.3.2", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", - "globals": "^13.6.0", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -979,7 +994,7 @@ "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", @@ -1048,13 +1063,13 @@ } }, "node_modules/espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", "dev": true, "dependencies": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1133,7 +1148,7 @@ "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "engines": [ "node >=0.6.0" ] @@ -1141,7 +1156,7 @@ "node_modules/eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", "engines": { "node": "> 0.1.90" } @@ -1159,13 +1174,13 @@ "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "node_modules/fecha": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", - "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, "node_modules/file-entry-cache": { "version": "6.0.1", @@ -1204,9 +1219,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", "funding": [ { "type": "individual", @@ -1225,7 +1240,7 @@ "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "engines": { "node": "*" } @@ -1243,6 +1258,19 @@ "node": ">= 0.12" } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -1257,7 +1285,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/function-bind": { "version": "1.1.1", @@ -1267,7 +1295,7 @@ "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, "node_modules/get-caller-file": { @@ -1292,20 +1320,20 @@ "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dependencies": { "assert-plus": "^1.0.0" } }, "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, @@ -1329,9 +1357,9 @@ } }, "node_modules/globals": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", - "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1344,9 +1372,9 @@ } }, "node_modules/got": { - "version": "11.8.3", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz", - "integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==", + "version": "11.8.5", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", + "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==", "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -1367,6 +1395,11 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, "node_modules/handlebars": { "version": "4.7.7", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", @@ -1390,7 +1423,7 @@ "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", "engines": { "node": ">=4" } @@ -1436,7 +1469,7 @@ "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -1495,7 +1528,7 @@ "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "engines": { "node": ">=0.8.19" @@ -1512,7 +1545,7 @@ "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1537,9 +1570,9 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", "dependencies": { "has": "^1.0.3" }, @@ -1550,7 +1583,7 @@ "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -1590,12 +1623,12 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isomorphic-ws": { "version": "4.0.1", @@ -1608,7 +1641,7 @@ "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "node_modules/jose": { "version": "2.0.5", @@ -1638,7 +1671,7 @@ "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "node_modules/json-buffer": { "version": "3.0.1", @@ -1658,13 +1691,24 @@ "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } }, "node_modules/jsonpath-plus": { "version": "0.19.0", @@ -1689,11 +1733,11 @@ } }, "node_modules/keyv": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.2.2.tgz", - "integrity": "sha512-uYS0vKTlBIjNCAUqrjlxmruxOEiZxZIHXyp32sdcGmP+ukFrmWUnE//RcPXJH3Vxrni1H2gsQbjHE0bH7MtMQQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.3.0.tgz", + "integrity": "sha512-C30Un9+63J0CsR7Wka5quXKqYZsT6dcRQ2aOwGcSc3RiQ4HGWpTAHlCA+puNfw2jA/s11EsxA1nCXgZRuRKMQQ==", "dependencies": { - "compress-brotli": "^1.3.6", + "compress-brotli": "^1.3.8", "json-buffer": "3.0.1" } }, @@ -1723,7 +1767,7 @@ "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -1757,9 +1801,9 @@ } }, "node_modules/lru-cache": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.0.tgz", - "integrity": "sha512-AmXqneQZL3KZMIgBpaPTeI6pfwh+xQ2vutMsyqOu1TBdEXFZgpG/80wuJ531w2ZN7TI0/oc8CPxzh/DKQudZqg==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz", + "integrity": "sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==", "engines": { "node": ">=12" } @@ -1861,9 +1905,9 @@ } }, "node_modules/moment": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", - "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", "optional": true, "engines": { "node": "*" @@ -1882,7 +1926,7 @@ "node_modules/mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", "optional": true, "dependencies": { "mkdirp": "~0.5.1", @@ -1896,7 +1940,7 @@ "node_modules/mv/node_modules/glob": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", "optional": true, "dependencies": { "inflight": "^1.0.4", @@ -1912,7 +1956,7 @@ "node_modules/mv/node_modules/rimraf": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", "optional": true, "dependencies": { "glob": "^6.0.1" @@ -1922,21 +1966,21 @@ } }, "node_modules/nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", "optional": true }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "node_modules/ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", "optional": true, "bin": { "ncp": "bin/ncp" @@ -1996,7 +2040,7 @@ "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dependencies": { "wrappy": "1" } @@ -2094,7 +2138,7 @@ "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "engines": { "node": ">=0.10.0" } @@ -2115,7 +2159,7 @@ "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -2127,26 +2171,26 @@ } }, "node_modules/prompt": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.2.2.tgz", - "integrity": "sha512-XNXhNv3PUHJDcDkISpCwSJxtw9Bor4FZnlMUDW64N/KCPdxhfVlpD5+YUXI/Z8a9QWmOhs9KSiVtR8nzPS0BYA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.3.0.tgz", + "integrity": "sha512-ZkaRWtaLBZl7KKAKndKYUL8WqNT+cQHKRZnT4RYYms48jQkFw3rrBL+/N5K/KtdEveHkxs982MX2BkDKub2ZMg==", "dependencies": { "@colors/colors": "1.5.0", - "async": "~0.9.0", + "async": "3.2.3", "read": "1.0.x", "revalidator": "0.1.x", "winston": "2.x" }, "engines": { - "node": ">= 0.6.6" + "node": ">= 6.0.0" } }, "node_modules/prompt/node_modules/winston": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", - "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", + "integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", "dependencies": { - "async": "~1.0.0", + "async": "^3.2.3", "colors": "1.0.x", "cycle": "1.0.x", "eyes": "0.1.x", @@ -2157,15 +2201,10 @@ "node": ">= 0.10.0" } }, - "node_modules/prompt/node_modules/winston/node_modules/async": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", - "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" - }, "node_modules/protobufjs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", - "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", + "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -2231,7 +2270,7 @@ "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "dependencies": { "mute-stream": "~0.0.4" }, @@ -2255,7 +2294,7 @@ "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "dependencies": { "resolve": "^1.1.6" }, @@ -2331,7 +2370,7 @@ "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "engines": { "node": ">=0.10.0" } @@ -2377,15 +2416,15 @@ "node_modules/revalidator": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", - "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=", + "integrity": "sha512-xcBILK2pA9oh4SiinPEZfhP8HfrB/ha+a2fTMyl7Om2WjlDVrOQy99N2MXXlUHqGJz4qEu2duXxHJjDWuK/0xg==", "engines": { "node": ">= 0.4.0" } }, "node_modules/rfc4648": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.1.tgz", - "integrity": "sha512-60e/YWs2/D3MV1ErdjhJHcmlgnyLUiG4X/14dgsfm9/zmCWLN16xI6YqJYSCd/OANM7bUNzJqPY5B8/02S9Ibw==" + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.2.tgz", + "integrity": "sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg==" }, "node_modules/rimraf": { "version": "3.0.2", @@ -2440,17 +2479,28 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", - "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dependencies": { - "lru-cache": "^7.4.0" + "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" }, "engines": { - "node": "^10.0.0 || ^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, "node_modules/shebang-command": { @@ -2510,9 +2560,9 @@ } }, "node_modules/ssh2": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.9.0.tgz", - "integrity": "sha512-rhhIZT0eMPvCBSOG8CpqZZ7gre2vgXaIqmb3Jb83t88rjsxIsFzDanqBJM9Ns8BmP1835A5IbQ199io4EUZwOA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.10.0.tgz", + "integrity": "sha512-OnKAAmf4j8wCRrXXZv3Tp5lCZkLJZtgZbn45ELiShCg27djDQ3XFGvIzuGsIsf4hdHslP+VdhA9BhUQdTdfd9w==", "hasInstallScript": true, "dependencies": { "asn1": "^0.2.4", @@ -2761,9 +2811,9 @@ } }, "node_modules/uglify-js": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.3.tgz", - "integrity": "sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.0.tgz", + "integrity": "sha512-FEikl6bR30n0T3amyBh3LoiBdqHRy/f4H80+My34HOesOKyHfOsxAPAxOoqC0JUnC1amnO0IwkYC3sko51caSw==", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -2773,9 +2823,17 @@ } }, "node_modules/underscore": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz", - "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==" + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz", + "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==" + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } }, "node_modules/uri-js": { "version": "4.4.1", @@ -2864,11 +2922,6 @@ "node": ">= 6.4.0" } }, - "node_modules/winston/node_modules/async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" - }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -2905,9 +2958,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", + "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", "engines": { "node": ">=8.3.0" }, @@ -2938,9 +2991,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.0.tgz", - "integrity": "sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA==", + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -2980,40 +3033,40 @@ } }, "@eslint/eslintrc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", - "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.1", - "globals": "^13.9.0", + "espree": "^9.3.2", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "@grpc/grpc-js": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.2.tgz", - "integrity": "sha512-9+89Ne1K8F9u86T+l1yIV2DS+dWHYVK61SsDZN4MFTFehOOaJ4rHxa1cW8Lwdn2/6tOx7N3+SY/vfcjztOHopA==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.7.tgz", + "integrity": "sha512-eBM03pu9hd3VqDQG+kHahiG1x80RGkkqqRb1Pchcwqej/KkAH95gAvKs6laqaHCycYaPK+TKuNQnOz9UXYA8qw==", "requires": { "@grpc/proto-loader": "^0.6.4", "@types/node": ">=12.12.47" } }, "@grpc/proto-loader": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", - "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", "requires": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", "long": "^4.0.0", - "protobufjs": "^6.10.0", + "protobufjs": "^6.11.3", "yargs": "^16.2.0" }, "dependencies": { @@ -3099,7 +3152,7 @@ "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" }, "@protobufjs/base64": { "version": "1.1.2", @@ -3114,12 +3167,12 @@ "@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" }, "@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -3128,27 +3181,27 @@ "@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" }, "@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" }, "@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" }, "@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" }, "@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "@sindresorhus/is": { "version": "4.6.0", @@ -3203,9 +3256,9 @@ } }, "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" }, "@types/minipass": { "version": "3.1.2", @@ -3216,9 +3269,9 @@ } }, "@types/node": { - "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" + "version": "17.0.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz", + "integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw==" }, "@types/request": { "version": "2.48.8", @@ -3257,9 +3310,9 @@ } }, "@types/tough-cookie": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", - "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" }, "@types/underscore": { "version": "1.11.4", @@ -3275,9 +3328,9 @@ } }, "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", "dev": true }, "acorn-jsx": { @@ -3336,12 +3389,12 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" }, "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" }, "async-mutex": { "version": "0.3.2", @@ -3352,21 +3405,21 @@ }, "dependencies": { "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" } } }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" }, "aws4": { "version": "1.11.0", @@ -3374,11 +3427,24 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", "requires": { - "follow-redirects": "^1.14.8" + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } } }, "balanced-match": { @@ -3389,7 +3455,7 @@ "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "requires": { "tweetnacl": "^0.14.3" } @@ -3423,7 +3489,7 @@ "byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==" }, "cacheable-lookup": { "version": "5.0.4", @@ -3463,7 +3529,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, "chalk": { "version": "4.1.2", @@ -3498,7 +3564,7 @@ "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "requires": { "mimic-response": "^1.0.0" } @@ -3523,7 +3589,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" } } }, @@ -3541,9 +3607,9 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "color-string": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", - "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "requires": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -3552,7 +3618,7 @@ "colors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==" }, "colorspace": { "version": "1.1.4", @@ -3572,9 +3638,9 @@ } }, "compress-brotli": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.6.tgz", - "integrity": "sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.8.tgz", + "integrity": "sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==", "requires": { "@types/json-buffer": "~3.0.0", "json-buffer": "~3.0.1" @@ -3583,12 +3649,12 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "cpu-features": { "version": "0.0.4", @@ -3613,12 +3679,12 @@ "cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==" }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "requires": { "assert-plus": "^1.0.0" } @@ -3661,7 +3727,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "doctrine": { "version": "3.0.0", @@ -3684,7 +3750,7 @@ "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -3720,12 +3786,12 @@ "dev": true }, "eslint": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz", - "integrity": "sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", + "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.1", + "@eslint/eslintrc": "^1.3.0", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -3736,14 +3802,14 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", + "espree": "^9.3.2", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", - "globals": "^13.6.0", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -3752,7 +3818,7 @@ "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", @@ -3796,13 +3862,13 @@ "dev": true }, "espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", "dev": true, "requires": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" } }, @@ -3860,12 +3926,12 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" }, "eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==" }, "fast-deep-equal": { "version": "3.1.3", @@ -3880,13 +3946,13 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "fecha": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", - "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, "file-entry-cache": { "version": "6.0.1", @@ -3919,14 +3985,14 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" }, "form-data": { "version": "2.5.1", @@ -3938,6 +4004,16 @@ "mime-types": "^2.1.12" } }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -3949,7 +4025,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "function-bind": { "version": "1.1.1", @@ -3959,7 +4035,7 @@ "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, "get-caller-file": { @@ -3975,20 +4051,20 @@ "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "requires": { "assert-plus": "^1.0.0" } }, "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } @@ -4003,18 +4079,18 @@ } }, "globals": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", - "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, "got": { - "version": "11.8.3", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz", - "integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==", + "version": "11.8.5", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", + "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==", "requires": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -4029,6 +4105,11 @@ "responselike": "^2.0.0" } }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, "handlebars": { "version": "4.7.7", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", @@ -4044,7 +4125,7 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" }, "har-validator": { "version": "5.1.5", @@ -4077,7 +4158,7 @@ "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -4117,7 +4198,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, "indent-string": { @@ -4128,7 +4209,7 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -4150,9 +4231,9 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", "requires": { "has": "^1.0.3" } @@ -4160,7 +4241,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true }, "is-fullwidth-code-point": { @@ -4185,12 +4266,12 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "isomorphic-ws": { "version": "4.0.1", @@ -4201,7 +4282,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "jose": { "version": "2.0.5", @@ -4222,7 +4303,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "json-buffer": { "version": "3.0.1", @@ -4242,13 +4323,22 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } }, "jsonpath-plus": { "version": "0.19.0", @@ -4267,11 +4357,11 @@ } }, "keyv": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.2.2.tgz", - "integrity": "sha512-uYS0vKTlBIjNCAUqrjlxmruxOEiZxZIHXyp32sdcGmP+ukFrmWUnE//RcPXJH3Vxrni1H2gsQbjHE0bH7MtMQQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.3.0.tgz", + "integrity": "sha512-C30Un9+63J0CsR7Wka5quXKqYZsT6dcRQ2aOwGcSc3RiQ4HGWpTAHlCA+puNfw2jA/s11EsxA1nCXgZRuRKMQQ==", "requires": { - "compress-brotli": "^1.3.6", + "compress-brotli": "^1.3.8", "json-buffer": "3.0.1" } }, @@ -4298,7 +4388,7 @@ "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" }, "lodash.merge": { "version": "4.6.2", @@ -4329,9 +4419,9 @@ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, "lru-cache": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.0.tgz", - "integrity": "sha512-AmXqneQZL3KZMIgBpaPTeI6pfwh+xQ2vutMsyqOu1TBdEXFZgpG/80wuJ531w2ZN7TI0/oc8CPxzh/DKQudZqg==" + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz", + "integrity": "sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==" }, "make-error": { "version": "1.3.6", @@ -4406,9 +4496,9 @@ } }, "moment": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", - "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", "optional": true }, "ms": { @@ -4424,7 +4514,7 @@ "mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", "optional": true, "requires": { "mkdirp": "~0.5.1", @@ -4435,7 +4525,7 @@ "glob": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", "optional": true, "requires": { "inflight": "^1.0.4", @@ -4448,7 +4538,7 @@ "rimraf": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", "optional": true, "requires": { "glob": "^6.0.1" @@ -4457,21 +4547,21 @@ } }, "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", "optional": true }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", "optional": true }, "neo-async": { @@ -4510,7 +4600,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "requires": { "wrappy": "1" } @@ -4586,7 +4676,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, "path-key": { "version": "3.1.1", @@ -4601,7 +4691,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "prelude-ls": { "version": "1.2.1", @@ -4610,43 +4700,36 @@ "dev": true }, "prompt": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.2.2.tgz", - "integrity": "sha512-XNXhNv3PUHJDcDkISpCwSJxtw9Bor4FZnlMUDW64N/KCPdxhfVlpD5+YUXI/Z8a9QWmOhs9KSiVtR8nzPS0BYA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.3.0.tgz", + "integrity": "sha512-ZkaRWtaLBZl7KKAKndKYUL8WqNT+cQHKRZnT4RYYms48jQkFw3rrBL+/N5K/KtdEveHkxs982MX2BkDKub2ZMg==", "requires": { "@colors/colors": "1.5.0", - "async": "~0.9.0", + "async": "3.2.3", "read": "1.0.x", "revalidator": "0.1.x", "winston": "2.x" }, "dependencies": { "winston": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", - "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", + "integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", "requires": { - "async": "~1.0.0", + "async": "^3.2.3", "colors": "1.0.x", "cycle": "1.0.x", "eyes": "0.1.x", "isstream": "0.1.x", "stack-trace": "0.0.x" - }, - "dependencies": { - "async": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", - "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" - } } } } }, "protobufjs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", - "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", + "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -4695,7 +4778,7 @@ "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "requires": { "mute-stream": "~0.0.4" } @@ -4713,7 +4796,7 @@ "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "requires": { "resolve": "^1.1.6" } @@ -4771,7 +4854,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, "resolve": { "version": "1.22.0", @@ -4805,12 +4888,12 @@ "revalidator": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", - "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=" + "integrity": "sha512-xcBILK2pA9oh4SiinPEZfhP8HfrB/ha+a2fTMyl7Om2WjlDVrOQy99N2MXXlUHqGJz4qEu2duXxHJjDWuK/0xg==" }, "rfc4648": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.1.tgz", - "integrity": "sha512-60e/YWs2/D3MV1ErdjhJHcmlgnyLUiG4X/14dgsfm9/zmCWLN16xI6YqJYSCd/OANM7bUNzJqPY5B8/02S9Ibw==" + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.2.tgz", + "integrity": "sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg==" }, "rimraf": { "version": "3.0.2", @@ -4842,11 +4925,21 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", - "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "requires": { - "lru-cache": "^7.4.0" + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + } } }, "shebang-command": { @@ -4891,9 +4984,9 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "ssh2": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.9.0.tgz", - "integrity": "sha512-rhhIZT0eMPvCBSOG8CpqZZ7gre2vgXaIqmb3Jb83t88rjsxIsFzDanqBJM9Ns8BmP1835A5IbQ199io4EUZwOA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.10.0.tgz", + "integrity": "sha512-OnKAAmf4j8wCRrXXZv3Tp5lCZkLJZtgZbn45ELiShCg27djDQ3XFGvIzuGsIsf4hdHslP+VdhA9BhUQdTdfd9w==", "requires": { "asn1": "^0.2.4", "bcrypt-pbkdf": "^1.0.2", @@ -5073,15 +5166,20 @@ "dev": true }, "uglify-js": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.3.tgz", - "integrity": "sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.0.tgz", + "integrity": "sha512-FEikl6bR30n0T3amyBh3LoiBdqHRy/f4H80+My34HOesOKyHfOsxAPAxOoqC0JUnC1amnO0IwkYC3sko51caSw==", "optional": true }, "underscore": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz", - "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==" + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz", + "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==" + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" }, "uri-js": { "version": "4.4.1", @@ -5140,13 +5238,6 @@ "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.5.0" - }, - "dependencies": { - "async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" - } } }, "winston-transport": { @@ -5186,9 +5277,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", + "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", "requires": {} }, "y18n": { @@ -5202,9 +5293,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.0.tgz", - "integrity": "sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA==", + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", diff --git a/package.json b/package.json index e8e807a..4d16cb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "democratic-csi", - "version": "1.6.3", + "version": "1.7.0", "description": "kubernetes csi driver framework", "main": "bin/democratic-csi", "scripts": { @@ -22,8 +22,9 @@ "@grpc/proto-loader": "^0.6.0", "@kubernetes/client-node": "^0.16.3", "async-mutex": "^0.3.1", - "axios": "^0.26.1", + "axios": "^0.27.2", "bunyan": "^1.8.15", + "fs-extra": "^10.1.0", "handlebars": "^4.7.7", "js-yaml": "^4.0.0", "lodash": "^4.17.21", diff --git a/src/driver/controller-client-common/index.js b/src/driver/controller-client-common/index.js index 1297b8f..a211888 100644 --- a/src/driver/controller-client-common/index.js +++ b/src/driver/controller-client-common/index.js @@ -3,6 +3,8 @@ const { CsiBaseDriver } = require("../index"); const { GrpcError, grpc } = require("../../utils/grpc"); const cp = require("child_process"); const fs = require("fs"); +const fse = require("fs-extra"); +const path = require("path"); const semver = require("semver"); /** @@ -230,9 +232,14 @@ class ControllerClientCommonDriver extends CsiBaseDriver { } async getDirectoryUsage(path) { - let result = await this.exec("du", ["-s", "--block-size=1", path]); - let size = result.stdout.split("\t", 1)[0]; - return size; + if (this.getNodeIsWindows()) { + this.ctx.logger.warn("du not implemented on windows"); + return 0; + } else { + let result = await this.exec("du", ["-s", "--block-size=1", path]); + let size = result.stdout.split("\t", 1)[0]; + return size; + } } exec(command, args, options = {}) { @@ -297,20 +304,39 @@ class ControllerClientCommonDriver extends CsiBaseDriver { } async cloneDir(source_path, target_path) { - await this.exec("mkdir", ["-p", target_path]); + if (this.getNodeIsWindows()) { + fse.copySync( + this.stripTrailingSlash(source_path), + this.stripTrailingSlash(target_path), + { + overwrite: true, + dereference: true, + preserveTimestamps: true, + //errorOnExist: true, + } + ); + } else { + await this.createDir(target_path); - /** - * trailing / is important - * rsync -a /mnt/storage/s/foo/ /mnt/storage/v/PVC-111/ - */ - await this.exec("rsync", [ - "-a", - this.stripTrailingSlash(source_path) + "/", - this.stripTrailingSlash(target_path) + "/", - ]); + /** + * trailing / is important + * rsync -a /mnt/storage/s/foo/ /mnt/storage/v/PVC-111/ + */ + await this.exec("rsync", [ + "-a", + this.stripTrailingSlash(source_path) + "/", + this.stripTrailingSlash(target_path) + "/", + ]); + } } async getAvailableSpaceAtPath(path) { + // https://www.npmjs.com/package/diskusage + // https://www.npmjs.com/package/check-disk-space + if (this.getNodeIsWindows()) { + this.ctx.logger.warn("df not implemented on windows"); + return 0; + } //df --block-size=1 --output=avail /mnt/storage/ // Avail //1481334328 @@ -325,11 +351,14 @@ class ControllerClientCommonDriver extends CsiBaseDriver { } async createDir(path) { - await this.exec("mkdir", ["-p", path]); + fs.mkdirSync(path, { + recursive: true, + mode: "755", + }); } async deleteDir(path) { - await this.exec("rm", ["-rf", path]); + fs.rmSync(path, { recursive: true, force: true }); return; @@ -346,7 +375,40 @@ class ControllerClientCommonDriver extends CsiBaseDriver { } async directoryExists(path) { - return fs.existsSync(path); + let r; + r = fs.existsSync(path); + if (!r) { + return r; + } + + if (!fs.statSync(path).isDirectory()) { + throw new Error(`path [${path}] exists but is not a directory`); + } + + return true; + } + + /** + * Have to be careful with the logic here as the controller could be running + * on win32 for *-client vs local-hostpath + * + * @param {*} path + * @returns + */ + async normalizePath(path) { + if (process.platform == "win32") { + return await this.noramlizePathWin32(path); + } else { + return await this.normalizePathPosix(path); + } + } + + async normalizePathPosix(p) { + return p.replaceAll(path.win32.sep, path.posix.sep); + } + + async noramlizePathWin32(p) { + return p.replaceAll(path.posix.sep, path.win32.sep); } /** @@ -441,7 +503,7 @@ class ControllerClientCommonDriver extends CsiBaseDriver { //let volume_content_source_volume_id; // create target dir - response = await driver.exec("mkdir", ["-p", volume_path]); + await driver.createDir(volume_path); // create dataset if (volume_content_source) { @@ -476,7 +538,7 @@ class ControllerClientCommonDriver extends CsiBaseDriver { } driver.ctx.logger.debug("controller source path: %s", source_path); - response = await driver.cloneDir(source_path, volume_path); + await driver.cloneDir(source_path, volume_path); } // set mode @@ -486,10 +548,7 @@ class ControllerClientCommonDriver extends CsiBaseDriver { this.options[config_key].dirPermissionsMode, volume_path ); - response = await driver.exec("chmod", [ - this.options[config_key].dirPermissionsMode, - volume_path, - ]); + fs.chmodSync(volume_path, this.options[config_key].dirPermissionsMode); } // set ownership @@ -503,16 +562,20 @@ class ControllerClientCommonDriver extends CsiBaseDriver { this.options[config_key].dirPermissionsGroup, volume_path ); - response = await driver.exec("chown", [ - (this.options[config_key].dirPermissionsUser - ? this.options[config_key].dirPermissionsUser - : "") + - ":" + - (this.options[config_key].dirPermissionsGroup - ? this.options[config_key].dirPermissionsGroup - : ""), - volume_path, - ]); + if (this.getNodeIsWindows()) { + driver.ctx.logger.warn("chown not implemented on windows"); + } else { + await driver.exec("chown", [ + (this.options[config_key].dirPermissionsUser + ? this.options[config_key].dirPermissionsUser + : "") + + ":" + + (this.options[config_key].dirPermissionsGroup + ? this.options[config_key].dirPermissionsGroup + : ""), + volume_path, + ]); + } } let volume_context = driver.getVolumeContext(name); diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index f1e7c0a..be9fd40 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -4,10 +4,81 @@ const https = require("https"); const { axios_request, stringify } = require("../../../utils/general"); const Mutex = require("async-mutex").Mutex; const registry = require("../../../utils/registry"); +const { GrpcError, grpc } = require("../../../utils/grpc"); const USER_AGENT = "democratic-csi"; const __REGISTRY_NS__ = "SynologyHttpClient"; +SYNO_ERRORS = { + 400: { + status: grpc.status.UNAUTHENTICATED, + message: "Failed to authenticate to the Synology DSM.", + }, + 407: { + status: grpc.status.UNAUTHENTICATED, + message: + "IP has been blocked to the Synology DSM due to too many failed attempts.", + }, + 18990002: { + status: grpc.status.RESOURCE_EXHAUSTED, + message: "The synology volume is out of disk space.", + }, + 18990318: { + status: grpc.status.INVALID_ARGUMENT, + message: + "The requested lun type is incompatible with the Synology filesystem.", + }, + 18990538: { + status: grpc.status.ALREADY_EXISTS, + message: "A LUN with this name already exists.", + }, + 18990541: { + status: grpc.status.RESOURCE_EXHAUSTED, + message: "The maximum number of LUNS has been reached.", + }, + 18990542: { + status: grpc.status.RESOURCE_EXHAUSTED, + message: "The maximum number if iSCSI target has been reached.", + }, + 18990708: { + status: grpc.status.INVALID_ARGUMENT, + message: "Bad target auth info.", + }, + 18990744: { + status: grpc.status.ALREADY_EXISTS, + message: "An iSCSI target with this name already exists.", + }, + 18990532: { status: grpc.status.NOT_FOUND, message: "No such snapshot." }, + 18990500: { status: grpc.status.INVALID_ARGUMENT, message: "Bad LUN type" }, + 18990543: { + status: grpc.status.RESOURCE_EXHAUSTED, + message: "Maximum number of snapshots reached.", + }, + 18990635: { + status: grpc.status.INVALID_ARGUMENT, + message: "Invalid ioPolicy.", + }, +}; + +class SynologyError extends GrpcError { + constructor(code, httpCode = undefined) { + super(0, ""); + this.synoCode = code; + this.httpCode = httpCode; + if (code > 0) { + const error = SYNO_ERRORS[code]; + this.code = error && error.status ? error.status : grpc.status.UNKNOWN; + this.message = + error && error.message + ? error.message + : `An unknown error occurred when executing a synology command (code = ${code}).`; + } else { + this.code = grpc.status.UNKNOWN; + this.message = `The synology webserver returned a status code ${httpCode}`; + } + } +} + class SynologyHttpClient { constructor(options = {}) { this.options = JSON.parse(JSON.stringify(options)); @@ -44,32 +115,49 @@ class SynologyHttpClient { } log_response(error, response, body, options) { - let prop; - let val; - - prop = "auth.username"; - val = _.get(options, prop, false); - if (val) { - _.set(options, prop, "redacted"); + const cleansedBody = JSON.parse(stringify(body)); + const cleansedOptions = JSON.parse(stringify(options)); + // This function handles arrays and objects + function recursiveCleanse(obj) { + for (const k in obj) { + if (typeof obj[k] == "object" && obj[k] !== null) { + recursiveCleanse(obj[k]); + } else { + if ( + [ + "account", + "passwd", + "username", + "password", + "_sid", + "sid", + "Authorization", + "authorization", + "user", + "mutual_user", + "mutual_password", + ].includes(k) + ) { + obj[k] = "redacted"; + } + } + } } + recursiveCleanse(cleansedBody); + recursiveCleanse(cleansedOptions); - prop = "auth.password"; - val = _.get(options, prop, false); - if (val) { - _.set(options, prop, "redacted"); - } + delete cleansedOptions.httpAgent; + delete cleansedOptions.httpsAgent; - prop = "headers.Authorization"; - val = _.get(options, prop, false); - if (val) { - _.set(options, prop, "redacted"); - } - - this.logger.debug("SYNOLOGY HTTP REQUEST: " + stringify(options)); + this.logger.debug("SYNOLOGY HTTP REQUEST: " + stringify(cleansedOptions)); this.logger.debug("SYNOLOGY HTTP ERROR: " + error); - this.logger.debug("SYNOLOGY HTTP STATUS: " + response.statusCode); - this.logger.debug("SYNOLOGY HTTP HEADERS: " + stringify(response.headers)); - this.logger.debug("SYNOLOGY HTTP BODY: " + stringify(body)); + this.logger.debug( + "SYNOLOGY HTTP STATUS: " + _.get(response, "statusCode", "") + ); + this.logger.debug( + "SYNOLOGY HTTP HEADERS: " + stringify(_.get(response, "headers", "")) + ); + this.logger.debug("SYNOLOGY HTTP BODY: " + stringify(cleansedBody)); } async do_request(method, path, data = {}, options = {}) { @@ -149,7 +237,7 @@ class SynologyHttpClient { } if (response.statusCode > 299 || response.statusCode < 200) { - reject(response); + reject(new SynologyError(null, response.statusCode)); } if (response.body.success === false) { @@ -157,7 +245,9 @@ class SynologyHttpClient { if (response.body.error.code == 119 && sid == client.sid) { client.sid = null; } - reject(response); + reject( + new SynologyError(response.body.error.code, response.statusCode) + ); } resolve(response); @@ -293,19 +383,19 @@ class SynologyHttpClient { return snapshots; } - async GetSnapshotByLunIDAndName(lun_id, name) { + async GetSnapshotByLunUUIDAndName(lun_uuid, name) { const get_snapshot_info = { - lid: lun_id, //check? - api: "SYNO.Core.Storage.iSCSILUN", - method: "load_snapshot", + api: "SYNO.Core.ISCSI.LUN", + method: "list_snapshot", version: 1, + src_lun_uuid: JSON.stringify(lun_uuid), }; let response = await this.do_request("GET", "entry.cgi", get_snapshot_info); - if (response.body.data) { - let snapshot = response.body.data.find((i) => { - return i.desc == name; + if (response.body.data.snapshots) { + let snapshot = response.body.data.snapshots.find((i) => { + return i.description == name; }); if (snapshot) { @@ -314,18 +404,18 @@ class SynologyHttpClient { } } - async GetSnapshotByLunIDAndSnapshotUUID(lun_id, snapshot_uuid) { + async GetSnapshotByLunUUIDAndSnapshotUUID(lun_uuid, snapshot_uuid) { const get_snapshot_info = { - lid: lun_id, //check? - api: "SYNO.Core.Storage.iSCSILUN", - method: "load_snapshot", + api: "SYNO.Core.ISCSI.LUN", + method: "list_snapshot", version: 1, + src_lun_uuid: JSON.stringify(lun_uuid), }; let response = await this.do_request("GET", "entry.cgi", get_snapshot_info); - if (response.body.data) { - let snapshot = response.body.data.find((i) => { + if (response.body.data.snapshots) { + let snapshot = response.body.data.snapshots.find((i) => { return i.uuid == snapshot_uuid; }); @@ -412,7 +502,7 @@ class SynologyHttpClient { response = await this.do_request("GET", "entry.cgi", iscsi_lun_create); return response.body.data.uuid; } catch (err) { - if ([18990538].includes(err.body.error.code)) { + if (err.synoCode === 18990538) { response = await this.do_request("GET", "entry.cgi", lun_list); let lun = response.body.data.luns.find((i) => { return i.name == iscsi_lun_create.name; @@ -503,7 +593,7 @@ class SynologyHttpClient { return response.body.data.target_id; } catch (err) { - if ([18990744].includes(err.body.error.code)) { + if (err.synoCode === 18990744) { //do lookup const iscsi_target_list = { api: "SYNO.Core.ISCSI.Target", @@ -549,7 +639,7 @@ class SynologyHttpClient { /** * 18990710 = non-existant */ - //if (![18990710].includes(err.body.error.code)) { + //if (err.synoCode !== 18990710) { throw err; //} } @@ -572,20 +662,34 @@ class SynologyHttpClient { ); } - async CreateClonedVolume(src_lun_uuid, dst_lun_name) { + async CreateClonedVolume( + src_lun_uuid, + dst_lun_name, + dst_location, + description + ) { const create_cloned_volume = { api: "SYNO.Core.ISCSI.LUN", version: 1, method: "clone", src_lun_uuid: JSON.stringify(src_lun_uuid), // src lun uuid dst_lun_name: dst_lun_name, // dst lun name + dst_location: dst_location, is_same_pool: true, // always true? string? clone_type: "democratic-csi", // check }; + if (description) { + create_cloned_volume.description = description; + } return await this.do_request("GET", "entry.cgi", create_cloned_volume); } - async CreateVolumeFromSnapshot(src_lun_uuid, snapshot_uuid, cloned_lun_name) { + async CreateVolumeFromSnapshot( + src_lun_uuid, + snapshot_uuid, + cloned_lun_name, + description + ) { const create_volume_from_snapshot = { api: "SYNO.Core.ISCSI.LUN", version: 1, @@ -595,6 +699,9 @@ class SynologyHttpClient { cloned_lun_name: cloned_lun_name, // cloned lun name clone_type: "democratic-csi", // check }; + if (description) { + create_volume_from_snapshot.description = description; + } return await this.do_request( "GET", "entry.cgi", diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js index 6db629f..7ce5f18 100644 --- a/src/driver/controller-synology/index.js +++ b/src/driver/controller-synology/index.js @@ -1,9 +1,12 @@ +const _ = require("lodash"); const { CsiBaseDriver } = require("../index"); +const GeneralUtils = require("../../utils/general"); const { GrpcError, grpc } = require("../../utils/grpc"); +const Handlebars = require("handlebars"); const registry = require("../../utils/registry"); const SynologyHttpClient = require("./http").SynologyHttpClient; const semver = require("semver"); -const sleep = require("../../utils/general").sleep; +const yaml = require("js-yaml"); const __REGISTRY_NS__ = "ControllerSynologyDriver"; @@ -142,6 +145,37 @@ class ControllerSynologyDriver extends CsiBaseDriver { } } + getObjectFromDevAttribs(list = []) { + if (!list) { + return {}; + } + return list.reduce( + (obj, item) => Object.assign(obj, { [item.dev_attrib]: item.enable }), + {} + ); + } + + getDevAttribsFromObject(obj, keepNull = false) { + return Object.entries(obj) + .filter((e) => keepNull || e[1] != null) + .map((e) => ({ dev_attrib: e[0], enable: e[1] })); + } + + parseParameterYamlData(data, fieldHint = "") { + try { + return yaml.load(data); + } catch { + if (err instanceof yaml.YAMLException) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `${fieldHint} not a valid YAML document.`.trim() + ); + } else { + throw err; + } + } + } + buildIscsiName(name) { let iscsiName = name; if (this.options.iscsi.namePrefix) { @@ -155,6 +189,25 @@ class ControllerSynologyDriver extends CsiBaseDriver { return iscsiName.toLowerCase(); } + /** + * Returns the value for the 'location' parameter indicating on which volume + * a LUN is to be created. + * + * @param {Object} parameters - Parameters received from a StorageClass + * @param {String} parameters.volume - The volume specified by the StorageClass + * @returns {String} The location of the volume. + */ + getLocation() { + let location = _.get(this.options, "synology.volume"); + if (!location) { + location = "volume1"; + } + if (!location.startsWith("/")) { + location = "/" + location; + } + return location; + } + assertCapabilities(capabilities) { const driverResourceType = this.getDriverResourceType(); this.ctx.logger.verbose("validating capabilities: %j", capabilities); @@ -171,7 +224,9 @@ class ControllerSynologyDriver extends CsiBaseDriver { if ( capability.mount.fs_type && - !["nfs", "cifs"].includes(capability.mount.fs_type) + !GeneralUtils.default_supported_file_filesystems().includes( + capability.mount.fs_type + ) ) { message = `invalid fs_type ${capability.mount.fs_type}`; return false; @@ -198,7 +253,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { if (capability.access_type == "mount") { if ( capability.mount.fs_type && - !["btrfs", "ext3", "ext4", "ext4dev", "xfs"].includes( + !GeneralUtils.default_supported_block_filesystems().includes( capability.mount.fs_type ) ) { @@ -310,6 +365,9 @@ class ControllerSynologyDriver extends CsiBaseDriver { } let volume_context = {}; + const normalizedParameters = driver.getNormalizedParameters( + call.request.parameters + ); switch (driver.getDriverShareType()) { case "nfs": // TODO: create volume here @@ -327,12 +385,53 @@ class ControllerSynologyDriver extends CsiBaseDriver { break; case "iscsi": let iscsiName = driver.buildIscsiName(name); + let lunTemplate; + let targetTemplate; let data; let target; let lun_mapping; let lun_uuid; let existingLun; + lunTemplate = Object.assign( + {}, + _.get(driver.options, "iscsi.lunTemplate", {}), + driver.parseParameterYamlData( + _.get(normalizedParameters, "lunTemplate", "{}"), + "parameters.lunTemplate" + ), + driver.parseParameterYamlData( + _.get(call.request, "secrets.lunTemplate", "{}"), + "secrets.lunTemplate" + ) + ); + targetTemplate = Object.assign( + {}, + _.get(driver.options, "iscsi.targetTemplate", {}), + driver.parseParameterYamlData( + _.get(normalizedParameters, "targetTemplate", "{}"), + "parameters.targetTemplate" + ), + driver.parseParameterYamlData( + _.get(call.request, "secrets.targetTemplate", "{}"), + "secrets.targetTemplate" + ) + ); + + // render the template for description + if (lunTemplate.description) { + lunTemplate.description = Handlebars.compile(lunTemplate.description)( + { + name: call.request.name, + parameters: call.request.parameters, + csi: { + name: this.ctx.args.csiName, + version: this.ctx.args.csiVersion, + }, + } + ); + } + // ensure volumes with the same name being requested a 2nd time but with a different size fails try { let lun = await httpClient.GetLunByName(iscsiName); @@ -361,13 +460,12 @@ class ControllerSynologyDriver extends CsiBaseDriver { if (volume_content_source) { let src_lun_uuid; - let src_lun_id; switch (volume_content_source.type) { case "snapshot": let parts = volume_content_source.snapshot.snapshot_id.split("/"); - src_lun_id = parts[2]; - if (!src_lun_id) { + src_lun_uuid = parts[2]; + if (!src_lun_uuid) { throw new GrpcError( grpc.status.NOT_FOUND, `invalid snapshot_id: ${volume_content_source.snapshot.snapshot_id}` @@ -382,13 +480,17 @@ class ControllerSynologyDriver extends CsiBaseDriver { ); } - let src_lun = await httpClient.GetLunByID(src_lun_id); - src_lun_uuid = src_lun.uuid; + // This is for backwards compatibility. Previous versions of this driver used the LUN ID instead of the + // UUID. If this is the case we need to get the LUN UUID before we can proceed. + if (!src_lun_uuid.includes("-")) { + src_lun_uuid = await httpClient.GetLunByID(src_lun_uuid).uuid; + } - let snapshot = await httpClient.GetSnapshotByLunIDAndSnapshotUUID( - src_lun_id, - snapshot_uuid - ); + let snapshot = + await httpClient.GetSnapshotByLunUUIDAndSnapshotUUID( + src_lun_uuid, + snapshot_uuid + ); if (!snapshot) { throw new GrpcError( grpc.status.NOT_FOUND, @@ -401,7 +503,8 @@ class ControllerSynologyDriver extends CsiBaseDriver { await httpClient.CreateVolumeFromSnapshot( src_lun_uuid, snapshot_uuid, - iscsiName + iscsiName, + lunTemplate.description ); } break; @@ -425,7 +528,12 @@ class ControllerSynologyDriver extends CsiBaseDriver { `invalid volume_id: ${volume_content_source.volume.volume_id}` ); } - await httpClient.CreateClonedVolume(src_lun_uuid, iscsiName); + await httpClient.CreateClonedVolume( + src_lun_uuid, + iscsiName, + driver.getLocation(), + lunTemplate.description + ); } break; default: @@ -444,20 +552,22 @@ class ControllerSynologyDriver extends CsiBaseDriver { } } else { // create lun - data = Object.assign({}, driver.options.iscsi.lunTemplate, { + data = Object.assign({}, lunTemplate, { name: iscsiName, - location: driver.options.synology.volume, + location: driver.getLocation(), size: capacity_bytes, }); + lun_uuid = await httpClient.CreateLun(data); } // create target let iqn = driver.options.iscsi.baseiqn + iscsiName; - data = Object.assign({}, driver.options.iscsi.targetTemplate, { + data = Object.assign({}, targetTemplate, { name: iscsiName, iqn, }); + let target_id = await httpClient.CreateTarget(data); //target = await httpClient.GetTargetByTargetID(target_id); target = await httpClient.GetTargetByIQN(iqn); @@ -609,12 +719,12 @@ class ControllerSynologyDriver extends CsiBaseDriver { let waitTimeBetweenChecks = settleSeconds * 1000; - await sleep(waitTimeBetweenChecks); + await GeneralUtils.sleep(waitTimeBetweenChecks); lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); while (currentCheck <= settleMaxRetries && lun_uuid) { currentCheck++; - await sleep(waitTimeBetweenChecks); + await GeneralUtils.sleep(waitTimeBetweenChecks); lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); } @@ -737,8 +847,9 @@ class ControllerSynologyDriver extends CsiBaseDriver { async GetCapacity(call) { const driver = this; const httpClient = await driver.getHttpClient(); + const location = driver.getLocation(); - if (!driver.options.synology.volume) { + if (!location) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing volume` @@ -753,9 +864,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { } } - let response = await httpClient.GetVolumeInfo( - driver.options.synology.volume - ); + let response = await httpClient.GetVolumeInfo(location); return { available_capacity: response.body.data.volume.size_free_byte }; } @@ -833,6 +942,24 @@ class ControllerSynologyDriver extends CsiBaseDriver { ); } + const normalizedParameters = driver.getNormalizedParameters( + call.request.parameters + ); + let lunSnapshotTemplate; + + lunSnapshotTemplate = Object.assign( + {}, + _.get(driver.options, "iscsi.lunSnapshotTemplate", {}), + driver.parseParameterYamlData( + _.get(normalizedParameters, "lunSnapshotTemplate", "{}"), + "parameters.lunSnapshotTemplate" + ), + driver.parseParameterYamlData( + _.get(call.request, "secrets.lunSnapshotTemplate", "{}"), + "secrets.lunSnapshotTemplate" + ) + ); + // check for other snapshopts with the same name on other volumes and fail as appropriate // TODO: technically this should only be checking lun/snapshots relevant to this specific install of the driver // but alas an isolation/namespacing mechanism does not exist in synology @@ -848,16 +975,16 @@ class ControllerSynologyDriver extends CsiBaseDriver { // check for already exists let snapshot; - snapshot = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name); + snapshot = await httpClient.GetSnapshotByLunUUIDAndName(lun.uuid, name); if (!snapshot) { - let data = Object.assign({}, driver.options.iscsi.lunSnapshotTemplate, { + let data = Object.assign({}, lunSnapshotTemplate, { src_lun_uuid: lun.uuid, taken_by: "democratic-csi", description: name, //check }); await httpClient.CreateSnapshot(data); - snapshot = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name); + snapshot = await httpClient.GetSnapshotByLunUUIDAndName(lun.uuid, name); if (!snapshot) { throw new Error(`failed to create snapshot`); @@ -871,7 +998,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { * is needed to create a volume from this snapshot. */ size_bytes: snapshot.total_size, - snapshot_id: `/lun/${lun.lun_id}/${snapshot.uuid}`, // add shanpshot_uuid //fixme + snapshot_id: `/lun/${lun.uuid}/${snapshot.uuid}`, source_volume_id: source_volume_id, //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto creation_time: { @@ -908,8 +1035,8 @@ class ControllerSynologyDriver extends CsiBaseDriver { } let parts = snapshot_id.split("/"); - let lun_id = parts[2]; - if (!lun_id) { + let lun_uuid = parts[2]; + if (!lun_uuid) { return {}; } @@ -918,9 +1045,14 @@ class ControllerSynologyDriver extends CsiBaseDriver { return {}; } - // TODO: delete snapshot - let snapshot = await httpClient.GetSnapshotByLunIDAndSnapshotUUID( - lun_id, + // This is for backwards compatibility. Previous versions of this driver used the LUN ID instead of the UUID. If + // this is the case we need to get the LUN UUID before we can proceed. + if (!lun_uuid.includes("-")) { + lun_uuid = await httpClient.GetLunByID(lun_uuid).uuid; + } + + let snapshot = await httpClient.GetSnapshotByLunUUIDAndSnapshotUUID( + lun_uuid, snapshot_uuid ); diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 31c0fa7..991a922 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -1,9 +1,9 @@ const _ = require("lodash"); const { ControllerZfsBaseDriver } = require("../controller-zfs"); const { GrpcError, grpc } = require("../../utils/grpc"); +const GeneralUtils = require("../../utils/general"); const registry = require("../../utils/registry"); const SshClient = require("../../utils/ssh").SshClient; -const sleep = require("../../utils/general").sleep; const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); const Handlebars = require("handlebars"); @@ -52,6 +52,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { getDriverZfsResourceType() { switch (this.options.driver) { case "zfs-generic-nfs": + case "zfs-generic-smb": return "filesystem"; case "zfs-generic-iscsi": return "volume"; @@ -60,6 +61,24 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { } } + generateSmbShareName(datasetName) { + const driver = this; + + driver.ctx.logger.verbose( + `generating smb share name for dataset: ${datasetName}` + ); + + let name = datasetName || ""; + name = name.replaceAll("/", "_"); + name = name.replaceAll("-", "_"); + + driver.ctx.logger.verbose( + `generated smb share name for dataset (${datasetName}): ${name}` + ); + + return name; + } + /** * should create any necessary share resources * should set the SHARE_VOLUME_CONTEXT_PROPERTY_NAME propery @@ -67,6 +86,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { * @param {*} datasetName */ async createShare(call, datasetName) { + const driver = this; const zb = await this.getZetabyte(); const execClient = this.getExecClient(); @@ -109,6 +129,41 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { }; return volume_context; + case "zfs-generic-smb": + let share; + switch (this.options.smb.shareStrategy) { + case "setDatasetProperties": + for (let key of ["share", "sharesmb"]) { + if ( + this.options.smb.shareStrategySetDatasetProperties.properties[ + key + ] + ) { + await zb.zfs.set(datasetName, { + [key]: + this.options.smb.shareStrategySetDatasetProperties + .properties[key], + }); + } + } + + share = driver.generateSmbShareName(datasetName); + break; + default: + break; + } + + properties = await zb.zfs.get(datasetName, ["mountpoint"]); + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); + + volume_context = { + node_attach_driver: "smb", + server: this.options.smb.shareHost, + share, + }; + return volume_context; + case "zfs-generic-iscsi": let basename; let iscsiName; @@ -176,8 +231,12 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { } } - response = await this.targetCliCommand( - ` + await GeneralUtils.retry( + 3, + 2000, + async () => { + await this.targetCliCommand( + ` # create target cd /iscsi create ${basename}:${iscsiName} @@ -195,6 +254,16 @@ create ${iscsiName} /dev/${extentDiskName} cd /iscsi/${basename}:${iscsiName}/tpg1/luns create /backstores/block/${iscsiName} ` + ); + }, + { + retryCondition: (err) => { + if (err.stdout && err.stdout.includes("Ran out of input")) { + return true; + } + return false; + }, + } ); break; default: @@ -258,7 +327,7 @@ create /backstores/block/${iscsiName} } } } - await sleep(2000); // let things settle + await GeneralUtils.sleep(2000); // let things settle break; default: throw new GrpcError( @@ -268,6 +337,36 @@ create /backstores/block/${iscsiName} } break; + case "zfs-generic-smb": + switch (this.options.smb.shareStrategy) { + case "setDatasetProperties": + for (let key of ["share", "sharesmb"]) { + if ( + this.options.smb.shareStrategySetDatasetProperties.properties[ + key + ] + ) { + try { + await zb.zfs.inherit(datasetName, key); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + // do nothing + } else { + throw err; + } + } + } + } + await GeneralUtils.sleep(2000); // let things settle + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown shareStrategy ${this.options.smb.shareStrategy}` + ); + } + break; + case "zfs-generic-iscsi": let basename; let iscsiName; @@ -307,8 +406,12 @@ create /backstores/block/${iscsiName} switch (this.options.iscsi.shareStrategy) { case "targetCli": basename = this.options.iscsi.shareStrategyTargetCli.basename; - response = await this.targetCliCommand( - ` + await GeneralUtils.retry( + 3, + 2000, + async () => { + await this.targetCliCommand( + ` # delete target cd /iscsi delete ${basename}:${iscsiName} @@ -317,7 +420,18 @@ delete ${basename}:${iscsiName} cd /backstores/block delete ${iscsiName} ` + ); + }, + { + retryCondition: (err) => { + if (err.stdout && err.stdout.includes("Ran out of input")) { + return true; + } + return false; + }, + } ); + break; default: break; @@ -362,19 +476,19 @@ delete ${iscsiName} let command = "sh"; let args = ["-c"]; - let taregetCliCommand = []; - taregetCliCommand.push(`echo "${data}"`.trim()); - taregetCliCommand.push("|"); - taregetCliCommand.push("targetcli"); + let targetCliArgs = ["targetcli"]; if ( _.get(this.options, "iscsi.shareStrategyTargetCli.sudoEnabled", false) ) { - command = "sudo"; - args.unshift("sh"); + targetCliArgs.unshift("sudo"); } - args.push("'" + taregetCliCommand.join(" ") + "'"); + let targetCliCommand = []; + targetCliCommand.push(`echo "${data}"`.trim()); + targetCliCommand.push("|"); + targetCliCommand.push(targetCliArgs.join(" ")); + args.push("'" + targetCliCommand.join(" ") + "'"); let logCommandTmp = command + " " + args.join(" "); let logCommand = ""; @@ -405,12 +519,12 @@ delete ${iscsiName} execClient.buildCommand(command, args), options ); - if (response.code != 0) { - throw new Error(JSON.stringify(response)); - } driver.ctx.logger.verbose( "TargetCLI response: " + JSON.stringify(response) ); + if (response.code != 0) { + throw response; + } return response; } } diff --git a/src/driver/controller-zfs-local/index.js b/src/driver/controller-zfs-local/index.js index 04f1a17..3c1911b 100644 --- a/src/driver/controller-zfs-local/index.js +++ b/src/driver/controller-zfs-local/index.js @@ -1,6 +1,7 @@ const _ = require("lodash"); const { ControllerZfsBaseDriver } = require("../controller-zfs"); const { GrpcError, grpc } = require("../../utils/grpc"); +const GeneralUtils = require("../../utils/general"); const LocalCliExecClient = require("./exec").LocalCliClient; const registry = require("../../utils/registry"); const { Zetabyte } = require("../../utils/zfs"); @@ -95,7 +96,7 @@ class ControllerZfsLocalDriver extends ControllerZfsBaseDriver { case "filesystem": return ["zfs"]; case "volume": - return ["btrfs", "ext3", "ext4", "ext4dev", "xfs"]; + return GeneralUtils.default_supported_block_filesystems(); } } diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js index d354e43..ccc5b01 100644 --- a/src/driver/controller-zfs/index.js +++ b/src/driver/controller-zfs/index.js @@ -1,7 +1,7 @@ const _ = require("lodash"); const { CsiBaseDriver } = require("../index"); const { GrpcError, grpc } = require("../../utils/grpc"); -const sleep = require("../../utils/general").sleep; +const GeneralUtils = require("../../utils/general"); const getLargestNumber = require("../../utils/general").getLargestNumber; const Handlebars = require("handlebars"); @@ -201,9 +201,9 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { const driverZfsResourceType = this.getDriverZfsResourceType(); switch (driverZfsResourceType) { case "filesystem": - return ["nfs", "cifs"]; + return GeneralUtils.default_supported_file_filesystems(); case "volume": - return ["btrfs", "ext3", "ext4", "ext4dev", "xfs"]; + return GeneralUtils.default_supported_block_filesystems(); } } @@ -620,6 +620,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { let snapshotParentDatasetName = this.getDetachedSnapshotParentDatasetName(); let zvolBlocksize = this.options.zfs.zvolBlocksize || "16K"; let name = call.request.name; + let volume_id = await driver.getVolumeIdFromName(name); let volume_content_source = call.request.volume_content_source; if (!datasetParentName) { @@ -710,7 +711,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { * NOTE: avoid the urge to templatize this given the name length limits for zvols * ie: namespace-name may quite easily exceed 58 chars */ - const datasetName = datasetParentName + "/" + name; + const datasetName = datasetParentName + "/" + volume_id; // ensure volumes with the same name being requested a 2nd time but with a different size fails try { @@ -862,7 +863,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { volume_content_source_snapshot_id + "@" + VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + - name; + volume_id; } driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); @@ -909,6 +910,12 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { }); } else { try { + // remove readonly/undesired props + let cloneProperties = volumeProperties; + delete cloneProperties["aclmode"]; + delete cloneProperties["aclinherit"]; + delete cloneProperties["acltype"]; + delete cloneProperties["casesensitivity"]; response = await zb.zfs.clone(fullSnapshotName, datasetName, { properties: volumeProperties, }); @@ -971,7 +978,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { volume_content_source_volume_id + "@" + VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + - name; + volume_id; driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); @@ -1024,9 +1031,15 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { } else { // create clone // zfs origin property contains parent info, ie: pool0/k8s/test/PVC-111@clone-test + // remove readonly/undesired props + let cloneProperties = volumeProperties; + delete cloneProperties["aclmode"]; + delete cloneProperties["aclinherit"]; + delete cloneProperties["acltype"]; + delete cloneProperties["casesensitivity"]; try { response = await zb.zfs.clone(fullSnapshotName, datasetName, { - properties: volumeProperties, + properties: cloneProperties, }); } catch (err) { if (err.toString().includes("dataset does not exist")) { @@ -1128,8 +1141,13 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { // TODO: this is unsfafe approach, make it better // probably could see if ^-.*\s and split and then shell escape if (this.options.zfs.datasetPermissionsAcls) { + let aclBinary = _.get( + driver.options, + "zfs.datasetPermissionsAclsBinary", + "setfacl" + ); for (const acl of this.options.zfs.datasetPermissionsAcls) { - command = execClient.buildCommand("setfacl", [ + command = execClient.buildCommand(aclBinary, [ acl, properties.mountpoint.value, ]); @@ -1147,7 +1165,6 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { } } } - break; case "volume": // set properties @@ -1191,7 +1208,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { const res = { volume: { - volume_id: name, + volume_id, //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 capacity_bytes: this.options.zfs.datasetEnableQuotas || @@ -1301,27 +1318,24 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { // NOTE: -R will recursively delete items + dependent filesets // delete dataset try { - let max_tries = 5; - let sleep_time = 3000; - let current_try = 1; - let success = false; - while (!success && current_try <= max_tries) { - try { + await GeneralUtils.retry( + 12, + 5000, + async () => { await zb.zfs.destroy(datasetName, { recurse: true, force: true }); - success = true; - } catch (err) { - if (err.toString().includes("dataset is busy")) { - current_try++; - if (current_try > max_tries) { - throw err; - } else { - await sleep(sleep_time); + }, + { + retryCondition: (err) => { + if ( + err.toString().includes("dataset is busy") || + err.toString().includes("target is busy") + ) { + return true; } - } else { - throw err; - } + return false; + }, } - } + ); } catch (err) { if (err.toString().includes("filesystem has dependent clones")) { throw new GrpcError( @@ -2190,7 +2204,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { }); // let things settle down - //await sleep(3000); + //await GneralUtils.sleep(3000); } else { try { await zb.zfs.snapshot(fullSnapshotName, { @@ -2198,7 +2212,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { }); // let things settle down - //await sleep(3000); + //await GeneralUtils.sleep(3000); } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( diff --git a/src/driver/factory.js b/src/driver/factory.js index 6cb4850..02dc2ae 100644 --- a/src/driver/factory.js +++ b/src/driver/factory.js @@ -33,6 +33,7 @@ function factory(ctx, options) { case "synology-iscsi": return new ControllerSynologyDriver(ctx, options); case "zfs-generic-nfs": + case "zfs-generic-smb": case "zfs-generic-iscsi": return new ControllerZfsGenericDriver(ctx, options); case "zfs-local-dataset": diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index f436ff9..3dd86aa 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -4,9 +4,8 @@ const { CsiBaseDriver } = require("../index"); const HttpClient = require("./http").Client; const TrueNASApiClient = require("./http/api").Api; const { Zetabyte } = require("../../utils/zfs"); -const getLargestNumber = require("../../utils/general").getLargestNumber; const registry = require("../../utils/registry"); -const sleep = require("../../utils/general").sleep; +const GeneralUtils = require("../../utils/general"); const Handlebars = require("handlebars"); const uuidv4 = require("uuid").v4; @@ -262,7 +261,27 @@ class FreeNASApiDriver extends CsiBaseDriver { break; } - response = await httpClient.post("/sharing/nfs", share); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.post("/sharing/nfs", share); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); /** * v1 = 201 @@ -483,7 +502,27 @@ class FreeNASApiDriver extends CsiBaseDriver { break; } - response = await httpClient.post(endpoint, share); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.post(endpoint, share); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); /** * v1 = 201 @@ -1363,7 +1402,27 @@ class FreeNASApiDriver extends CsiBaseDriver { }); if (deleteAsset) { - response = await httpClient.delete(endpoint); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.delete(endpoint); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); // returns a 500 if does not exist // v1 = 204 @@ -1444,12 +1503,35 @@ class FreeNASApiDriver extends CsiBaseDriver { }); if (deleteAsset) { - response = await httpClient.delete(endpoint); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.delete(endpoint); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); // returns a 500 if does not exist // v1 = 204 // v2 = 200 - if (![200, 204].includes(response.statusCode)) { + if ( + ![200, 204].includes(response.statusCode) && + !JSON.stringify(response.body).includes("does not exist") + ) { throw new GrpcError( grpc.status.UNKNOWN, `received error deleting smb share - share: ${shareId} code: ${ @@ -1477,7 +1559,7 @@ class FreeNASApiDriver extends CsiBaseDriver { break; case "iscsi": // Delete target - // NOTE: deletting a target inherently deletes associated targetgroup(s) and targettoextent(s) + // NOTE: deleting a target inherently deletes associated targetgroup(s) and targettoextent(s) // Delete extent try { @@ -1565,7 +1647,7 @@ class FreeNASApiDriver extends CsiBaseDriver { targetId, retries ); - await sleep(retryWait); + await GeneralUtils.sleep(retryWait); response = await httpClient.delete(endpoint); } @@ -1958,7 +2040,7 @@ class FreeNASApiDriver extends CsiBaseDriver { if (capability.access_type == "mount") { if ( capability.mount.fs_type && - !["btrfs", "ext3", "ext4", "ext4dev", "xfs"].includes( + !GeneralUtils.default_supported_block_filesystems().includes( capability.mount.fs_type ) ) { @@ -2025,6 +2107,7 @@ class FreeNASApiDriver extends CsiBaseDriver { */ async Probe(call) { const driver = this; + const httpApiClient = await driver.getTrueNASHttpApiClient(); if (driver.ctx.args.csiMode.includes("controller")) { let datasetParentName = this.getVolumeParentDatasetName() + "/"; @@ -2039,6 +2122,14 @@ class FreeNASApiDriver extends CsiBaseDriver { `datasetParentName and detachedSnapshotsDatasetParentName must not overlap` ); } + + if (!(await httpApiClient.getIsScale())) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `driver is only availalbe with TrueNAS SCALE` + ); + } + return { ready: { value: true } }; } else { return { ready: { value: true } }; @@ -2066,6 +2157,7 @@ class FreeNASApiDriver extends CsiBaseDriver { let snapshotParentDatasetName = this.getDetachedSnapshotParentDatasetName(); let zvolBlocksize = this.options.zfs.zvolBlocksize || "16K"; let name = call.request.name; + let volume_id = await driver.getVolumeIdFromName(name); let volume_content_source = call.request.volume_content_source; let minimum_volume_size = await driver.getMinimumVolumeSize(); let default_required_bytes = 1073741824; @@ -2171,7 +2263,7 @@ class FreeNASApiDriver extends CsiBaseDriver { * NOTE: avoid the urge to templatize this given the name length limits for zvols * ie: namespace-name may quite easily exceed 58 chars */ - const datasetName = datasetParentName + "/" + name; + const datasetName = datasetParentName + "/" + volume_id; // ensure volumes with the same name being requested a 2nd time but with a different size fails try { @@ -2326,7 +2418,7 @@ class FreeNASApiDriver extends CsiBaseDriver { volume_content_source_snapshot_id + "@" + VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + - name; + volume_id; } driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); @@ -2378,7 +2470,7 @@ class FreeNASApiDriver extends CsiBaseDriver { ) { job = await httpApiClient.CoreGetJobs({ id: job_id }); job = job[0]; - await sleep(3000); + await GeneralUtils.sleep(3000); } job.error = job.error || ""; @@ -2488,7 +2580,7 @@ class FreeNASApiDriver extends CsiBaseDriver { volume_content_source_volume_id + "@" + VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + - name; + volume_id; driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); @@ -2538,7 +2630,7 @@ class FreeNASApiDriver extends CsiBaseDriver { ) { job = await httpApiClient.CoreGetJobs({ id: job_id }); job = job[0]; - await sleep(3000); + await GeneralUtils.sleep(3000); } job.error = job.error || ""; @@ -2626,6 +2718,9 @@ class FreeNASApiDriver extends CsiBaseDriver { volsize: driverZfsResourceType == "volume" ? capacity_bytes : undefined, sparse: driverZfsResourceType == "volume" ? sparse : undefined, create_ancestors: true, + share_type: driver.getDriverShareType().includes("smb") + ? "SMB" + : "GENERIC", user_properties: httpApiClient.getPropertiesKeyValueArray( httpApiClient.getUserProperties(volumeProperties) ), @@ -2721,7 +2816,18 @@ class FreeNASApiDriver extends CsiBaseDriver { } if (setPerms) { - await httpApiClient.FilesystemSetperm(perms); + response = await httpApiClient.FilesystemSetperm(perms); + await httpApiClient.CoreWaitForJob(response, 30); + // SetPerm does not alter ownership with extended ACLs + // run this in addition just for good measure + if (perms.uid || perms.gid) { + response = await httpApiClient.FilesystemChown({ + path: perms.path, + uid: perms.uid, + gid: perms.gid, + }); + await httpApiClient.CoreWaitForJob(response, 30); + } } // set acls @@ -2777,7 +2883,7 @@ class FreeNASApiDriver extends CsiBaseDriver { const res = { volume: { - volume_id: name, + volume_id, //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 capacity_bytes: this.options.zfs.datasetEnableQuotas || @@ -3649,7 +3755,10 @@ class FreeNASApiDriver extends CsiBaseDriver { // so we must be cognizant and use the highest possible value here // note that whatever value is returned here can/will essentially impact the refquota // value of a derived volume - size_bytes = getLargestNumber(row.referenced, row.logicalreferenced); + size_bytes = GeneralUtils.getLargestNumber( + row.referenced, + row.logicalreferenced + ); } else { // get the size of the parent volume size_bytes = row.volsize; @@ -3930,7 +4039,7 @@ class FreeNASApiDriver extends CsiBaseDriver { while (!job || !["SUCCESS", "ABORTED", "FAILED"].includes(job.state)) { job = await httpApiClient.CoreGetJobs({ id: job_id }); job = job[0]; - await sleep(3000); + await GeneralUtils.sleep(3000); } job.error = job.error || ""; @@ -4045,7 +4154,7 @@ class FreeNASApiDriver extends CsiBaseDriver { // so we must be cognizant and use the highest possible value here // note that whatever value is returned here can/will essentially impact the refquota // value of a derived volume - size_bytes = getLargestNumber( + size_bytes = GeneralUtils.getLargestNumber( properties.referenced.rawvalue, properties.logicalreferenced.rawvalue // TODO: perhaps include minimum volume size here? diff --git a/src/driver/freenas/http/api.js b/src/driver/freenas/http/api.js index ef4b52a..9878d18 100644 --- a/src/driver/freenas/http/api.js +++ b/src/driver/freenas/http/api.js @@ -681,7 +681,13 @@ class Api { throw new Error(JSON.stringify(response.body)); } - async CoreWaitForJob(job_id, timeout = 0) { + /** + * + * @param {*} job_id + * @param {*} timeout in seconds + * @returns + */ + async CoreWaitForJob(job_id, timeout = 0, check_interval = 3000) { if (!job_id) { throw new Error("invalid job_id"); } @@ -692,16 +698,17 @@ class Api { let job; // wait for job to finish - while (!job || !["SUCCESS", "ABORTED", "FAILED"].includes(job.state)) { + do { + if (job) { + await sleep(check_interval); + } job = await this.CoreGetJobs({ id: job_id }); job = job[0]; - await sleep(3000); - currentTime = Date.now() / 1000; if (timeout > 0 && currentTime > startTime + timeout) { throw new Error("timeout waiting for job to complete"); } - } + } while (!["SUCCESS", "ABORTED", "FAILED"].includes(job.state)); return job; } @@ -754,7 +761,38 @@ class Api { response = await httpClient.post(endpoint, data); if (response.statusCode == 200) { - return; + return response.body; + } + + throw new Error(JSON.stringify(response.body)); + } + + /** + * + * @param {*} data + */ + async FilesystemChown(data) { + /* + { + "path": "string", + "uid": 0, + "gid": 0, + "options": { + "recursive": false, + "traverse": false + } + } + */ + + const httpClient = await this.getHttpClient(false); + let response; + let endpoint; + + endpoint = `/filesystem/chown`; + response = await httpClient.post(endpoint, data); + + if (response.statusCode == 200) { + return response.body; } throw new Error(JSON.stringify(response.body)); diff --git a/src/driver/freenas/http/index.js b/src/driver/freenas/http/index.js index d516610..4d2009c 100644 --- a/src/driver/freenas/http/index.js +++ b/src/driver/freenas/http/index.js @@ -86,6 +86,12 @@ class Client { httpAgent: this.getHttpAgent(), httpsAgent: this.getHttpsAgent(), timeout: 60 * 1000, + validateStatus: function (status) { + if (status >= 500) { + return false; + } + return true; + }, }; if (client.options.apiKey) { @@ -122,10 +128,17 @@ class Client { _.set(options, prop, "redacted"); } + delete options.httpAgent; + delete options.httpsAgent; + this.logger.debug("FREENAS HTTP REQUEST: " + stringify(options)); this.logger.debug("FREENAS HTTP ERROR: " + error); - this.logger.debug("FREENAS HTTP STATUS: " + response.statusCode); - this.logger.debug("FREENAS HTTP HEADERS: " + stringify(response.headers)); + this.logger.debug( + "FREENAS HTTP STATUS: " + _.get(response, "statusCode", "") + ); + this.logger.debug( + "FREENAS HTTP HEADERS: " + stringify(_.get(response, "headers", "")) + ); this.logger.debug("FREENAS HTTP BODY: " + stringify(body)); } diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 12c3821..3debb76 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -4,8 +4,9 @@ const { GrpcError, grpc } = require("../../utils/grpc"); const registry = require("../../utils/registry"); const SshClient = require("../../utils/ssh").SshClient; const HttpClient = require("./http").Client; +const TrueNASApiClient = require("./http/api").Api; const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); -const { sleep, stringify } = require("../../utils/general"); +const GeneralUtils = require("../../utils/general"); const Handlebars = require("handlebars"); @@ -112,6 +113,13 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { ); } + async getTrueNASHttpApiClient() { + return registry.getAsync(`${__REGISTRY_NS__}:api_client`, async () => { + const httpClient = await this.getHttpClient(); + return new TrueNASApiClient(httpClient, this.ctx.cache); + }); + } + getDriverShareType() { switch (this.options.driver) { case "freenas-nfs": @@ -300,7 +308,27 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { break; } - response = await httpClient.post("/sharing/nfs", share); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.post("/sharing/nfs", share); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); /** * v1 = 201 @@ -521,7 +549,27 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { break; } - response = await httpClient.post(endpoint, share); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.post(endpoint, share); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); /** * v1 = 201 @@ -1402,7 +1450,27 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { }); if (deleteAsset) { - response = await httpClient.delete(endpoint); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.delete(endpoint); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); // returns a 500 if does not exist // v1 = 204 @@ -1484,12 +1552,35 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { }); if (deleteAsset) { - response = await httpClient.delete(endpoint); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.delete(endpoint); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); // returns a 500 if does not exist // v1 = 204 // v2 = 200 - if (![200, 204].includes(response.statusCode)) { + if ( + ![200, 204].includes(response.statusCode) && + !JSON.stringify(response.body).includes("does not exist") + ) { throw new GrpcError( grpc.status.UNKNOWN, `received error deleting smb share - share: ${shareId} code: ${ @@ -1606,7 +1697,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { targetId, retries ); - await sleep(retryWait); + await GeneralUtils.sleep(retryWait); response = await httpClient.delete(endpoint); } @@ -1716,6 +1807,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { async setFilesystemMode(path, mode) { const httpClient = await this.getHttpClient(); const apiVersion = httpClient.getApiVersion(); + const httpApiClient = await this.getTrueNASHttpApiClient(); switch (apiVersion) { case 1: @@ -1747,6 +1839,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { response = await httpClient.post(endpoint, perms); if (response.statusCode == 200) { + await httpApiClient.CoreWaitForJob(response.body, 30); return; } @@ -1764,6 +1857,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { async setFilesystemOwnership(path, user = false, group = false) { const httpClient = await this.getHttpClient(); const apiVersion = httpClient.getApiVersion(); + const httpApiClient = await this.getTrueNASHttpApiClient(); if (user === false || typeof user == "undefined" || user === null) { user = ""; @@ -1832,6 +1926,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { response = await httpClient.post(endpoint, perms); if (response.statusCode == 200) { + await httpApiClient.CoreWaitForJob(response.body, 30); return; } @@ -2122,7 +2217,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { // likely bad creds/url throw new GrpcError( grpc.status.UNKNOWN, - `FreeNAS error getting system version info: ${stringify({ + `FreeNAS error getting system version info: ${GeneralUtils.stringify({ errors: versionErrors, responses: versionResponses, })}` diff --git a/src/driver/index.js b/src/driver/index.js index 735effc..a1db307 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -2,6 +2,8 @@ const _ = require("lodash"); const cp = require("child_process"); const os = require("os"); const fs = require("fs"); +const CsiProxyClient = require("../utils/csi_proxy_client").CsiProxyClient; +const k8s = require("@kubernetes/client-node"); const { GrpcError, grpc } = require("../utils/grpc"); const { Mount } = require("../utils/mount"); const { OneClient } = require("../utils/oneclient"); @@ -9,11 +11,15 @@ const { Filesystem } = require("../utils/filesystem"); const { ISCSI } = require("../utils/iscsi"); const registry = require("../utils/registry"); const semver = require("semver"); -const sleep = require("../utils/general").sleep; +const GeneralUtils = require("../utils/general"); const { Zetabyte } = require("../utils/zfs"); const __REGISTRY_NS__ = "CsiBaseDriver"; +const NODE_OS_DRIVER_CSI_PROXY = "csi-proxy"; +const NODE_OS_DRIVER_POSIX = "posix"; +const NODE_OS_DRIVER_WINDOWS = "windows"; + /** * common code shared between all drivers * this is **NOT** meant to work as a proxy @@ -161,6 +167,213 @@ class CsiBaseDriver { }); } + /** + * + * @returns CsiProxyClient + */ + getDefaultCsiProxyClientInstance() { + return registry.get(`${__REGISTRY_NS__}:default_csi_proxy_instance`, () => { + const options = {}; + options.services = _.get(this.options, "node.csiProxy.services", {}); + return new CsiProxyClient(options); + }); + } + + getDefaultKubernetsConfigInstance() { + return registry.get( + `${__REGISTRY_NS__}:default_kubernetes_config_instance`, + () => { + const kc = new k8s.KubeConfig(); + kc.loadFromDefault(); + return kc; + } + ); + } + + getCsiProxyEnabled() { + const defaultValue = process.platform == "win32"; + return _.get(this.options, "node.csiProxy.enabled", defaultValue); + } + + getNodeIsWindows() { + return process.platform == "win32"; + } + + __getNodeOsDriver() { + if (this.getNodeIsWindows()) { + return NODE_OS_DRIVER_WINDOWS; + } + + //if (this.getNodeIsWindows() || this.getCsiProxyEnabled()) { + // return NODE_OS_DRIVER_CSI_PROXY; + //} + + return NODE_OS_DRIVER_POSIX; + } + + getMountFlagValue(mount_flags = [], flag = "") { + for (let i = mount_flags.length - 1; i >= 0; i--) { + const mount_flag = mount_flags[i]; + if (mount_flag.startsWith(`${flag}=`)) { + return mount_flag.split("=", 2)[1] || ""; + } + } + } + + async getDerivedVolumeContextDriver() { + const driver = this; + let d = _.get(driver.options, "_private.csi.volume.volumeContext.driver"); + if ( + !d && + (process.env.KUBERNETES_SERVICE_HOST || + process.env.KUBERNETES_SERVICE_PORT) + ) { + // test for k8s + d = "kubernetes"; + } + + if (!d) { + // test for Nomad + } + + if (!d && process.env.CSI_SANITY == 1) { + d = "memory"; + } + + return d; + } + + /** + * Used predominantly with windows due to limitations with the csi-proxy + * + * @param {*} call + * @returns + */ + async getDerivedVolumeContext(call) { + const driver = this; + const volume_id = call.request.volume_id; + const d = await driver.getDerivedVolumeContextDriver(); + driver.ctx.logger.debug(`looking up volume_context using driver: ${d}`); + let volume_context; + switch (d) { + case "memory": + driver.volume_context_cache = driver.volume_context_cache || {}; + volume_context = driver.volume_context_cache[volume_id]; + break; + case "kubernetes": + const kc = driver.getDefaultKubernetsConfigInstance(); + const k8sApi = kc.makeApiClient(k8s.CoreV1Api); + + async function findPVByDriverHandle(driver, volumeHandle) { + if (!driver || !volumeHandle) { + return; + } + + let pv; + let pvs; + let kcontinue; + do { + pvs = await k8sApi.listPersistentVolume( + undefined, + undefined, + kcontinue, + undefined, + undefined, + undefined // limit + ); + pv = pvs.body.items.find((item) => { + return ( + item.spec.csi.driver == driver && + item.spec.csi.volumeHandle == volumeHandle + ); + }); + kcontinue = pvs.body.metadata._continue; + } while (!pv && pvs.body.metadata._continue); + + return pv; + } + + const pv = await findPVByDriverHandle( + driver.ctx.args.csiName, + volume_id + ); + if (pv) { + volume_context = pv.spec.csi.volumeAttributes; + } + break; + default: + throw new Error(`unknow derived volume context driver: ${d}`); + } + + //if (!volume_context) { + // throw new Error(`failed to retrieve volume_context for ${volume_id}`); + //} + + if (!volume_context) { + volume_context = _.get( + driver.options, + `_private.volume_context.${volume_id}` + ); + } + + driver.ctx.logger.debug( + "retrived derived volume_context %j", + volume_context + ); + return volume_context; + } + + /** + * Should only be used for testing purposes, generally these details should + * come from a CO or some other stateful storage mechanism + * + * @param {*} volume_id + * @param {*} volume_context + */ + async setVolumeContextCache(volume_id, volume_context) { + const driver = this; + if (process.env.CSI_SANITY == 1) { + if (!driver.volume_context_cache) { + driver.volume_context_cache = {}; + } + if (!driver.volume_context_cache[volume_id]) { + driver.ctx.logger.debug( + "setting volume_context_cache %s %j", + volume_id, + volume_context + ); + driver.volume_context_cache[volume_id] = volume_context; + } + } + } + + /** + * Translates a `name` to a `volume_id`. Generally the purpose is to shorten + * the value of `volume_id` to play nicely with scenarios that do not support + * long names (ie: smb share, etc) + * + * @param {*} name + * @returns + */ + async getVolumeIdFromName(name) { + const driver = this; + const strategy = _.get( + driver.options, + "_private.csi.volume.idHash.strategy", + "" + ); + switch (strategy.toLowerCase()) { + case "md5": + return GeneralUtils.md5(name); + case "crc32": + return GeneralUtils.crc32(name); + case "crc16": + return GeneralUtils.crc16(name); + default: + return name; + } + } + async GetPluginInfo(call) { return { name: this.ctx.args.csiName, @@ -349,6 +562,7 @@ class CsiBaseDriver { const iscsi = driver.getDefaultISCSIInstance(); let result; let device; + let block_device_info; const volume_id = call.request.volume_id; if (!volume_id) { @@ -367,7 +581,7 @@ class CsiBaseDriver { } const access_type = capability.access_type || "mount"; const volume_context = call.request.volume_context; - let fs_type; + let fs_type = _.get(capability, "mount.fs_type"); let mount_flags; let volume_mount_group; const node_attach_driver = volume_context.node_attach_driver; @@ -390,11 +604,18 @@ class CsiBaseDriver { */ if (access_type == "mount") { - fs_type = capability.mount.fs_type; mount_flags = capability.mount.mount_flags || []; + + // yaml mount_flags + if (_.get(driver.options, "node.mount.mount_flags")) { + mount_flags.push( + ..._.get(driver.options, "node.mount.mount_flags").split(",") + ); + } + // add secrets mount_flags if (normalizedSecrets.mount_flags) { - mount_flags.push(normalizedSecrets.mount_flags); + mount_flags.push(...normalizedSecrets.mount_flags.split(",")); } switch (node_attach_driver) { @@ -438,465 +659,1309 @@ class CsiBaseDriver { } } - // csi spec stipulates that staging_target_path is a directory even for block mounts - result = await filesystem.pathExists(staging_target_path); - if (!result) { - await filesystem.mkdir(staging_target_path, ["-p", "-m", "0750"]); - } - - switch (node_attach_driver) { - case "nfs": - case "lustre": - device = `${volume_context.server}:${volume_context.share}`; - break; - case "smb": - device = `//${volume_context.server}/${volume_context.share}`; - - // if not present add guest - let has_username = mount_flags.some((element) => { - element = element.trim().toLowerCase(); - return element.startsWith("username="); - }); - - // prevents driver from hanging on stdin waiting for a password to be entered at the cli - if (!has_username) { - let has_guest = mount_flags.some((element) => { - element = element.trim().toLowerCase(); - return element === "guest"; - }); - - if (!has_guest) { - mount_flags.push("guest"); - } - } - break; - case "iscsi": - let portals = []; - if (volume_context.portal) { - portals.push(volume_context.portal.trim()); - } - - if (volume_context.portals) { - volume_context.portals.split(",").forEach((portal) => { - portals.push(portal.trim()); - }); - } - - // ensure full portal value - portals = portals.map((value) => { - if (!value.includes(":")) { - value += ":3260"; - } - - return value.trim(); - }); - - // ensure unique entries only - portals = [...new Set(portals)]; - - // stores actual device paths after iscsi login - let iscsiDevices = []; - - // stores configuration of targets/iqn/luns to connect to - let iscsiConnections = []; - for (let portal of portals) { - iscsiConnections.push({ - portal, - iqn: volume_context.iqn, - lun: volume_context.lun, - }); - } - - /** - * TODO: allow sending in iscsiConnection in a raw/manual format - * TODO: allow option to determine if send_targets should be invoked - * TODO: allow option to control whether nodedb entry should be created by driver - * TODO: allow option to control whether nodedb entry should be deleted by driver - */ - - for (let iscsiConnection of iscsiConnections) { - // create DB entry - // https://library.netapp.com/ecmdocs/ECMP1654943/html/GUID-8EC685B4-8CB6-40D8-A8D5-031A3899BCDC.html - // put these options in place to force targets managed by csi to be explicitly attached (in the case of unclearn shutdown etc) - let nodeDB = { - "node.startup": "manual", - //"node.session.scan": "manual", - }; - const nodeDBKeyPrefix = "node-db."; - for (const key in normalizedSecrets) { - if (key.startsWith(nodeDBKeyPrefix)) { - nodeDB[key.substr(nodeDBKeyPrefix.length)] = - normalizedSecrets[key]; - } - } - - // create 'DB' entry - await iscsi.iscsiadm.createNodeDBEntry( - iscsiConnection.iqn, - iscsiConnection.portal, - nodeDB - ); - // login - await iscsi.iscsiadm.login( - iscsiConnection.iqn, - iscsiConnection.portal - ); - - // get associated session - let session = await iscsi.iscsiadm.getSession( - iscsiConnection.iqn, - iscsiConnection.portal - ); - - // rescan in scenarios when login previously occurred but volumes never appeared - await iscsi.iscsiadm.rescanSession(session); - - // find device name - device = `/dev/disk/by-path/ip-${iscsiConnection.portal}-iscsi-${iscsiConnection.iqn}-lun-${iscsiConnection.lun}`; - let deviceByPath = device; - - // can take some time for device to show up, loop for some period - result = await filesystem.pathExists(device); - let timer_start = Math.round(new Date().getTime() / 1000); - let timer_max = 30; - let deviceCreated = result; - while (!result) { - await sleep(2000); - result = await filesystem.pathExists(device); - - if (result) { - deviceCreated = true; - break; - } - - let current_time = Math.round(new Date().getTime() / 1000); - if (!result && current_time - timer_start > timer_max) { - driver.ctx.logger.warn( - `hit timeout waiting for device node to appear: ${device}` - ); - break; - } - } - - if (deviceCreated) { - device = await filesystem.realpath(device); - iscsiDevices.push(device); - - driver.ctx.logger.info( - `successfully logged into portal ${iscsiConnection.portal} and created device ${deviceByPath} with realpath ${device}` - ); - } - } - - // let things settle - // this will help in dm scenarios - await sleep(2000); - - // filter duplicates - iscsiDevices = iscsiDevices.filter((value, index, self) => { - return self.indexOf(value) === index; - }); - - // only throw an error if we were not able to attach to *any* devices - if (iscsiDevices.length < 1) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unable to attach any iscsi devices` - ); - } - - if (iscsiDevices.length != iscsiConnections.length) { - driver.ctx.logger.warn( - `failed to attach all iscsi devices/targets/portals` - ); - - // TODO: allow a parameter to control this behavior in some form - if (false) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unable to attach all iscsi devices` - ); - } - } - - // compare all device-mapper slaves with the newly created devices - // if any of the new devices are device-mapper slaves treat this as a - // multipath scenario - let allDeviceMapperSlaves = - await filesystem.getAllDeviceMapperSlaveDevices(); - let commonDevices = allDeviceMapperSlaves.filter((value) => - iscsiDevices.includes(value) - ); - - const useMultipath = - iscsiConnections.length > 1 || commonDevices.length > 0; - - // discover multipath device to use - if (useMultipath) { - device = await filesystem.getDeviceMapperDeviceFromSlaves( - iscsiDevices, - false - ); - - if (!device) { - throw new GrpcError( - grpc.status.UNKNOWN, - `failed to discover multipath device` - ); - } - } - break; - case "hostpath": - result = await mount.pathIsMounted(staging_target_path); - // if not mounted, mount + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + // csi spec stipulates that staging_target_path is a directory even for block mounts + result = await filesystem.pathExists(staging_target_path); if (!result) { - await mount.bindMount(volume_context.path, staging_target_path); - return {}; - } else { - return {}; + await filesystem.mkdir(staging_target_path, ["-p", "-m", "0750"]); } - break; - case "oneclient": - let oneclient = driver.getDefaultOneClientInstance(); - device = "oneclient"; - result = await mount.deviceIsMountedAtPath(device, staging_target_path); - if (result) { - return {}; - } + // get the `device` set + switch (node_attach_driver) { + case "nfs": + case "lustre": + device = `${volume_context.server}:${volume_context.share}`; + break; + case "smb": + device = `//${volume_context.server}/${volume_context.share}`; - if (volume_context.space_names) { - volume_context.space_names.split(",").forEach((space) => { - mount_flags.push("--space", space); - }); - } + // if not present add guest + let has_username = mount_flags.some((element) => { + element = element.trim().toLowerCase(); + return element.startsWith("username="); + }); - if (volume_context.space_ids) { - volume_context.space_ids.split(",").forEach((space) => { - mount_flags.push("--space-id", space); - }); - } - - if (normalizedSecrets.token) { - mount_flags.push("-t", normalizedSecrets.token); - } else { - if (volume_context.token) { - mount_flags.push("-t", volume_context.token); - } - } - - result = await oneclient.mount( - staging_target_path, - ["-H", volume_context.server].concat(mount_flags) - ); - - if (result) { - return {}; - } - - throw new GrpcError( - grpc.status.UNKNOWN, - `failed to mount oneclient: ${volume_context.server}` - ); - - break; - case "zfs-local": - // TODO: make this a geneic zb instance (to ensure works with node-manual driver) - const zb = driver.getDefaultZetabyteInstance(); - result = await zb.zfs.get(`${volume_context.zfs_asset_name}`, [ - "type", - "mountpoint", - ]); - result = result[`${volume_context.zfs_asset_name}`]; - switch (result.type.value) { - case "filesystem": - if (result.mountpoint.value != "legacy") { - // zfs set mountpoint=legacy - // zfs inherit mountpoint - await zb.zfs.set(`${volume_context.zfs_asset_name}`, { - mountpoint: "legacy", + // prevents driver from hanging on stdin waiting for a password to be entered at the cli + if (!has_username) { + let has_guest = mount_flags.some((element) => { + element = element.trim().toLowerCase(); + return element === "guest"; }); - } - device = `${volume_context.zfs_asset_name}`; - if (!fs_type) { - fs_type = "zfs"; + + if (!has_guest) { + mount_flags.push("guest"); + } + + if (volume_mount_group) { + mount_flags.push(`gid=${volume_mount_group}`); + } } break; - case "volume": - device = `/dev/zvol/${volume_context.zfs_asset_name}`; + case "iscsi": + let portals = []; + if (volume_context.portal) { + portals.push(volume_context.portal.trim()); + } + + if (volume_context.portals) { + volume_context.portals.split(",").forEach((portal) => { + portals.push(portal.trim()); + }); + } + + // ensure full portal value + portals = portals.map((value) => { + if (!value.includes(":")) { + value += ":3260"; + } + + return value.trim(); + }); + + // ensure unique entries only + portals = [...new Set(portals)]; + + // stores actual device paths after iscsi login + let iscsiDevices = []; + + // stores configuration of targets/iqn/luns to connect to + let iscsiConnections = []; + for (let portal of portals) { + iscsiConnections.push({ + portal, + iqn: volume_context.iqn, + lun: volume_context.lun, + }); + } + + /** + * TODO: allow sending in iscsiConnection in a raw/manual format + * TODO: allow option to determine if send_targets should be invoked + * TODO: allow option to control whether nodedb entry should be created by driver + * TODO: allow option to control whether nodedb entry should be deleted by driver + */ + + for (let iscsiConnection of iscsiConnections) { + // create DB entry + // https://library.netapp.com/ecmdocs/ECMP1654943/html/GUID-8EC685B4-8CB6-40D8-A8D5-031A3899BCDC.html + // put these options in place to force targets managed by csi to be explicitly attached (in the case of unclearn shutdown etc) + let nodeDB = { + "node.startup": "manual", + //"node.session.scan": "manual", + }; + const nodeDBKeyPrefix = "node-db."; + for (const key in normalizedSecrets) { + if (key.startsWith(nodeDBKeyPrefix)) { + nodeDB[key.substr(nodeDBKeyPrefix.length)] = + normalizedSecrets[key]; + } + } + + // create 'DB' entry + await GeneralUtils.retry(5, 2000, async () => { + await iscsi.iscsiadm.createNodeDBEntry( + iscsiConnection.iqn, + iscsiConnection.portal, + nodeDB + ); + }); + + // login + await GeneralUtils.retry(15, 2000, async () => { + await iscsi.iscsiadm.login( + iscsiConnection.iqn, + iscsiConnection.portal + ); + }); + + // get associated session + let session = await iscsi.iscsiadm.getSession( + iscsiConnection.iqn, + iscsiConnection.portal + ); + + // rescan in scenarios when login previously occurred but volumes never appeared + await iscsi.iscsiadm.rescanSession(session); + + // find device name + device = `/dev/disk/by-path/ip-${iscsiConnection.portal}-iscsi-${iscsiConnection.iqn}-lun-${iscsiConnection.lun}`; + let deviceByPath = device; + + // can take some time for device to show up, loop for some period + result = await filesystem.pathExists(device); + let timer_start = Math.round(new Date().getTime() / 1000); + let timer_max = 30; + let deviceCreated = result; + while (!result) { + await GeneralUtils.sleep(2000); + result = await filesystem.pathExists(device); + + if (result) { + deviceCreated = true; + break; + } + + let current_time = Math.round(new Date().getTime() / 1000); + if (!result && current_time - timer_start > timer_max) { + driver.ctx.logger.warn( + `hit timeout waiting for device node to appear: ${device}` + ); + break; + } + } + + if (deviceCreated) { + device = await filesystem.realpath(device); + iscsiDevices.push(device); + + driver.ctx.logger.info( + `successfully logged into portal ${iscsiConnection.portal} and created device ${deviceByPath} with realpath ${device}` + ); + } + } + + // let things settle + // this will help in dm scenarios + await GeneralUtils.sleep(2000); + + // filter duplicates + iscsiDevices = iscsiDevices.filter((value, index, self) => { + return self.indexOf(value) === index; + }); + + // only throw an error if we were not able to attach to *any* devices + if (iscsiDevices.length < 1) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unable to attach any iscsi devices` + ); + } + + if (iscsiDevices.length != iscsiConnections.length) { + driver.ctx.logger.warn( + `failed to attach all iscsi devices/targets/portals` + ); + + // TODO: allow a parameter to control this behavior in some form + if (false) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unable to attach all iscsi devices` + ); + } + } + + // compare all device-mapper slaves with the newly created devices + // if any of the new devices are device-mapper slaves treat this as a + // multipath scenario + let allDeviceMapperSlaves = + await filesystem.getAllDeviceMapperSlaveDevices(); + let commonDevices = allDeviceMapperSlaves.filter((value) => + iscsiDevices.includes(value) + ); + + const useMultipath = + iscsiConnections.length > 1 || commonDevices.length > 0; + + // discover multipath device to use + if (useMultipath) { + device = await filesystem.getDeviceMapperDeviceFromSlaves( + iscsiDevices, + false + ); + + if (!device) { + throw new GrpcError( + grpc.status.UNKNOWN, + `failed to discover multipath device` + ); + } + } + + break; + case "hostpath": + result = await mount.pathIsMounted(staging_target_path); + // if not mounted, mount + if (!result) { + await mount.bindMount(volume_context.path, staging_target_path); + return {}; + } else { + return {}; + } + + break; + case "oneclient": + let oneclient = driver.getDefaultOneClientInstance(); + device = "oneclient"; + result = await mount.deviceIsMountedAtPath( + device, + staging_target_path + ); + if (result) { + return {}; + } + + if (volume_context.space_names) { + volume_context.space_names.split(",").forEach((space) => { + mount_flags.push("--space", space); + }); + } + + if (volume_context.space_ids) { + volume_context.space_ids.split(",").forEach((space) => { + mount_flags.push("--space-id", space); + }); + } + + if (normalizedSecrets.token) { + mount_flags.push("-t", normalizedSecrets.token); + } else { + if (volume_context.token) { + mount_flags.push("-t", volume_context.token); + } + } + + result = await oneclient.mount( + staging_target_path, + ["-H", volume_context.server].concat(mount_flags) + ); + + if (result) { + return {}; + } + + throw new GrpcError( + grpc.status.UNKNOWN, + `failed to mount oneclient: ${volume_context.server}` + ); + + break; + case "zfs-local": + // TODO: make this a geneic zb instance (to ensure works with node-manual driver) + const zb = driver.getDefaultZetabyteInstance(); + result = await zb.zfs.get(`${volume_context.zfs_asset_name}`, [ + "type", + "mountpoint", + ]); + result = result[`${volume_context.zfs_asset_name}`]; + switch (result.type.value) { + case "filesystem": + if (result.mountpoint.value != "legacy") { + // zfs set mountpoint=legacy + // zfs inherit mountpoint + await zb.zfs.set(`${volume_context.zfs_asset_name}`, { + mountpoint: "legacy", + }); + } + device = `${volume_context.zfs_asset_name}`; + if (!fs_type) { + fs_type = "zfs"; + } + break; + case "volume": + device = `/dev/zvol/${volume_context.zfs_asset_name}`; + break; + default: + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown zfs asset type: ${result.type.value}` + ); + } break; default: throw new GrpcError( - grpc.status.UNKNOWN, - `unknown zfs asset type: ${result.type.value}` + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` ); } - break; - default: - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `unknown/unsupported node_attach_driver: ${node_attach_driver}` - ); - } - switch (access_type) { - case "mount": - let is_block = false; - switch (node_attach_driver) { - case "iscsi": - is_block = true; - break; - case "zfs-local": - is_block = device.startsWith("/dev/zvol/"); - break; - } - - if (is_block) { - // block specific logic - if (!fs_type) { - fs_type = "ext4"; - } - - if (await filesystem.isBlockDevice(device)) { - // format - result = await filesystem.deviceIsFormatted(device); - if (!result) { - let formatOptions = _.get( - driver.options.node.format, - [fs_type, "customOptions"], - [] - ); - if (!Array.isArray(formatOptions)) { - formatOptions = []; - } - await filesystem.formatDevice(device, fs_type, formatOptions); + // deal with `device` now that we have one + switch (access_type) { + case "mount": + let is_block = false; + switch (node_attach_driver) { + case "iscsi": + is_block = true; + break; + case "zfs-local": + is_block = device.startsWith("/dev/zvol/"); + break; } - let fs_info = await filesystem.getDeviceFilesystemInfo(device); - fs_type = fs_info.type; + // format device + if (is_block) { + // block specific logic + if (!fs_type) { + fs_type = "ext4"; + } - // fsck + let partition_count = + await filesystem.getBlockDevicePartitionCount(device); + if (partition_count > 0) { + // data partion MUST be the last partition on the drive + // to properly support expand/resize operations + device = await filesystem.getBlockDeviceLastPartition(device); + driver.ctx.logger.debug( + `device has partitions, mount device is: ${device}` + ); + + await filesystem.expandPartition(device); + } + + if (fs_type == "ntfs") { + if (partition_count < 1) { + // gpt is what csi-proxy uses by default + let ntfs_partition_label = "gpt"; + switch (ntfs_partition_label.toLowerCase()) { + case "dos": + // partion dos + await filesystem.partitionDevice(device, "dos", "07"); + break; + case "gpt": + // partion gpt + await filesystem.partitionDeviceWindows(device); + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported ntfs_partition_label: ${ntfs_partition_label}` + ); + } + device = await filesystem.getBlockDeviceLargestPartition( + device + ); + } + } + + if (await filesystem.isBlockDevice(device)) { + // format + result = await filesystem.deviceIsFormatted(device); + if (!result) { + let formatOptions = _.get( + driver.options.node.format, + [fs_type, "customOptions"], + [] + ); + if (!Array.isArray(formatOptions)) { + formatOptions = []; + } + await filesystem.formatDevice(device, fs_type, formatOptions); + } + + let fs_info = await filesystem.getDeviceFilesystemInfo(device); + fs_type = fs_info.type; + + // fsck + result = await mount.deviceIsMountedAtPath( + device, + staging_target_path + ); + if (!result) { + // https://github.com/democratic-csi/democratic-csi/issues/52#issuecomment-768463401 + let checkFilesystem = + driver.options.node.mount.checkFilesystem[fs_type] || {}; + if (checkFilesystem.enabled) { + await filesystem.checkFilesystem( + device, + fs_type, + checkFilesystem.customOptions || [], + checkFilesystem.customFilesystemOptions || [] + ); + } + } + } + } + + // set default fs_type if still unset + if (!fs_type) { + switch (node_attach_driver) { + case "nfs": + fs_type = "nfs"; + break; + case "lustre": + fs_type = "lustre"; + break; + case "smb": + fs_type = "cifs"; + break; + case "iscsi": + fs_type = "ext4"; + break; + default: + break; + } + } + + // mount `device` result = await mount.deviceIsMountedAtPath( device, staging_target_path ); if (!result) { - // https://github.com/democratic-csi/democratic-csi/issues/52#issuecomment-768463401 - let checkFilesystem = - driver.options.node.mount.checkFilesystem[fs_type] || {}; - if (checkFilesystem.enabled) { - await filesystem.checkFilesystem( - device, - fs_type, - checkFilesystem.customOptions || [], - checkFilesystem.customFilesystemOptions || [] - ); + // expand fs if necessary + if (await filesystem.isBlockDevice(device)) { + // go ahead and expand fs (this covers cloned setups where expand is not explicitly invoked) + switch (fs_type) { + case "exfat": + case "ntfs": + case "vfat": + //await filesystem.checkFilesystem(device, fs_info.type); + await filesystem.expandFilesystem(device, fs_type); + break; + } + } + + let mount_fs_type = fs_type; + if (mount_fs_type == "ntfs") { + mount_fs_type = "ntfs3"; + } + + // handle volume_mount_group where appropriate + if (volume_mount_group) { + switch (fs_type) { + case "exfat": + case "ntfs": + case "vfat": + mount_flags.push(`gid=${volume_mount_group}`); + break; + } + } + + switch (fs_type) { + case "xfs": + // https://github.com/democratic-csi/democratic-csi/issues/191 + // to avoid issues with cloned volumes + mount_flags.push(`nouuid`); + break; + } + + await mount.mount( + device, + staging_target_path, + ["-t", mount_fs_type].concat(["-o", mount_flags.join(",")]) + ); + } + + // expand fs if necessary + if (await filesystem.isBlockDevice(device)) { + // go ahead and expand fs (this covers cloned setups where expand is not explicitly invoked) + switch (fs_type) { + case "ext3": + case "ext4": + case "ext4dev": + //await filesystem.checkFilesystem(device, fs_info.type); + try { + await filesystem.expandFilesystem(device, fs_type); + } catch (err) { + // mount is clean and rw, but it will not expand until clean umount has been done + // failed to execute filesystem command: resize2fs /dev/sda, response: {"code":1,"stdout":"Couldn't find valid filesystem superblock.\n","stderr":"resize2fs 1.44.5 (15-Dec-2018)\nresize2fs: Superblock checksum does not match superblock while trying to open /dev/sda\n"} + // /dev/sda on /var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-4a80757e-5e87-475d-826f-44fcc4719348/globalmount type ext4 (rw,relatime,stripe=256) + if ( + err.code == 1 && + err.stdout.includes("find valid filesystem superblock") && + err.stderr.includes("checksum does not match superblock") + ) { + driver.ctx.logger.warn( + `successful mount, unsuccessful fs resize: attempting abnormal umount/mount/resize2fs to clear things up ${staging_target_path} (${device})` + ); + + // try an unmount/mount/fsck cycle again just to clean things up + await mount.umount(staging_target_path, []); + await mount.mount( + device, + staging_target_path, + ["-t", fs_type].concat(["-o", mount_flags.join(",")]) + ); + await filesystem.expandFilesystem(device, fs_type); + } else { + throw err; + } + } + break; + case "btrfs": + case "xfs": + //await filesystem.checkFilesystem(device, fs_info.type); + await filesystem.expandFilesystem( + staging_target_path, + fs_type + ); + break; + case "exfat": + case "ntfs": + case "vfat": + // noop + break; + default: + // unsupported filesystem + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `unsupported/unknown filesystem ${fs_type}` + ); } } - } - } - result = await mount.deviceIsMountedAtPath(device, staging_target_path); - if (!result) { - if (!fs_type) { - switch (node_attach_driver) { - case "nfs": - fs_type = "nfs"; - break; - case "lustre": - fs_type = "lustre"; - break; - case "smb": - fs_type = "cifs"; - break; - case "iscsi": - fs_type = "ext4"; - break; - default: - break; + break; + case "block": + //result = await mount.deviceIsMountedAtPath(device, block_path); + result = await mount.deviceIsMountedAtPath("dev", block_path); + if (!result) { + result = await filesystem.pathExists(staging_target_path); + if (!result) { + await filesystem.mkdir(staging_target_path, [ + "-p", + "-m", + "0750", + ]); + } + + result = await filesystem.pathExists(block_path); + if (!result) { + await filesystem.touch(block_path); + } + + await mount.bindMount(device, block_path, [ + "-o", + bind_mount_flags.join(","), + ]); } - } - await mount.mount( - device, - staging_target_path, - ["-t", fs_type].concat(["-o", mount_flags.join(",")]) + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported access_type: ${access_type}` + ); + } + break; + case NODE_OS_DRIVER_WINDOWS: + // sanity check node_attach_driver + if (!["smb", "iscsi", "hostpath"].includes(node_attach_driver)) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `windows does not work with node_attach_driver: ${node_attach_driver}` ); } - if (await filesystem.isBlockDevice(device)) { - // go ahead and expand fs (this covers cloned setups where expand is not explicitly invoked) - switch (fs_type) { - case "ext4": - case "ext3": - case "ext4dev": - //await filesystem.checkFilesystem(device, fs_info.type); - try { - await filesystem.expandFilesystem(device, fs_type); - } catch (err) { - // mount is clean and rw, but it will not expand until clean umount has been done - // failed to execute filesystem command: resize2fs /dev/sda, response: {"code":1,"stdout":"Couldn't find valid filesystem superblock.\n","stderr":"resize2fs 1.44.5 (15-Dec-2018)\nresize2fs: Superblock checksum does not match superblock while trying to open /dev/sda\n"} - // /dev/sda on /var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-4a80757e-5e87-475d-826f-44fcc4719348/globalmount type ext4 (rw,relatime,stripe=256) - if ( - err.code == 1 && - err.stdout.includes("find valid filesystem superblock") && - err.stderr.includes("checksum does not match superblock") - ) { - driver.ctx.logger.warn( - `successful mount, unsuccessful fs resize: attempting abnormal umount/mount/resize2fs to clear things up ${staging_target_path} (${device})` - ); - - // try an unmount/mount/fsck cycle again just to clean things up - await mount.umount(staging_target_path, []); - await mount.mount( - device, - staging_target_path, - ["-t", fs_type].concat(["-o", mount_flags.join(",")]) - ); - await filesystem.expandFilesystem(device, fs_type); - } else { - throw err; - } - } - break; - case "btrfs": - case "xfs": - //await filesystem.checkFilesystem(device, fs_info.type); - await filesystem.expandFilesystem(staging_target_path, fs_type); - break; - default: - // unsupported filesystem - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `unsupported/unknown filesystem ${fs_type}` - ); - } + // sanity check fs_type + if (fs_type && !["ntfs", "cifs"].includes(fs_type)) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `windows does not work with fs_type: ${fs_type}` + ); } + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); + let win_staging_target_path = + filesystem.covertUnixSeparatorToWindowsSeparator(staging_target_path); + + switch (node_attach_driver) { + case "smb": + device = `//${volume_context.server}/${volume_context.share}`; + const username = driver.getMountFlagValue(mount_flags, "username"); + const password = driver.getMountFlagValue(mount_flags, "password"); + + if (!username || !password) { + throw new Error("username and password required"); + } + + /** + * smb mount creates a link at this location and if the dir already exists + * it explodes + * + * if path exists but is NOT symlink delete it + */ + result = await filesystem.pathExists(win_staging_target_path); + + if (result) { + if (!(await filesystem.isSymbolicLink(win_staging_target_path))) { + fs.rmdirSync(win_staging_target_path); + } else { + result = await wutils.GetItem(win_staging_target_path); + // UNC\172.29.0.111\tank_k8s_test_PVC_111\ + let target = _.get(result, "Target.[0]", ""); + let parts = target.split("\\"); + if ( + parts[1] != volume_context.server && + parts[2] != volume_context.share + ) { + throw new Error( + `${target} mounted already at ${win_staging_target_path}` + ); + } else { + // finish early, assured we have what we need + return {}; + } + } + } + + try { + result = await wutils.GetSmbGlobalMapping( + filesystem.covertUnixSeparatorToWindowsSeparator(device) + ); + if (!result) { + // check for mount option cache=none and set -UseWriteThrough $true + await wutils.NewSmbGlobalMapping( + filesystem.covertUnixSeparatorToWindowsSeparator(device), + `${volume_context.server}\\${username}`, + password + ); + } + } catch (e) { + let details = _.get(e, "stderr", ""); + if (!details.includes("0x80041001")) { + throw e; + } + } + try { + await wutils.NewSmbLink( + filesystem.covertUnixSeparatorToWindowsSeparator(device), + win_staging_target_path + ); + } catch (e) { + let details = _.get(e, "stderr", ""); + if (!details.includes("ResourceExists")) { + throw e; + } else { + if ( + !(await filesystem.isSymbolicLink(win_staging_target_path)) + ) { + throw new Error("staging path exists but is not symlink"); + } + } + } + break; + case "iscsi": + switch (access_type) { + case "mount": + let portals = []; + if (volume_context.portal) { + portals.push(volume_context.portal.trim()); + } + + if (volume_context.portals) { + volume_context.portals.split(",").forEach((portal) => { + portals.push(portal.trim()); + }); + } + + // ensure full portal value + portals = portals.map((value) => { + if (!value.includes(":")) { + value += ":3260"; + } + + return value.trim(); + }); + + // ensure unique entries only + portals = [...new Set(portals)]; + + // stores configuration of targets/iqn/luns to connect to + let iscsiConnections = []; + for (let portal of portals) { + iscsiConnections.push({ + portal, + iqn: volume_context.iqn, + lun: volume_context.lun, + }); + } + + let successful_logins = 0; + let multipath = iscsiConnections.length > 1; + + // no multipath support yet + // https://github.com/kubernetes-csi/csi-proxy/pull/99 + for (let iscsiConnection of iscsiConnections) { + // add target portal + let parts = iscsiConnection.portal.split(":"); + let target_address = parts[0]; + let target_port = parts[1] || "3260"; + + // this is idempotent + try { + await wutils.NewIscsiTargetPortal( + target_address, + target_port + ); + } catch (e) { + driver.ctx.logger.warn( + `failed adding target portal: ${JSON.stringify( + iscsiConnection + )}: ${e.stderr}` + ); + if (!multipath) { + throw e; + } else { + continue; + } + } + + // login + try { + let auth_type = "NONE"; + let chap_username = ""; + let chap_secret = ""; + if ( + normalizedSecrets[ + "node-db.node.session.auth.authmethod" + ] == "CHAP" + ) { + // set auth_type + if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets[ + "node-db.node.session.auth.password" + ] && + normalizedSecrets[ + "node-db.node.session.auth.username_in" + ] && + normalizedSecrets[ + "node-db.node.session.auth.password_in" + ] + ) { + auth_type = "MUTUAL_CHAP"; + } else if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets["node-db.node.session.auth.password"] + ) { + auth_type = "ONE_WAY_CHAP"; + } + + // set credentials + if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets["node-db.node.session.auth.password"] + ) { + chap_username = + normalizedSecrets[ + "node-db.node.session.auth.username" + ]; + + chap_secret = + normalizedSecrets[ + "node-db.node.session.auth.password" + ]; + } + } + await wutils.ConnectIscsiTarget( + target_address, + target_port, + iscsiConnection.iqn, + auth_type, + chap_username, + chap_secret, + multipath + ); + } catch (e) { + let details = _.get(e, "stderr", ""); + if ( + !details.includes( + "The target has already been logged in via an iSCSI session" + ) + ) { + driver.ctx.logger.warn( + `failed connection to ${JSON.stringify( + iscsiConnection + )}: ${e.stderr}` + ); + if (!multipath) { + throw e; + } + } + } + + // discover? + //await csiProxyClient.executeRPC("iscsi", "DiscoverTargetPortal", { + // target_portal, + //}); + successful_logins++; + } + + if (iscsiConnections.length != successful_logins) { + driver.ctx.logger.warn( + `failed to login to all portals: total - ${iscsiConnections.length}, logins - ${successful_logins}` + ); + } + + // let things settle + // this will help in dm scenarios + await GeneralUtils.sleep(2000); + + // rescan + await wutils.UpdateHostStorageCache(); + + // get device + let disks = await wutils.GetTargetDisksByIqnLun( + volume_context.iqn, + volume_context.lun + ); + let disk; + + if (disks.length == 0) { + throw new GrpcError( + grpc.status.UNAVAILABLE, + `0 disks created by ${successful_logins} successful logins` + ); + } + + if (disks.length > 1) { + if (multipath) { + let disk_number_set = new Set(); + disks.forEach((i_disk) => { + disk_number_set.add(i_disk.DiskNumber); + }); + if (disk_number_set.length > 1) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + "using multipath but mpio is not properly configured (multiple disk numbers with same iqn/lun)" + ); + } + // find first disk that is online + disk = disks.find((i_disk) => { + return i_disk.OperationalStatus == "Online"; + }); + + if (!disk) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + "using multipath but mpio is not properly configured (failed to detect an online disk)" + ); + } + } else { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `not using multipath but discovered ${disks.length} disks (multiple disks with same iqn/lun)` + ); + } + } else { + disk = disks[0]; + } + + if (multipath && !disk.Path.startsWith("\\\\?\\mpio#")) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + "using multipath but mpio is not properly configured (discover disk is not an mpio disk)" + ); + } + + // needs to be initialized + await wutils.PartitionDisk(disk.DiskNumber); + + let partition = await wutils.GetLastPartitionByDiskNumber( + disk.DiskNumber + ); + + let volume = await wutils.GetVolumeByDiskNumberPartitionNumber( + disk.DiskNumber, + partition.PartitionNumber + ); + if (!volume) { + throw new Error("failed to create/discover volume for disk"); + } + + result = await wutils.VolumeIsFormatted(volume.UniqueId); + if (!result) { + // format device + await wutils.FormatVolume(volume.UniqueId); + } + + result = await wutils.GetItem(win_staging_target_path); + if (!result) { + fs.mkdirSync(win_staging_target_path, { + recursive: true, + mode: "755", + }); + result = await wutils.GetItem(win_staging_target_path); + } + + if (!volume.UniqueId.includes(result.Target[0])) { + // mount up! + await wutils.MountVolume( + volume.UniqueId, + win_staging_target_path + ); + } + break; + case "block": + default: + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `access_type ${access_type} unsupported` + ); + } + break; + case "hostpath": + // if exists already delete if folder, return if symlink + if (await filesystem.pathExists(win_staging_target_path)) { + // remove pre-created dir by CO + if (!(await filesystem.isSymbolicLink(win_staging_target_path))) { + fs.rmdirSync(win_staging_target_path); + } else { + // assume symlink points to the correct location + return {}; + } + } + + // create symlink + fs.symlinkSync( + filesystem.covertUnixSeparatorToWindowsSeparator( + volume_context.path + ), + win_staging_target_path + ); + return {}; + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } break; - case "block": - //result = await mount.deviceIsMountedAtPath(device, block_path); - result = await mount.deviceIsMountedAtPath("dev", block_path); - if (!result) { - result = await filesystem.pathExists(staging_target_path); - if (!result) { - await filesystem.mkdir(staging_target_path, ["-p", "-m", "0750"]); - } + case NODE_OS_DRIVER_CSI_PROXY: + // sanity check node_attach_driver + if (!["smb", "iscsi"].includes(node_attach_driver)) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `csi-proxy does not work with node_attach_driver: ${node_attach_driver}` + ); + } - result = await filesystem.pathExists(block_path); - if (!result) { - await filesystem.touch(block_path); - } + // sanity check fs_type + if (fs_type && !["ntfs", "cifs"].includes(fs_type)) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `csi-proxy does not work with fs_type: ${fs_type}` + ); + } - await mount.bindMount(device, block_path, [ - "-o", - bind_mount_flags.join(","), - ]); + // load up the client instance + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); + + switch (node_attach_driver) { + case "smb": + /** + * smb mount creates a link at this location and if the dir already exists + * it explodes + * + * if path exists but is NOT symlink delete it + */ + result = await csiProxyClient.FilesystemPathExists( + staging_target_path + ); + if (result) { + result = await csiProxyClient.FilesystemIsSymlink( + staging_target_path + ); + if (!result) { + await csiProxyClient.executeRPC("filesystem", "Rmdir", { + path: staging_target_path, + }); + } + } + + device = `//${volume_context.server}/${volume_context.share}`; + const username = driver.getMountFlagValue(mount_flags, "username"); + const password = driver.getMountFlagValue(mount_flags, "password"); + + if (!username || !password) { + throw new Error("username and password required"); + } + + try { + await csiProxyClient.executeRPC("smb", "NewSmbGlobalMapping", { + // convert path separator for windows style path + remote_path: + filesystem.covertUnixSeparatorToWindowsSeparator(device), + local_path: staging_target_path, + username: `${volume_context.server}\\${username}`, + password, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("ResourceExists")) { + throw e; + } else { + // path should be a symlink if already present + result = await csiProxyClient.executeRPC( + "filesystem", + "IsSymlink", + { path: staging_target_path } + ); + if (!_.get(result, "is_symlink", false)) { + throw e; + } + } + } + + break; + case "iscsi": + switch (access_type) { + case "mount": + let portals = []; + if (volume_context.portal) { + portals.push(volume_context.portal.trim()); + } + + if (volume_context.portals) { + volume_context.portals.split(",").forEach((portal) => { + portals.push(portal.trim()); + }); + } + + // ensure full portal value + portals = portals.map((value) => { + if (!value.includes(":")) { + value += ":3260"; + } + + return value.trim(); + }); + + // ensure unique entries only + portals = [...new Set(portals)]; + + // stores actual device paths after iscsi login + let iscsiDevices = []; + + // stores configuration of targets/iqn/luns to connect to + let iscsiConnections = []; + for (let portal of portals) { + iscsiConnections.push({ + portal, + iqn: volume_context.iqn, + lun: volume_context.lun, + }); + } + + // no multipath support yet + // https://github.com/kubernetes-csi/csi-proxy/pull/99 + for (let iscsiConnection of iscsiConnections) { + // add target portal + let parts = iscsiConnection.portal.split(":"); + let target_address = parts[0]; + let target_port = parts[1] || "3260"; + let target_portal = { + target_address, + target_port, + }; + // this is idempotent + await csiProxyClient.executeRPC("iscsi", "AddTargetPortal", { + target_portal, + }); + + // login + try { + let auth_type = "NONE"; + let chap_username = ""; + let chap_secret = ""; + if ( + normalizedSecrets[ + "node-db.node.session.auth.authmethod" + ] == "CHAP" + ) { + // set auth_type + if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets[ + "node-db.node.session.auth.password" + ] && + normalizedSecrets[ + "node-db.node.session.auth.username_in" + ] && + normalizedSecrets[ + "node-db.node.session.auth.password_in" + ] + ) { + auth_type = "MUTUAL_CHAP"; + } else if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets["node-db.node.session.auth.password"] + ) { + auth_type = "ONE_WAY_CHAP"; + } + + // set credentials + if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets["node-db.node.session.auth.password"] + ) { + chap_username = + normalizedSecrets[ + "node-db.node.session.auth.username" + ]; + + chap_secret = + normalizedSecrets[ + "node-db.node.session.auth.password" + ]; + } + } + await csiProxyClient.executeRPC("iscsi", "ConnectTarget", { + target_portal, + iqn: iscsiConnection.iqn, + /** + * NONE + * ONE_WAY_CHAP + * MUTUAL_CHAP + */ + auth_type, + chap_username, + chap_secret, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if ( + !details.includes( + "The target has already been logged in via an iSCSI session" + ) + ) { + throw e; + } + } + + // discover? + //await csiProxyClient.executeRPC("iscsi", "DiscoverTargetPortal", { + // target_portal, + //}); + + // rescan + await csiProxyClient.executeRPC("disk", "Rescan"); + + // get device + result = await csiProxyClient.executeRPC( + "iscsi", + "GetTargetDisks", + { + target_portal, + iqn: iscsiConnection.iqn, + } + ); + + // TODO: this is a gross assumption since we currently only allow 1 lun per target + // iterate this response and find disk + //result = await csiProxyClient.executeRPC("disk", "ListDiskLocations"); + let diskIds = _.get(result, "diskIDs", []); + if (diskIds.length != 1) { + throw new Error( + `${diskIds.length} disks on the target, no way to know which is the relevant disk` + ); + } + let disk_number = diskIds[0]; + + result = await csiProxyClient.executeRPC( + "volume", + "ListVolumesOnDisk", + { disk_number } + ); + + let node_volume_id; + node_volume_id = + await csiProxyClient.getVolumeIdFromDiskNumber(disk_number); + + if (!node_volume_id) { + // this is technically idempotent call so should not hurt anything if already initialized + await csiProxyClient.executeRPC("disk", "PartitionDisk", { + disk_number, + }); + node_volume_id = + await csiProxyClient.getVolumeIdFromDiskNumber( + disk_number + ); + } + + if (!node_volume_id) { + throw new Error( + "failed to create/discover volume for disk" + ); + } + result = await csiProxyClient.executeRPC( + "volume", + "IsVolumeFormatted", + { volume_id: node_volume_id } + ); + + // format device + if (!result.formatted) { + await csiProxyClient.executeRPC("volume", "FormatVolume", { + volume_id: node_volume_id, + }); + } + + // ensure staging path present + result = await csiProxyClient.FilesystemPathExists( + staging_target_path + ); + if (!result) { + await csiProxyClient.executeRPC("filesystem", "Mkdir", { + path: staging_target_path, + }); + } + + // mount up! + try { + result = await csiProxyClient.executeRPC( + "volume", + "MountVolume", + { + volume_id: node_volume_id, + target_path: staging_target_path, + } + ); + } catch (e) { + // assume for now that if something is mounted in the location it the desired volume + let details = _.get(e, "details", ""); + if ( + !details.includes( + "The requested access path is already in use" + ) + ) { + throw e; + } + } + + // let things settle + // this will help in dm scenarios + await GeneralUtils.sleep(2000); + + // windows does not support multipath currently + // break if we make it this far + break; + } + + break; + case "block": + default: + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `access_type ${access_type} unsupported` + ); + } + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); } break; default: throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `unknown/unsupported access_type: ${access_type}` + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` ); } @@ -943,189 +2008,466 @@ class CsiBaseDriver { // TODO: use the x-* mount options to detect if we should delete target - try { - result = await mount.pathIsMounted(block_path); - } catch (err) { - /** - * on stalled fs such as nfs, even findmnt will return immediately for the base mount point - * so in the case of timeout here (base mount point and then a file/folder beneath it) we almost certainly are not a block device - * AND the fs is probably stalled - */ - if (err.timeout) { - driver.ctx.logger.warn( - `detected stale mount, attempting to force unmount: ${normalized_staging_path}` - ); - await mount.umount( - normalized_staging_path, - umount_args.concat(umount_force_extra_args) - ); - result = false; // assume we are *NOT* a block device at this point - } else { - throw err; - } - } + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + try { + result = await mount.pathIsMounted(block_path); + } catch (err) { + /** + * on stalled fs such as nfs, even findmnt will return immediately for the base mount point + * so in the case of timeout here (base mount point and then a file/folder beneath it) we almost certainly are not a block device + * AND the fs is probably stalled + */ + if (err.timeout) { + driver.ctx.logger.warn( + `detected stale mount, attempting to force unmount: ${normalized_staging_path}` + ); + await mount.umount( + normalized_staging_path, + umount_args.concat(umount_force_extra_args) + ); + result = false; // assume we are *NOT* a block device at this point + } else { + throw err; + } + } - if (result) { - is_block = true; - access_type = "block"; - block_device_info = await filesystem.getBlockDevice(block_path); - normalized_staging_path = block_path; - } else { - result = await mount.pathIsMounted(staging_target_path); - if (result) { - let device = await mount.getMountPointDevice(staging_target_path); - result = await filesystem.isBlockDevice(device); if (result) { is_block = true; - block_device_info = await filesystem.getBlockDevice(device); - } - } - } - - result = await mount.pathIsMounted(normalized_staging_path); - if (result) { - try { - result = await mount.umount(normalized_staging_path, umount_args); - } catch (err) { - if (err.timeout) { - driver.ctx.logger.warn( - `hit timeout waiting to unmount path: ${normalized_staging_path}` - ); - result = await mount.getMountDetails(normalized_staging_path); - switch (result.fstype) { - case "nfs": - case "nfs4": - driver.ctx.logger.warn( - `detected stale nfs filesystem, attempting to force unmount: ${normalized_staging_path}` - ); - result = await mount.umount( - normalized_staging_path, - umount_args.concat(umount_force_extra_args) - ); - break; - default: - throw err; - break; - } + access_type = "block"; + block_device_info = await filesystem.getBlockDevice(block_path); + normalized_staging_path = block_path; } else { - throw err; + result = await mount.pathIsMounted(staging_target_path); + if (result) { + let device = await mount.getMountPointDevice(staging_target_path); + result = await filesystem.isBlockDevice(device); + if (result) { + is_block = true; + block_device_info = await filesystem.getBlockDevice(device); + } + } } - } - } - if (is_block) { - let realBlockDeviceInfos = []; - // detect if is a multipath device - is_device_mapper = await filesystem.isDeviceMapperDevice( - block_device_info.path - ); - - if (is_device_mapper) { - let realBlockDevices = await filesystem.getDeviceMapperDeviceSlaves( - block_device_info.path - ); - for (const realBlockDevice of realBlockDevices) { - realBlockDeviceInfos.push( - await filesystem.getBlockDevice(realBlockDevice) - ); - } - } else { - realBlockDeviceInfos = [block_device_info]; - } - - // TODO: this could be made async to detach all simultaneously - for (const block_device_info_i of realBlockDeviceInfos) { - if (block_device_info_i.tran == "iscsi") { - // figure out which iscsi session this belongs to and logout - // scan /dev/disk/by-path/ip-*? - // device = `/dev/disk/by-path/ip-${volume_context.portal}-iscsi-${volume_context.iqn}-lun-${volume_context.lun}`; - // parse output from `iscsiadm -m session -P 3` - let sessions = await iscsi.iscsiadm.getSessionsDetails(); - for (let i = 0; i < sessions.length; i++) { - let session = sessions[i]; - let is_attached_to_session = false; - - if ( - session.attached_scsi_devices && - session.attached_scsi_devices.host && - session.attached_scsi_devices.host.devices - ) { - is_attached_to_session = - session.attached_scsi_devices.host.devices.some((device) => { - if (device.attached_scsi_disk == block_device_info_i.name) { + result = await mount.pathIsMounted(normalized_staging_path); + if (result) { + try { + result = await GeneralUtils.retry( + 10, + 0, + async () => { + return await mount.umount(normalized_staging_path, umount_args); + }, + { + minExecutionTime: 1000, + retryCondition: (err) => { + if (_.get(err, "stderr", "").includes("busy")) { return true; } - return false; - }); - } - - if (is_attached_to_session) { - let timer_start; - let timer_max; - - timer_start = Math.round(new Date().getTime() / 1000); - timer_max = 30; - let loggedOut = false; - while (!loggedOut) { - try { - await iscsi.iscsiadm.logout(session.target, [ - session.persistent_portal, - ]); - loggedOut = true; - } catch (err) { - await sleep(2000); - let current_time = Math.round(new Date().getTime() / 1000); - if (current_time - timer_start > timer_max) { - // not throwing error for now as future invocations would not enter code path anyhow - loggedOut = true; - //throw new GrpcError( - // grpc.status.UNKNOWN, - // `hit timeout trying to logout of iscsi target: ${session.persistent_portal}` - //); - } - } + }, } - - timer_start = Math.round(new Date().getTime() / 1000); - timer_max = 30; - let deletedEntry = false; - while (!deletedEntry) { - try { - await iscsi.iscsiadm.deleteNodeDBEntry( - session.target, - session.persistent_portal + ); + } catch (err) { + if (err.timeout) { + driver.ctx.logger.warn( + `hit timeout waiting to unmount path: ${normalized_staging_path}` + ); + result = await mount.getMountDetails(normalized_staging_path); + switch (result.fstype) { + case "nfs": + case "nfs4": + driver.ctx.logger.warn( + `detected stale nfs filesystem, attempting to force unmount: ${normalized_staging_path}` ); - deletedEntry = true; - } catch (err) { - await sleep(2000); - let current_time = Math.round(new Date().getTime() / 1000); - if (current_time - timer_start > timer_max) { - // not throwing error for now as future invocations would not enter code path anyhow - deletedEntry = true; - //throw new GrpcError( - // grpc.status.UNKNOWN, - // `hit timeout trying to delete iscsi node DB entry: ${session.target}, ${session.persistent_portal}` - //); + result = await mount.umount( + normalized_staging_path, + umount_args.concat(umount_force_extra_args) + ); + break; + default: + throw err; + } + } else { + throw err; + } + } + } + + if (is_block) { + let realBlockDeviceInfos = []; + // detect if is a multipath device + is_device_mapper = await filesystem.isDeviceMapperDevice( + block_device_info.path + ); + + if (is_device_mapper) { + let realBlockDevices = await filesystem.getDeviceMapperDeviceSlaves( + block_device_info.path + ); + for (const realBlockDevice of realBlockDevices) { + realBlockDeviceInfos.push( + await filesystem.getBlockDevice(realBlockDevice) + ); + } + } else { + realBlockDeviceInfos = [block_device_info]; + } + + // TODO: this could be made async to detach all simultaneously + for (const block_device_info_i of realBlockDeviceInfos) { + if (await filesystem.deviceIsIscsi(block_device_info_i.path)) { + let parent_block_device = await filesystem.getBlockDeviceParent( + block_device_info_i.path + ); + + // figure out which iscsi session this belongs to and logout + // scan /dev/disk/by-path/ip-*? + // device = `/dev/disk/by-path/ip-${volume_context.portal}-iscsi-${volume_context.iqn}-lun-${volume_context.lun}`; + // parse output from `iscsiadm -m session -P 3` + let sessions = await iscsi.iscsiadm.getSessionsDetails(); + for (let i = 0; i < sessions.length; i++) { + let session = sessions[i]; + let is_attached_to_session = false; + + if ( + session.attached_scsi_devices && + session.attached_scsi_devices.host && + session.attached_scsi_devices.host.devices + ) { + is_attached_to_session = + session.attached_scsi_devices.host.devices.some( + (device) => { + if ( + device.attached_scsi_disk == parent_block_device.name + ) { + return true; + } + return false; + } + ); + } + + if (is_attached_to_session) { + let timer_start; + let timer_max; + + timer_start = Math.round(new Date().getTime() / 1000); + timer_max = 30; + let loggedOut = false; + while (!loggedOut) { + try { + await iscsi.iscsiadm.logout(session.target, [ + session.persistent_portal, + ]); + loggedOut = true; + } catch (err) { + await GeneralUtils.sleep(2000); + let current_time = Math.round( + new Date().getTime() / 1000 + ); + if (current_time - timer_start > timer_max) { + // not throwing error for now as future invocations would not enter code path anyhow + loggedOut = true; + //throw new GrpcError( + // grpc.status.UNKNOWN, + // `hit timeout trying to logout of iscsi target: ${session.persistent_portal}` + //); + } + } + } + + timer_start = Math.round(new Date().getTime() / 1000); + timer_max = 30; + let deletedEntry = false; + while (!deletedEntry) { + try { + await iscsi.iscsiadm.deleteNodeDBEntry( + session.target, + session.persistent_portal + ); + deletedEntry = true; + } catch (err) { + await GeneralUtils.sleep(2000); + let current_time = Math.round( + new Date().getTime() / 1000 + ); + if (current_time - timer_start > timer_max) { + // not throwing error for now as future invocations would not enter code path anyhow + deletedEntry = true; + //throw new GrpcError( + // grpc.status.UNKNOWN, + // `hit timeout trying to delete iscsi node DB entry: ${session.target}, ${session.persistent_portal}` + //); + } + } } } } } } } - } - } - if (access_type == "block") { - // remove touched file - result = await filesystem.pathExists(block_path); - if (result) { - result = await filesystem.rm(block_path); - } - } + if (access_type == "block") { + // remove touched file + result = await filesystem.pathExists(block_path); + if (result) { + result = await GeneralUtils.retry( + 30, + 0, + async () => { + return await filesystem.rm(block_path); + }, + { + minExecutionTime: 1000, + retryCondition: (err) => { + if (_.get(err, "stderr", "").includes("busy")) { + return true; + } + }, + } + ); + } + } - result = await filesystem.pathExists(staging_target_path); - if (result) { - result = await filesystem.rmdir(staging_target_path); + result = await filesystem.pathExists(staging_target_path); + if (result) { + result = await GeneralUtils.retry( + 30, + 0, + async () => { + return await filesystem.rmdir(staging_target_path); + }, + { + minExecutionTime: 1000, + retryCondition: (err) => { + if (_.get(err, "stderr", "").includes("busy")) { + return true; + } + }, + } + ); + } + break; + case NODE_OS_DRIVER_WINDOWS: { + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); + + let win_normalized_staging_path = + filesystem.covertUnixSeparatorToWindowsSeparator( + normalized_staging_path + ); + + async function removePath(p) { + // remove staging path + try { + fs.rmdirSync(p); + } catch (e) { + if (e.code !== "ENOENT") { + throw e; + } + } + } + + let node_attach_driver; + let win_volume_id; + + result = await wutils.GetItem(win_normalized_staging_path); + if (result) { + let target = _.get(result, "Target.[0]", ""); + if (target.startsWith("UNC")) { + node_attach_driver = "smb"; + } + if (target.startsWith("Volume")) { + win_volume_id = `\\\\?\\${target}`; + if (await wutils.VolumeIsIscsi(win_volume_id)) { + node_attach_driver = "iscsi"; + } + } + + if (!node_attach_driver) { + // nothing we care about + node_attach_driver = "bypass"; + } + + switch (node_attach_driver) { + case "smb": + // remove symlink *before* disconnecting + await removePath(win_normalized_staging_path); + let parts = target.split("\\"); + // only remove global mapping if we certain there may not be other + // consumers of the mapping/share (ie: smb-client scenarios, etc) + if (!parts[3]) { + await wutils.RemoveSmbGlobalMapping( + `\\\\${parts[1]}\\${parts[2]}` + ); + } + break; + case "iscsi": + // write volume cache + await wutils.WriteVolumeCache(win_volume_id); + + // unmount volume + await wutils.UnmountVolume( + win_volume_id, + win_normalized_staging_path + ); + + // find sessions associated with volume/disks + let sessions = await wutils.GetIscsiSessionsByVolumeId( + win_volume_id + ); + + // logout of sessions + for (let session of sessions) { + await wutils.DisconnectIscsiTargetByNodeAddress( + session.TargetNodeAddress + ); + } + + // delete target/target portal/etc + // do NOT do this now as removing the portal will remove all targets associated with it + break; + case "hostpath": + // allow below code to remove symlink + break; + case "bypass": + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + } + + // remove staging path + await removePath(win_normalized_staging_path); + break; + } + case NODE_OS_DRIVER_CSI_PROXY: + // load up the client instance + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); + // for testing purposes + const volume_context = await driver.getDerivedVolumeContext(call); + if (!volume_context) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `unable to retrieve volume_context for volume: ${volume_id}` + ); + } + + const node_attach_driver = volume_context.node_attach_driver; + + async function removePath(p) { + // remove staging path + try { + await csiProxyClient.executeRPC("filesystem", "Rmdir", { + path: p, + // remove all contents under the directory + //force: false, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if ( + !details.includes("The system cannot find the file specified") + ) { + throw e; + } + } + } + + switch (node_attach_driver) { + case "smb": + try { + await csiProxyClient.executeRPC("smb", "RemoveSmbGlobalMapping", { + remote_path: `\\\\${volume_context.server}\\${volume_context.share}`, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("No MSFT_SmbGlobalMapping objects found")) { + throw e; + } + } + + break; + case "iscsi": + let target_portal = { + target_address: volume_context.portal.split(":")[0], + target_port: volume_context.portal.split(":")[1] || 3260, + }; + + let iqn = volume_context.iqn; + let node_volume_id; + + // ok to be null/undefined + node_volume_id = await csiProxyClient.getVolumeIdFromIscsiTarget( + target_portal, + iqn + ); + + if (node_volume_id) { + // write volume cache + await csiProxyClient.executeRPC("volume", "WriteVolumeCache", { + volume_id: node_volume_id, + }); + + // umount first + try { + await csiProxyClient.executeRPC("volume", "UnmountVolume", { + volume_id: node_volume_id, + target_path: staging_target_path, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("The access path is not valid")) { + throw e; + } + } + } + + try { + await csiProxyClient.executeRPC("iscsi", "DisconnectTarget", { + target_portal, + iqn, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("ObjectNotFound")) { + throw e; + } + } + + // do NOT remove target portal etc, windows handles this quite differently than + // linux and removing the portal would remove all the targets/etc + /* + try { + await csiProxyClient.executeRPC("iscsi", "RemoveTargetPortal", { + target_portal, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("ObjectNotFound")) { + throw e; + } + } + */ + + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + + // remove staging path + await removePath(normalized_staging_path); + break; + default: + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` + ); } return {}; @@ -1181,109 +2523,273 @@ class CsiBaseDriver { if (readonly) bind_mount_flags.push("ro"); // , "x-democratic-csi.ro" - switch (node_attach_driver) { - case "nfs": - case "smb": - case "lustre": - case "oneclient": - case "hostpath": - case "iscsi": - case "zfs-local": - // ensure appropriate directories/files - switch (access_type) { - case "mount": - // ensure directory exists - result = await filesystem.pathExists(target_path); - if (!result) { - await filesystem.mkdir(target_path, ["-p", "-m", "0750"]); + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + switch (node_attach_driver) { + case "nfs": + case "smb": + case "lustre": + case "oneclient": + case "hostpath": + case "iscsi": + case "zfs-local": + // ensure appropriate directories/files + switch (access_type) { + case "mount": + // ensure directory exists + result = await filesystem.pathExists(target_path); + if (!result) { + await filesystem.mkdir(target_path, ["-p", "-m", "0750"]); + } + + break; + case "block": + // ensure target_path directory exists as target path should be a file + let target_dir = await filesystem.dirname(target_path); + result = await filesystem.pathExists(target_dir); + if (!result) { + await filesystem.mkdir(target_dir, ["-p", "-m", "0750"]); + } + + // ensure target file exists + result = await filesystem.pathExists(target_path); + if (!result) { + await filesystem.touch(target_path); + } + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unsupported/unknown access_type ${access_type}` + ); } - break; - case "block": - // ensure target_path directory exists as target path should be a file - let target_dir = await filesystem.dirname(target_path); - result = await filesystem.pathExists(target_dir); - if (!result) { - await filesystem.mkdir(target_dir, ["-p", "-m", "0750"]); + // ensure bind mount + if (staging_target_path) { + let normalized_staging_device; + let normalized_staging_path; + + if (access_type == "block") { + normalized_staging_path = staging_target_path + "/block_device"; + } else { + normalized_staging_path = staging_target_path; + } + + // sanity check to ensure the staged path is actually mounted + result = await mount.pathIsMounted(normalized_staging_path); + if (!result) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `staging path is not mounted: ${normalized_staging_path}` + ); + } + + result = await mount.pathIsMounted(target_path); + // if not mounted, mount + if (!result) { + await mount.bindMount(normalized_staging_path, target_path, [ + "-o", + bind_mount_flags.join(","), + ]); + } else { + // if is mounted, ensure proper source + if (access_type == "block") { + normalized_staging_device = "dev"; // special syntax for single file bind mounts + } else { + normalized_staging_device = await mount.getMountPointDevice( + staging_target_path + ); + } + result = await mount.deviceIsMountedAtPath( + normalized_staging_device, + target_path + ); + if (!result) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `it appears something else is already mounted at ${target_path}` + ); + } + } + + return {}; } - // ensure target file exists - result = await filesystem.pathExists(target_path); - if (!result) { - await filesystem.touch(target_path); - } - break; + // unsupported filesystem + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `only staged configurations are valid` + ); default: throw new GrpcError( grpc.status.INVALID_ARGUMENT, - `unsupported/unknown access_type ${access_type}` + `unknown/unsupported node_attach_driver: ${node_attach_driver}` ); } + break; + case NODE_OS_DRIVER_WINDOWS: + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); - // ensure bind mount - if (staging_target_path) { - let normalized_staging_device; - let normalized_staging_path; + switch (node_attach_driver) { + //case "nfs": + case "smb": + //case "lustre": + //case "oneclient": + case "hostpath": + case "iscsi": + //case "zfs-local": + // ensure appropriate directories/files + switch (access_type) { + case "mount": + break; + case "block": + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unsupported/unknown access_type ${access_type}` + ); + } - if (access_type == "block") { - normalized_staging_path = staging_target_path + "/block_device"; - } else { - normalized_staging_path = staging_target_path; - } + // ensure bind mount + if (staging_target_path) { + let normalized_staging_path; - // sanity check to ensure the staged path is actually mounted - result = await mount.pathIsMounted(normalized_staging_path); - if (!result) { + if (access_type == "block") { + normalized_staging_path = staging_target_path + "/block_device"; + } else { + normalized_staging_path = staging_target_path; + } + + normalized_staging_path = + filesystem.covertUnixSeparatorToWindowsSeparator( + normalized_staging_path + ); + + // source path + result = await filesystem.pathExists(normalized_staging_path); + if (!result) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `staging path is not mounted: ${normalized_staging_path}` + ); + } + + // target path + result = await filesystem.pathExists(target_path); + // already published + if (result) { + if (!(await filesystem.isSymbolicLink(target_path))) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `target path exists but is not a symlink as it should be: ${target_path}` + ); + } + return {}; + } + + // create symlink + fs.symlinkSync(normalized_staging_path, target_path); + return {}; + } + + // unsupported filesystem throw new GrpcError( grpc.status.FAILED_PRECONDITION, - `staging path is not mounted: ${normalized_staging_path}` + `only staged configurations are valid` ); - } - - result = await mount.pathIsMounted(target_path); - // if not mounted, mount - if (!result) { - await mount.bindMount(normalized_staging_path, target_path, [ - "-o", - bind_mount_flags.join(","), - ]); - } else { - // if is mounted, ensure proper source - if (access_type == "block") { - normalized_staging_device = "dev"; // special syntax for single file bind mounts - } else { - normalized_staging_device = await mount.getMountPointDevice( - staging_target_path - ); - } - result = await mount.deviceIsMountedAtPath( - normalized_staging_device, - target_path + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` ); - if (!result) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `it appears something else is already mounted at ${target_path}` - ); - } - } - - return {}; } + break; + case NODE_OS_DRIVER_CSI_PROXY: + switch (node_attach_driver) { + //case "nfs": + case "smb": + //case "lustre": + //case "oneclient": + //case "hostpath": + case "iscsi": + //case "zfs-local": + // ensure appropriate directories/files + switch (access_type) { + case "mount": + break; + case "block": + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unsupported/unknown access_type ${access_type}` + ); + } - // unsupported filesystem - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `only staged configurations are valid` - ); + // ensure bind mount + if (staging_target_path) { + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); + + let normalized_staging_path; + + if (access_type == "block") { + normalized_staging_path = staging_target_path + "/block_device"; + } else { + normalized_staging_path = staging_target_path; + } + + // source path + result = await csiProxyClient.FilesystemPathExists( + normalized_staging_path + ); + if (!result) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `staging path is not mounted: ${normalized_staging_path}` + ); + } + + // target path + result = await csiProxyClient.FilesystemPathExists(target_path); + // already published + if (result) { + result = await csiProxyClient.FilesystemIsSymlink(target_path); + if (!result) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `target path exists but is not a symlink as it should be: ${target_path}` + ); + } + return {}; + } + + // create symlink + await csiProxyClient.executeRPC("filesystem", "CreateSymlink", { + source_path: normalized_staging_path, + target_path, + }); + + return {}; + } + + // unsupported filesystem + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `only staged configurations are valid` + ); + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + break; default: throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `unknown/unsupported node_attach_driver: ${node_attach_driver}` + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` ); } - - return {}; } async NodeUnpublishVolume(call) { @@ -1303,64 +2809,152 @@ class CsiBaseDriver { const umount_args = []; const umount_force_extra_args = ["--force", "--lazy"]; - try { - result = await mount.pathIsMounted(target_path); - } catch (err) { - // running findmnt on non-existant paths return immediately - // the only time this should timeout is on a stale fs - // so if timeout is hit we should be near certain it is indeed mounted - if (err.timeout) { - driver.ctx.logger.warn( - `detected stale mount, attempting to force unmount: ${target_path}` - ); - await mount.umount( - target_path, - umount_args.concat(umount_force_extra_args) - ); - result = false; // assume we have fully unmounted - } else { - throw err; - } - } - - if (result) { - try { - result = await mount.umount(target_path, umount_args); - } catch (err) { - if (err.timeout) { - driver.ctx.logger.warn( - `hit timeout waiting to unmount path: ${target_path}` - ); - // bind mounts do show the 'real' fs details - result = await mount.getMountDetails(target_path); - switch (result.fstype) { - case "nfs": - case "nfs4": - driver.ctx.logger.warn( - `detected stale nfs filesystem, attempting to force unmount: ${target_path}` - ); - result = await mount.umount( - target_path, - umount_args.concat(umount_force_extra_args) - ); - break; - default: - throw err; - break; + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + try { + result = await mount.pathIsMounted(target_path); + } catch (err) { + // running findmnt on non-existant paths return immediately + // the only time this should timeout is on a stale fs + // so if timeout is hit we should be near certain it is indeed mounted + if (err.timeout) { + driver.ctx.logger.warn( + `detected stale mount, attempting to force unmount: ${target_path}` + ); + await mount.umount( + target_path, + umount_args.concat(umount_force_extra_args) + ); + result = false; // assume we have fully unmounted + } else { + throw err; } - } else { - throw err; } - } - } - result = await filesystem.pathExists(target_path); - if (result) { - if (fs.lstatSync(target_path).isDirectory()) { - result = await filesystem.rmdir(target_path); - } else { - result = await filesystem.rm([target_path]); - } + if (result) { + try { + result = await GeneralUtils.retry( + 10, + 0, + async () => { + return await mount.umount(target_path, umount_args); + }, + { + minExecutionTime: 1000, + retryCondition: (err) => { + if (_.get(err, "stderr", "").includes("busy")) { + return true; + } + }, + } + ); + } catch (err) { + if (err.timeout) { + driver.ctx.logger.warn( + `hit timeout waiting to unmount path: ${target_path}` + ); + // bind mounts do show the 'real' fs details + result = await mount.getMountDetails(target_path); + switch (result.fstype) { + case "nfs": + case "nfs4": + driver.ctx.logger.warn( + `detected stale nfs filesystem, attempting to force unmount: ${target_path}` + ); + result = await mount.umount( + target_path, + umount_args.concat(umount_force_extra_args) + ); + break; + default: + throw err; + } + } else { + throw err; + } + } + } + + result = await filesystem.pathExists(target_path); + if (result) { + if (fs.lstatSync(target_path).isDirectory()) { + result = await GeneralUtils.retry( + 30, + 0, + async () => { + return await filesystem.rmdir(target_path); + }, + { + minExecutionTime: 1000, + retryCondition: (err) => { + if (_.get(err, "stderr", "").includes("busy")) { + return true; + } + }, + } + ); + } else { + result = await GeneralUtils.retry( + 30, + 0, + async () => { + return await filesystem.rm([target_path]); + }, + { minExecutionTime: 1000 } + ); + } + } + + break; + case NODE_OS_DRIVER_WINDOWS: + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); + let win_target_path = + filesystem.covertUnixSeparatorToWindowsSeparator(target_path); + + result = await filesystem.pathExists(win_target_path); + if (!result) { + return {}; + } + + if (!(await filesystem.isSymbolicLink(win_target_path))) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `target path is not a symlink ${win_target_path}` + ); + } + + fs.rmdirSync(win_target_path); + break; + case NODE_OS_DRIVER_CSI_PROXY: + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); + + result = await csiProxyClient.FilesystemPathExists(target_path); + if (!result) { + return {}; + } + + result = await csiProxyClient.executeRPC("filesystem", "IsSymlink", { + path: target_path, + }); + + if (!result.is_symlink) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `target path is not a symlink ${target_path}` + ); + } + + await csiProxyClient.executeRPC("filesystem", "Rmdir", { + path: target_path, + }); + + break; + default: + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` + ); } return {}; @@ -1397,60 +2991,181 @@ class CsiBaseDriver { res.volume_condition = { abnormal, message }; } - if ( - (await mount.isBindMountedBlockDevice(volume_path)) || - (await mount.isBindMountedBlockDevice(block_path)) - ) { - device_path = block_path; - access_type = "block"; - } else { - device_path = volume_path; - access_type = "mount"; - } - - switch (access_type) { - case "mount": - if (!(await mount.pathIsMounted(device_path))) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `nothing mounted at path: ${device_path}` - ); + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + if ( + (await mount.isBindMountedBlockDevice(volume_path)) || + (await mount.isBindMountedBlockDevice(block_path)) + ) { + device_path = block_path; + access_type = "block"; + } else { + device_path = volume_path; + access_type = "mount"; + } + + switch (access_type) { + case "mount": + if (!(await mount.pathIsMounted(device_path))) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `nothing mounted at path: ${device_path}` + ); + } + result = await mount.getMountDetails(device_path, [ + "avail", + "size", + "used", + ]); + + res.usage = [ + { + available: result.avail, + total: result.size, + used: result.used, + unit: "BYTES", + }, + ]; + break; + case "block": + if (!(await filesystem.pathExists(device_path))) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `nothing mounted at path: ${device_path}` + ); + } + result = await filesystem.getBlockDevice(device_path); + + res.usage = [ + { + total: result.size, + unit: "BYTES", + }, + ]; + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unsupported/unknown access_type ${access_type}` + ); } - result = await mount.getMountDetails(device_path, [ - "avail", - "size", - "used", - ]); - res.usage = [ - { - available: result.avail, - total: result.size, - used: result.used, - unit: "BYTES", - }, - ]; break; - case "block": - if (!(await filesystem.pathExists(device_path))) { + case NODE_OS_DRIVER_WINDOWS: { + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); + let win_volume_path = + filesystem.covertUnixSeparatorToWindowsSeparator(volume_path); + // ensure path is mounted + result = await filesystem.pathExists(win_volume_path); + if (!result) { throw new GrpcError( grpc.status.NOT_FOUND, - `nothing mounted at path: ${device_path}` + `volume_path ${win_volume_path} is not currently mounted` ); } - result = await filesystem.getBlockDevice(device_path); - res.usage = [ - { - total: result.size, - unit: "BYTES", - }, - ]; + let node_attach_driver; + + let target = (await wutils.GetRealTarget(win_volume_path)) || ""; + if (target.startsWith("\\\\")) { + node_attach_driver = "smb"; + } + if (target.startsWith("\\\\?\\Volume")) { + if (await wutils.VolumeIsIscsi(target)) { + node_attach_driver = "iscsi"; + } + } + + if (!node_attach_driver) { + // nothing we care about + node_attach_driver = "bypass"; + } + + switch (node_attach_driver) { + case "smb": + res.usage = [{ total: 0, unit: "BYTES" }]; + break; + case "iscsi": + let node_volume = await wutils.GetVolumeByVolumeId(target); + res.usage = [ + { + available: node_volume.SizeRemaining, + total: node_volume.Size, + used: node_volume.Size - node_volume.SizeRemaining, + unit: "BYTES", + }, + ]; + break; + case "bypass": + res.usage = [{ total: 0, unit: "BYTES" }]; + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + break; + } + case NODE_OS_DRIVER_CSI_PROXY: + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); + const volume_context = await driver.getDerivedVolumeContext(call); + if (!volume_context) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `unable to retrieve volume_context for volume: ${volume_id}` + ); + } + + const node_attach_driver = volume_context.node_attach_driver; + + // ensure path is mounted + result = await csiProxyClient.FilesystemPathExists(volume_path); + if (!result) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `volume_path ${volume_path} is not currently mounted` + ); + } + + switch (node_attach_driver) { + case "smb": + res.usage = [{ total: 0, unit: "BYTES" }]; + break; + case "iscsi": + let node_volume_id = + await csiProxyClient.getVolumeIdFromIscsiTarget( + volume_context.portal, + volume_context.iqn + ); + result = await csiProxyClient.executeRPC( + "volume", + "GetVolumeStats", + { + volume_id: node_volume_id, + } + ); + res.usage = [ + { + available: result.total_bytes - result.used_bytes, + total: result.total_bytes, + used: result.used_bytes, + unit: "BYTES", + }, + ]; + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } break; default: throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `unsupported/unknown access_type ${access_type}` + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` ); } @@ -1477,6 +3192,7 @@ class CsiBaseDriver { let is_formatted; let fs_type; let is_device_mapper = false; + let result; const volume_id = call.request.volume_id; if (!volume_id) { @@ -1490,91 +3206,265 @@ class CsiBaseDriver { const capacity_range = call.request.capacity_range; const volume_capability = call.request.volume_capability; - if ( - (await mount.isBindMountedBlockDevice(volume_path)) || - (await mount.isBindMountedBlockDevice(block_path)) - ) { - access_type = "block"; - device_path = block_path; - } else { - access_type = "mount"; - device_path = volume_path; - } + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + if ( + (await mount.isBindMountedBlockDevice(volume_path)) || + (await mount.isBindMountedBlockDevice(block_path)) + ) { + access_type = "block"; + device_path = block_path; + } else { + access_type = "mount"; + device_path = volume_path; + } - try { - device = await mount.getMountPointDevice(device_path); - is_formatted = await filesystem.deviceIsFormatted(device); - is_block = await filesystem.isBlockDevice(device); - } catch (err) { - if (err.code == 1) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `volume_path ${volume_path} is not currently mounted` - ); - } - } - - if (is_block) { - let rescan_devices = []; - // detect if is a multipath device - is_device_mapper = await filesystem.isDeviceMapperDevice(device); - if (is_device_mapper) { - // NOTE: want to make sure we scan the dm device *after* all the underlying slaves - rescan_devices = await filesystem.getDeviceMapperDeviceSlaves(device); - } - - rescan_devices.push(device); - - for (let sdevice of rescan_devices) { - // TODO: technically rescan is only relevant/available for remote drives - // such as iscsi etc, should probably limit this call as appropriate - // for now crudely checking the scenario inside the method itself - await filesystem.rescanDevice(sdevice); - } - - // let things settle - // it appears the dm devices can take a second to figure things out - if (is_device_mapper || true) { - await sleep(2000); - } - - if (is_formatted && access_type == "mount") { - fs_info = await filesystem.getDeviceFilesystemInfo(device); - fs_type = fs_info.type; - if (fs_type) { - switch (fs_type) { - case "ext4": - case "ext3": - case "ext4dev": - //await filesystem.checkFilesystem(device, fs_info.type); - await filesystem.expandFilesystem(device, fs_type); - break; - case "btrfs": - case "xfs": - let mount_info = await mount.getMountDetails(device_path); - if (["btrfs", "xfs"].includes(mount_info.fstype)) { - //await filesystem.checkFilesystem(device, fs_info.type); - await filesystem.expandFilesystem(device_path, fs_type); - } - break; - default: - // unsupported filesystem - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `unsupported/unknown filesystem ${fs_type}` - ); + try { + device = await mount.getMountPointDevice(device_path); + is_formatted = await filesystem.deviceIsFormatted(device); + is_block = await filesystem.isBlockDevice(device); + } catch (err) { + if (err.code == 1) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `volume_path ${volume_path} is not currently mounted` + ); } } - } else { - //block device unformatted - return {}; + + if (is_block) { + let rescan_devices = []; + // detect if is a multipath device + is_device_mapper = await filesystem.isDeviceMapperDevice(device); + if (is_device_mapper) { + // NOTE: want to make sure we scan the dm device *after* all the underlying slaves + rescan_devices = await filesystem.getDeviceMapperDeviceSlaves( + device + ); + } + + rescan_devices.push(device); + + for (let sdevice of rescan_devices) { + // TODO: technically rescan is only relevant/available for remote drives + // such as iscsi etc, should probably limit this call as appropriate + // for now crudely checking the scenario inside the method itself + await filesystem.rescanDevice(sdevice); + } + + // let things settle + // it appears the dm devices can take a second to figure things out + if (is_device_mapper || true) { + await GeneralUtils.sleep(2000); + } + + if (is_formatted && access_type == "mount") { + fs_info = await filesystem.getDeviceFilesystemInfo(device); + fs_type = fs_info.type; + if (fs_type) { + switch (fs_type) { + case "ext3": + case "ext4": + case "ext4dev": + //await filesystem.checkFilesystem(device, fs_info.type); + await filesystem.expandFilesystem(device, fs_type); + break; + case "btrfs": + case "xfs": + let mount_info = await mount.getMountDetails(device_path); + if (["btrfs", "xfs"].includes(mount_info.fstype)) { + //await filesystem.checkFilesystem(device, fs_info.type); + await filesystem.expandFilesystem(device_path, fs_type); + } + break; + case "exfat": + case "ntfs": + case "vfat": + // TODO: return error here, cannot be expanded while online + //await filesystem.checkFilesystem(device, fs_info.type); + //await filesystem.expandFilesystem(device, fs_type); + break; + default: + // unsupported filesystem + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `unsupported/unknown filesystem ${fs_type}` + ); + } + } + } else { + //block device unformatted + return {}; + } + } else { + // not block device + return {}; + } + + break; + case NODE_OS_DRIVER_WINDOWS: { + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); + + let node_attach_driver; + let win_volume_path = + filesystem.covertUnixSeparatorToWindowsSeparator(volume_path); + + // ensure path is mounted + result = await filesystem.pathExists(win_volume_path); + if (!result) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `volume_path ${win_volume_path} is not currently mounted` + ); + } + + let target = (await wutils.GetRealTarget(win_volume_path)) || ""; + if (target.startsWith("\\\\")) { + node_attach_driver = "smb"; + } + if (target.startsWith("\\\\?\\Volume")) { + if (await wutils.VolumeIsIscsi(target)) { + node_attach_driver = "iscsi"; + } + } + + if (!node_attach_driver) { + // nothing we care about + node_attach_driver = "bypass"; + } + + switch (node_attach_driver) { + case "smb": + // noop + break; + case "iscsi": + // rescan devices + await wutils.UpdateHostStorageCache(); + await wutils.ResizeVolume(target); + break; + case "bypass": + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + + break; } - } else { - // not block device - return {}; + case NODE_OS_DRIVER_CSI_PROXY: + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); + const volume_context = await driver.getDerivedVolumeContext(call); + if (!volume_context) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `unable to retrieve volume_context for volume: ${volume_id}` + ); + } + + const node_attach_driver = volume_context.node_attach_driver; + + // ensure path is mounted + result = await csiProxyClient.FilesystemPathExists(volume_path); + if (!result) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `volume_path ${volume_path} is not currently mounted` + ); + } + + switch (node_attach_driver) { + case "iscsi": + const node_volume_id = + await csiProxyClient.getVolumeIdFromIscsiTarget( + volume_context.portal, + volume_context.iqn + ); + const disk_number = + await csiProxyClient.getDiskNumberFromIscsiTarget( + volume_context.portal, + volume_context.iqn + ); + + if (node_volume_id) { + const required_bytes = _.get( + call.request, + "capacity_range.required_bytes" + ); + if (required_bytes) { + await csiProxyClient.executeRPC("disk", "Rescan"); + try { + await csiProxyClient.executeRPC("volume", "ResizeVolume", { + volume_id: node_volume_id, + resize_bytes: 0, + }); + } catch (e) { + let details = _.get(e, "details", ""); + // seems to be a false positive + if ( + !details.includes( + "The size of the extent is less than the minimum of 1MB" + ) + ) { + throw e; + } + + await csiProxyClient.executeRPC("disk", "GetDiskStats", { + disk_number, + }); + + result = await csiProxyClient.executeRPC( + "volume", + "GetVolumeStats", + { + volume_id: node_volume_id, + } + ); + + let diff = Math.abs(result.total_bytes - required_bytes); + let percentage_diff = parseInt((diff / required_bytes) * 100); + /** + * 15MB is used by the 1ast partition on the initialized disk + * + * 100MB + * TODO: possibly change this to a percentage instead of absolute numbers + */ + let max_delta = 104857600; + driver.ctx.logger.debug( + "resize diff %s (%s%%)", + diff, + percentage_diff + ); + if (diff > max_delta) { + throw new GrpcError( + grpc.status.OUT_OF_RANGE, + `expanded size ${result.total_bytes} is too far off (${diff}) from requested size (${required_bytes})` + ); + } + } + } + } else { + throw new GrpcError(grpc.status.NOT_FOUND, `cannot find volume`); + } + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + break; + default: + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` + ); } return {}; } } + module.exports.CsiBaseDriver = CsiBaseDriver; diff --git a/src/utils/csi_proxy_client.js b/src/utils/csi_proxy_client.js new file mode 100644 index 0000000..d0587c0 --- /dev/null +++ b/src/utils/csi_proxy_client.js @@ -0,0 +1,269 @@ +const _ = require("lodash"); +const grpc = require("./grpc").grpc; +const path = require("path"); +const protoLoader = require("@grpc/proto-loader"); + +const PROTO_BASE_PATH = + path.dirname(path.dirname(__dirname)) + path.sep + "csi_proxy_proto"; + +/** + * leave connection null as by default the named pipe is derrived + */ +const DEFAULT_SERVICES = { + filesystem: { version: "v1", connection: null }, + disk: { version: "v1", connection: null }, + volume: { version: "v1", connection: null }, + smb: { version: "v1", connection: null }, + system: { version: "v1alpha1", connection: null }, + iscsi: { version: "v1alpha2", connection: null }, +}; + +function capitalize(s) { + return s && s[0].toUpperCase() + s.slice(1); +} + +class CsiProxyClient { + constructor(options = {}) { + this.clients = {}; + + // initialize all clients + const services = Object.assign( + {}, + DEFAULT_SERVICES, + options.services || {} + ); + + const pipePrefix = options.pipe_prefix || "csi-proxy"; + + for (const serviceName in services) { + const service = services[serviceName]; + const serviceVersion = + service.version || DEFAULT_SERVICES[serviceName].version; + const serviceConnection = + // HANGS + // Http2Session client (38) nghttp2 has 13 bytes to send directly + // Http2Session client (38) wants read? 1 + // Then pipe closes after 60 seconds-ish + service.connection || + `unix:////./pipe/${pipePrefix}-${serviceName}-${serviceVersion}`; + // EACCESS + //service.connection || + //`unix:///csi/${pipePrefix}-${serviceName}-${serviceVersion}`; + //service.connection || + //`unix:///csi/csi.sock.internal`; + + const PROTO_PATH = `${PROTO_BASE_PATH}\\${serviceName}\\${serviceVersion}\\api.proto`; + + const packageDefinition = protoLoader.loadSync(PROTO_PATH, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs: [__dirname + "/../csi_proxy_proto"], + }); + const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); + const serviceInstance = new protoDescriptor[serviceVersion][ + capitalize(serviceName) + ](serviceConnection, grpc.credentials.createInsecure()); + this.clients[serviceName] = serviceInstance; + } + } + + async executeRPC(serviceName, methodName, options = {}) { + function rescursivePathFixer(obj) { + for (const k in obj) { + if (typeof obj[k] == "object" && obj[k] !== null) { + rescursivePathFixer(obj[k]); + } else { + if (k.includes("path")) { + obj[k] = obj[k].replaceAll("/", "\\"); + } + } + } + } + + rescursivePathFixer(options); + + const cleansedOptions = JSON.parse(JSON.stringify(options)); + // This function handles arrays and objects + function recursiveCleanse(obj) { + for (const k in obj) { + if (typeof obj[k] == "object" && obj[k] !== null) { + recursiveCleanse(obj[k]); + } else { + if ( + k.includes("secret") || + k.includes("username") || + k.includes("password") + ) { + obj[k] = "redacted"; + } + } + } + } + recursiveCleanse(cleansedOptions); + + console.log( + "csi-proxy request %s/%s - data: %j", + capitalize(serviceName), + methodName, + cleansedOptions + ); + + return new Promise((resolve, reject) => { + const functionRef = this.clients[serviceName.toLowerCase()][methodName]; + if (!functionRef) { + reject( + new Error( + `missing method ${methodName} on service ${capitalize(serviceName)}` + ) + ); + return; + } + + this.clients[serviceName.toLowerCase()][methodName]( + options, + (error, data) => { + console.log( + "csi-proxy response %s/%s - error: %j, data: %j", + capitalize(serviceName), + methodName, + error, + data + ); + + if (error) { + reject(error); + } + + resolve(data); + } + ); + }); + } + + /** + * Returns a disk_number if the target has 0 or 1 disks + * + * @param {*} target_portal + * @param {*} iqn + * @returns + */ + async getDiskNumberFromIscsiTarget(target_portal, iqn) { + let result; + + if (typeof target_portal != "object") { + target_portal = { + target_address: target_portal.split(":")[0], + target_port: target_portal.split(":")[1] || 3260, + }; + } + + // get device + try { + result = await this.executeRPC("iscsi", "GetTargetDisks", { + target_portal, + iqn, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("ObjectNotFound")) { + throw e; + } + } + + let diskIds = _.get(result, "diskIDs", []); + if (diskIds.length > 1) { + throw new Error( + `${diskIds.length} disks on the target, no way to know which is the relevant disk` + ); + } + + return diskIds[0]; + } + + /** + * Returns a volume_id if the disk has 0 or 1 volumes + * + * @param {*} disk_number + * @returns + */ + async getVolumeIdFromDiskNumber(disk_number) { + let result; + + if (disk_number == 0 || disk_number > 0) { + result = await this.executeRPC("volume", "ListVolumesOnDisk", { + disk_number, + }); + + let volume_ids = _.get(result, "volume_ids", []); + /** + * the 1st partition is a sort of system partion and is "" + * usually around 15MB in size + */ + volume_ids = volume_ids.filter((item) => { + return Boolean(item); + }); + + if (volume_ids.length > 1) { + throw new Error( + `${volume_ids.length} volumes on the disk, no way to know which is the relevant volume` + ); + } + + // ok of null/undefined + return volume_ids[0]; + } + } + + /** + * Return a volume_id if the target and disk both have 0 or 1 entries + * + * @param {*} target_portal + * @param {*} iqn + * @returns + */ + async getVolumeIdFromIscsiTarget(target_portal, iqn) { + const disk_number = await this.getDiskNumberFromIscsiTarget(...arguments); + return await this.getVolumeIdFromDiskNumber(disk_number); + } + + async FilesystemPathExists(path) { + let result; + try { + result = await this.executeRPC("filesystem", "PathExists", { + path, + }); + + return result.exists; + } catch (e) { + let details = _.get(e, "details", ""); + if (details.includes("not an absolute Windows path")) { + return false; + } else { + throw e; + } + } + } + + async FilesystemIsSymlink(path) { + let result; + try { + result = await this.executeRPC("filesystem", "IsSymlink", { + path, + }); + + return result.is_symlink; + } catch (e) { + let details = _.get(e, "details", ""); + if (details.includes("not an absolute Windows path")) { + return false; + } else { + throw e; + } + } + } +} + +module.exports.CsiProxyClient = CsiProxyClient; diff --git a/src/utils/filesystem.js b/src/utils/filesystem.js index f2c897c..a5cfd63 100644 --- a/src/utils/filesystem.js +++ b/src/utils/filesystem.js @@ -1,5 +1,7 @@ const cp = require("child_process"); const fs = require("fs"); +const GeneralUtils = require("./general"); +const path = require("path"); const DEFAULT_TIMEOUT = process.env.FILESYSTEM_DEFAULT_TIMEOUT || 30000; @@ -25,6 +27,10 @@ class Filesystem { } } + covertUnixSeparatorToWindowsSeparator(p) { + return p.replaceAll(path.posix.sep, path.win32.sep); + } + /** * Attempt to discover if device is a block device * @@ -223,8 +229,12 @@ class Filesystem { } } + async isSymbolicLink(path) { + return fs.lstatSync(path).isSymbolicLink(); + } + /** - * create symlink + * remove file * * @param {*} device */ @@ -298,7 +308,7 @@ class Filesystem { async getBlockDevice(device) { const filesystem = this; device = await filesystem.realpath(device); - let args = ["-a", "-b", "-l", "-J", "-O"]; + let args = ["-a", "-b", "-J", "-O"]; args.push(device); let result; @@ -312,30 +322,214 @@ class Filesystem { } /** - * blkid -p -o export + * + * @param {*} device + * @returns + */ + async getBlockDeviceLargestPartition(device) { + const filesystem = this; + let block_device_info = await filesystem.getBlockDevice(device); + if (block_device_info.children) { + let child; + for (const child_i of block_device_info.children) { + if (child_i.type == "part") { + if (!child) { + child = child_i; + } else { + if (child_i.size > child.size) { + child = child_i; + } + } + } + } + return `${child.path}`; + } + } + + /** + * + * @param {*} device + * @returns + */ + async getBlockDeviceLastPartition(device) { + const filesystem = this; + let block_device_info = await filesystem.getBlockDevice(device); + if (block_device_info.children) { + let child; + for (const child_i of block_device_info.children) { + if (child_i.type == "part") { + if (!child) { + child = child_i; + } else { + let minor = child["maj:min"].split(":")[1]; + let minor_i = child_i["maj:min"].split(":")[1]; + if (minor_i > minor) { + child = child_i; + } + } + } + } + return `${child.path}`; + } + } + + /** + * + * @param {*} device + * @returns + */ + async getBlockDevicePartitionCount(device) { + const filesystem = this; + let count = 0; + let block_device_info = await filesystem.getBlockDevice(device); + if (block_device_info.children) { + for (const child_i of block_device_info.children) { + if (child_i.type == "part") { + count++; + } + } + } + return count; + } + + async getBlockDeviceHasParitionTable(device) { + const filesystem = this; + let block_device_info = await filesystem.getBlockDevice(device); + + return block_device_info.pttype ? true : false; + } + + /** + * DOS + * - type=83 = Linux + * - type=07 = HPFS/NTFS/exFAT + * + * GPT + * - type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 = linux + * - type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 = ntfs + * - type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B = EFI + * + * @param {*} device + * @param {*} label + * @param {*} type + */ + async partitionDevice( + device, + label = "gpt", + type = "0FC63DAF-8483-4772-8E79-3D69D8477DE4" + ) { + const filesystem = this; + let args = [device]; + let result; + + try { + result = await filesystem.exec("sfdisk", args, { + stdin: `label: ${label}\n`, + }); + result = await filesystem.exec("sfdisk", args, { + stdin: `type=${type}\n`, + }); + } catch (err) { + throw err; + } + } + + /** + * mimic the behavior of partitioning a new data drive in windows directly + * + * https://en.wikipedia.org/wiki/Microsoft_Reserved_Partition + * + * @param {*} device + */ + async partitionDeviceWindows(device) { + const filesystem = this; + let args = [device]; + let result; + let block_device_info = await filesystem.getBlockDevice(device); + + //let sixteen_megabytes = 16777216; + //let thirtytwo_megabytes = 33554432; + //let onehundredtwentyeight_megabytes = 134217728; + + let msr_partition_size = "16M"; + let label = "gpt"; + let msr_guid = "E3C9E316-0B5C-4DB8-817D-F92DF00215AE"; + let ntfs_guid = "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7"; + + if (block_device_info.type != "disk") { + throw new Error( + `cannot partition device of type: ${block_device_info.type}` + ); + } + + /** + * On drives less than 16GB in size, the MSR is 32MB. + * On drives greater than or equal two 16GB, the MSR is 128 MB. + * It is only 128 MB for Win 7/8 ( On drives less than 16GB in size, the MSR is 32MB ) & 16 MB for win 10! + */ + let msr_partition_size_break = 17179869184; // 16GB + + // TODO: this size may be sectors so not really disk size in terms of GB + if (block_device_info.size >= msr_partition_size_break) { + // ignoring for now, appears windows 10+ use 16MB always + //msr_partition_size = "128M"; + } + + try { + result = await filesystem.exec("sfdisk", args, { + stdin: `label: ${label}\n`, + }); + // must send ALL partitions at once (newline separated), cannot send them 1 at a time + result = await filesystem.exec("sfdisk", args, { + stdin: `size=${msr_partition_size},type=${msr_guid}\ntype=${ntfs_guid}\n`, + }); + } catch (err) { + throw err; + } + } + + /** * * @param {*} device */ async deviceIsFormatted(device) { const filesystem = this; - let args = ["-p", "-o", "export", device]; let result; try { - result = await filesystem.exec("blkid", args); + result = await filesystem.getBlockDevice(device); + return result.fstype ? true : false; } catch (err) { - if (err.code == 2 && err.stderr.includes("No such device or address")) { - throw err; - } - - if (err.code == 2) { - return false; - } - throw err; } + } - return true; + async deviceIsIscsi(device) { + const filesystem = this; + let result; + + do { + if (result) { + device = `/dev/${result.pkname}`; + } + result = await filesystem.getBlockDevice(device); + } while (result.pkname); + + return result && result.tran == "iscsi"; + } + + async getBlockDeviceParent(device) { + const filesystem = this; + let result; + + do { + if (result) { + device = `/dev/${result.pkname}`; + } + result = await filesystem.getBlockDevice(device); + } while (result.pkname); + + return result; } /** @@ -438,6 +632,31 @@ class Filesystem { } } + async expandPartition(device) { + const filesystem = this; + const command = "growpart"; + const args = []; + + let block_device_info = await filesystem.getBlockDevice(device); + let device_fs_info = await filesystem.getDeviceFilesystemInfo(device); + let growpart_partition = device_fs_info["part_entry_number"]; + let parent_block_device = await filesystem.getBlockDeviceParent(device); + + args.push(parent_block_device.path, growpart_partition); + + try { + await filesystem.exec(command, args); + } catch (err) { + if ( + err.code == 1 && + err.stdout && + err.stdout.includes("could only be grown by") + ) { + return; + } + } + } + /** * expand a given filesystem * @@ -458,6 +677,9 @@ class Filesystem { args = args.concat(["filesystem", "resize", "max"]); args.push(device); // in this case should be a mounted path break; + case "exfat": + // https://github.com/exfatprogs/exfatprogs/issues/134 + return; case "ext4": case "ext3": case "ext4dev": @@ -465,6 +687,16 @@ class Filesystem { args = args.concat(options); args.push(device); break; + case "ntfs": + // must be unmounted + command = "ntfsresize"; + await filesystem.exec(command, ["-c", device]); + await filesystem.exec(command, ["-n", device]); + args = args.concat("-P", "-f"); + args = args.concat(options); + //args = args.concat(["-s", "max"]); + args.push(device); + break; case "xfs": command = "xfs_growfs"; args = args.concat(options); @@ -481,6 +713,10 @@ class Filesystem { try { result = await filesystem.exec(command, args); + // must clear the dirty bit after resize + if (fstype.toLowerCase() == "ntfs") { + await filesystem.exec("ntfsfix", ["-d", device]); + } return result; } catch (err) { throw err; @@ -521,6 +757,15 @@ class Filesystem { args.push("-f"); args.push("-p"); break; + case "ntfs": + /** + * -b, --clear-bad-sectors Clear the bad sector list + * -d, --clear-dirty Clear the volume dirty flag + */ + command = "ntfsfix"; + args.puuh("-d"); + args.push(device); + break; case "xfs": command = "xfs_repair"; args = args.concat(["-o", "force_geometry"]); @@ -589,16 +834,31 @@ class Filesystem { * @param {*} path */ async pathExists(path) { - const filesystem = this; - let args = []; - args.push(path); - + let result = false; try { - await filesystem.exec("stat", args); + await GeneralUtils.retry( + 10, + 200, + () => { + fs.statSync(path); + }, + { + retryCondition: (err) => { + if (err.code == "UNKNOWN") { + return true; + } + return false; + }, + } + ); + result = true; } catch (err) { - return false; + if (err.code !== "ENOENT") { + throw err; + } } - return true; + + return result; } exec(command, args, options = {}) { @@ -607,6 +867,12 @@ class Filesystem { //options.timeout = DEFAULT_TIMEOUT; } + let stdin; + if (options.stdin) { + stdin = options.stdin; + delete options.stdin; + } + const filesystem = this; args = args || []; @@ -614,13 +880,27 @@ class Filesystem { args.unshift(command); command = filesystem.options.paths.sudo; } - console.log("executing filesystem command: %s %s", command, args.join(" ")); - + let command_log = `${command} ${args.join(" ")}`.trim(); + if (stdin) { + command_log = `echo '${stdin}' | ${command_log}` + .trim() + .replace(/\n/, "\\n"); + } + console.log("executing filesystem command: %s", command_log); + return new Promise((resolve, reject) => { const child = filesystem.options.executor.spawn(command, args, options); let stdout = ""; let stderr = ""; - + + child.on("spawn", function () { + if (stdin) { + child.stdin.setEncoding("utf-8"); + child.stdin.write(stdin); + child.stdin.end(); + } + }); + child.stdout.on("data", function (data) { stdout = stdout + data; }); diff --git a/src/utils/general.js b/src/utils/general.js index accc106..79654ac 100644 --- a/src/utils/general.js +++ b/src/utils/general.js @@ -1,4 +1,6 @@ +const _ = require("lodash"); const axios = require("axios"); +const crypto = require("crypto"); function sleep(ms) { return new Promise((resolve) => { @@ -6,6 +8,64 @@ function sleep(ms) { }); } +function md5(val) { + return crypto.createHash("md5").update(val).digest("hex"); +} + +function crc32(val) { + for (var a, o = [], c = 0; c < 256; c++) { + a = c; + for (var f = 0; f < 8; f++) a = 1 & a ? 3988292384 ^ (a >>> 1) : a >>> 1; + o[c] = a; + } + for (var n = -1, t = 0; t < val.length; t++) + n = (n >>> 8) ^ o[255 & (n ^ val.charCodeAt(t))]; + return (-1 ^ n) >>> 0; +} + +const crctab16 = new Uint16Array([ + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, + 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, + 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, + 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, + 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, + 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, + 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, + 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, + 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, + 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, 0x4014, + 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, + 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, + 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, + 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, + 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, + 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, + 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 0x2942, 0x38cb, 0x0a50, + 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, + 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, + 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, + 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, + 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, + 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, + 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, + 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, + 0x3de3, 0x2c6a, 0x1ef1, 0x0f78, +]); + +// calculate the 16-bit CRC of data with predetermined length. +function crc16(data) { + var res = 0x0ffff; + + for (let b of data) { + res = ((res >> 8) & 0x0ff) ^ crctab16[(res ^ b) & 0xff]; + } + + return ~res & 0x0ffff; +} + function lockKeysFromRequest(call, serviceMethodName) { switch (serviceMethodName) { // controller @@ -53,11 +113,19 @@ function getLargestNumber() { return number; } +function stripWindowsDriveLetter(path) { + return path.replace(/^[a-zA-Z]:/, ""); +} + +function hasWindowsDriveLetter(path) { + return /^[a-zA-Z]:/i.test(path); +} + /** * transition function to replicate `request` style requests using axios - * - * @param {*} options - * @param {*} callback + * + * @param {*} options + * @param {*} callback */ function axios_request(options, callback = function () {}) { function prep_response(res) { @@ -80,7 +148,14 @@ function axios_request(options, callback = function () {}) { // The request was made and the server responded with a status code // that falls out of the range of 2xx let res = prep_response(err.response); - callback(null, res, res.body); + let senderr = false; + if ( + options.validateStatus && + typeof options.validateStatus == "function" + ) { + senderr = true; + } + callback(senderr ? err : null, res, res.body); } else if (err.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of @@ -110,8 +185,82 @@ function stringify(value) { return JSON.stringify(value, getCircularReplacer()); } +function default_supported_block_filesystems() { + return ["btrfs", "exfat", "ext3", "ext4", "ext4dev", "ntfs", "vfat", "xfs"]; +} + +function default_supported_file_filesystems() { + return ["nfs", "cifs"]; +} + +async function retry(retries, retriesDelay, code, options = {}) { + let current_try = 0; + let maxwait = _.get(options, "maxwait"); + let logerrors = _.get(options, "logerrors", false); + let retryCondition = options.retryCondition; + let executeStartTime; + + do { + current_try++; + try { + executeStartTime = Date.now(); + return await code(); + } catch (err) { + if (current_try >= retries) { + throw err; + } + if (retryCondition) { + let retry = retryCondition(err); + if (!retry) { + console.log(`retry - failed condition, not trying again`); + throw err; + } + } + if (logerrors === true) { + console.log(`retry - err:`, err); + } + } + + // handle minExecutionTime + if (options.minExecutionTime > 0) { + let executionElapsedTIme = Date.now() - executeStartTime; + let minExecutionDelayTime = + options.minExecutionTime - executionElapsedTIme; + if (minExecutionDelayTime > 0) { + await sleep(minExecutionDelayTime); + } + } + + // handle delay + let sleep_time = retriesDelay; + if (_.get(options, "exponential", false) === true) { + sleep_time = retriesDelay * current_try; + } + + if (maxwait) { + if (sleep_time > maxwait) { + sleep_time = maxwait; + } + } + if (sleep_time > 0) { + console.log(`retry - waiting ${sleep_time}ms before trying again`); + await sleep(sleep_time); + } + } while (true); +} + module.exports.sleep = sleep; +module.exports.md5 = md5; +module.exports.crc32 = crc32; +module.exports.crc16 = crc16; module.exports.lockKeysFromRequest = lockKeysFromRequest; module.exports.getLargestNumber = getLargestNumber; module.exports.stringify = stringify; +module.exports.stripWindowsDriveLetter = stripWindowsDriveLetter; +module.exports.hasWindowsDriveLetter = hasWindowsDriveLetter; module.exports.axios_request = axios_request; +module.exports.default_supported_block_filesystems = + default_supported_block_filesystems; +module.exports.default_supported_file_filesystems = + default_supported_file_filesystems; +module.exports.retry = retry; diff --git a/src/utils/mount.js b/src/utils/mount.js index eade177..0448557 100644 --- a/src/utils/mount.js +++ b/src/utils/mount.js @@ -298,7 +298,7 @@ class Mount { return false; } const mount_info = await mount.getMountDetails(path); - const is_block = filesystem.isBlockDevice(path); + const is_block = await filesystem.isBlockDevice(path); if (mount_info.fstype == "devtmpfs" && is_block) { return true; } diff --git a/src/utils/polyfills.js b/src/utils/polyfills.js new file mode 100644 index 0000000..0e22848 --- /dev/null +++ b/src/utils/polyfills.js @@ -0,0 +1,5 @@ +if (typeof String.prototype.replaceAll == "undefined") { + String.prototype.replaceAll = function (match, replace) { + return this.replace(new RegExp(match, "g"), () => replace); + }; +} diff --git a/src/utils/powershell.js b/src/utils/powershell.js new file mode 100644 index 0000000..08d2a56 --- /dev/null +++ b/src/utils/powershell.js @@ -0,0 +1,85 @@ +const cp = require("child_process"); + +class Powershell { + async exec(command, options = {}) { + if (!options.hasOwnProperty("timeout")) { + // TODO: cannot use this as fsck etc are too risky to kill + //options.timeout = DEFAULT_TIMEOUT; + } + + //cmd := exec.Command("powershell", "-Mta", "-NoProfile", "-Command", command) + + let stdin; + if (options.stdin) { + stdin = options.stdin; + delete options.stdin; + } + + // https://github.com/kubernetes-csi/csi-proxy/blob/master/pkg/utils/utils.go + const _command = "powershell"; + const args = [ + "-Mta", + "-NoProfile", + "-Command", + command + ]; + + let command_log = `${_command} ${args.join(" ")}`.trim(); + if (stdin) { + command_log = `echo '${stdin}' | ${command_log}` + .trim() + .replace(/\n/, "\\n"); + } + console.log("executing powershell command: %s", command_log); + + return new Promise((resolve, reject) => { + const child = cp.spawn(_command, args, options); + let stdout = ""; + let stderr = ""; + + child.on("spawn", function () { + if (stdin) { + child.stdin.setEncoding("utf-8"); + child.stdin.write(stdin); + child.stdin.end(); + } + }); + + child.stdout.on("data", function (data) { + stdout = stdout + data; + }); + + child.stderr.on("data", function (data) { + stderr = stderr + data; + }); + + child.on("close", function (code) { + const result = { code, stdout, stderr, timeout: false }; + + // timeout scenario + if (code === null) { + result.timeout = true; + reject(result); + } + + if (code) { + console.log( + "failed to execute powershell command: %s, response: %j", + command_log, + result + ); + reject(result); + } else { + try { + result.parsed = JSON.parse(result.stdout); + } catch (err) { }; + resolve(result); + } + }); + }); + } +} + + + +module.exports.Powershell = Powershell; \ No newline at end of file diff --git a/src/utils/ssh.js b/src/utils/ssh.js index d008090..e39041f 100644 --- a/src/utils/ssh.js +++ b/src/utils/ssh.js @@ -1,4 +1,6 @@ -var Client = require("ssh2").Client; +const Client = require("ssh2").Client; +const { E_CANCELED, Mutex } = require("async-mutex"); +const GeneralUtils = require("./general"); class SshClient { constructor(options = {}) { @@ -8,7 +10,47 @@ class SshClient { this.logger = this.options.logger; } else { this.logger = console; + console.silly = console.debug; } + + if (!this.options.connection.hasOwnProperty("keepaliveInterval")) { + this.options.connection.keepaliveInterval = 10000; + } + + if (this.options.connection.debug == true) { + this.options.connection.debug = function (msg) { + this.debug(msg); + }; + } + + this.conn_mutex = new Mutex(); + this.conn_state; + this.conn_err; + this.ready_event_count = 0; + this.error_event_count = 0; + + this.conn = new Client(); + // invoked before close + this.conn.on("end", () => { + this.conn_state = "ended"; + this.debug("Client :: end"); + }); + // invoked after end + this.conn.on("close", () => { + this.conn_state = "closed"; + this.debug("Client :: close"); + }); + this.conn.on("error", (err) => { + this.conn_state = "error"; + this.conn_err = err; + this.error_event_count++; + this.debug("Client :: error"); + }); + this.conn.on("ready", () => { + this.conn_state = "ready"; + this.ready_event_count++; + this.debug("Client :: ready"); + }); } /** @@ -27,17 +69,119 @@ class SshClient { this.logger.silly(...arguments); } + async _connect() { + const start_ready_event_count = this.ready_event_count; + const start_error_event_count = this.error_event_count; + try { + await this.conn_mutex.runExclusive(async () => { + this.conn.connect(this.options.connection); + do { + if (start_error_event_count != this.error_event_count) { + throw this.conn_err; + } + + if (start_ready_event_count != this.ready_event_count) { + break; + } + + await GeneralUtils.sleep(100); + } while (true); + }); + } catch (err) { + if (err === E_CANCELED) { + return; + } + throw err; + } + } + + async connect() { + if (this.conn_state == "ready") { + return; + } + + return this._connect(); + } + async exec(command, options = {}, stream_proxy = null) { + // default is to reuse + if (process.env.SSH_REUSE_CONNECTION == "0") { + return this._nexec(...arguments); + } else { + return this._rexec(...arguments); + } + } + + async _rexec(command, options = {}, stream_proxy = null) { + const client = this; + const conn = this.conn; + + return new Promise(async (resolve, reject) => { + do { + try { + await this.connect(); + conn.exec(command, options, function (err, stream) { + if (err) { + reject(err); + return; + } + let stderr; + let stdout; + + if (stream_proxy) { + stream_proxy.on("kill", (signal) => { + stream.destroy(); + }); + } + + stream + .on("close", function (code, signal) { + client.debug( + "Stream :: close :: code: " + code + ", signal: " + signal + ); + if (stream_proxy) { + stream_proxy.emit("close", ...arguments); + } + resolve({ stderr, stdout, code, signal }); + //conn.end(); + }) + .on("data", function (data) { + client.debug("STDOUT: " + data); + if (stream_proxy) { + stream_proxy.stdout.emit("data", ...arguments); + } + if (stdout == undefined) { + stdout = ""; + } + stdout = stdout.concat(data); + }) + .stderr.on("data", function (data) { + client.debug("STDERR: " + data); + if (stream_proxy) { + stream_proxy.stderr.emit("data", ...arguments); + } + if (stderr == undefined) { + stderr = ""; + } + stderr = stderr.concat(data); + }); + }); + break; + } catch (err) { + if (err.message && !err.message.includes("Not connected")) { + throw err; + } + } + await GeneralUtils.sleep(1000); + } while (true); + }); + } + + async _nexec(command, options = {}, stream_proxy = null) { const client = this; return new Promise((resolve, reject) => { var conn = new Client(); - if (client.options.connection.debug == true) { - client.options.connection.debug = function (msg) { - client.debug(msg); - }; - } - conn .on("error", function (err) { client.debug("Client :: error"); @@ -50,7 +194,10 @@ class SshClient { // TERM: "", //}; conn.exec(command, options, function (err, stream) { - if (err) reject(err); + if (err) { + reject(err); + return; + } let stderr; let stdout; stream diff --git a/src/utils/windows.js b/src/utils/windows.js new file mode 100644 index 0000000..c02ceb3 --- /dev/null +++ b/src/utils/windows.js @@ -0,0 +1,783 @@ +const _ = require("lodash"); +const GeneralUtils = require("./general"); +const Powershell = require("./powershell").Powershell; + +/** + * https://kubernetes.io/blog/2021/08/16/windows-hostprocess-containers/ + * https://github.com/kubernetes-csi/csi-proxy/tree/master/pkg/os + * + * multipath notes: + * - http://scst.sourceforge.net/mc_s.html + * - https://github.com/kubernetes-csi/csi-proxy/pull/99 + * - https://docs.microsoft.com/en-us/azure/storsimple/storsimple-8000-configure-mpio-windows-server + * - https://support.purestorage.com/Legacy_Documentation/Setting_the_MPIO_Policy + * - https://docs.microsoft.com/en-us/powershell/module/mpio/?view=windowsserver2022-ps + * + * Get-WindowsFeature -Name 'Multipath-IO' + * Add-WindowsFeature -Name 'Multipath-IO' + * + * Enable-MSDSMAutomaticClaim -BusType "iSCSI" + * Disable-MSDSMAutomaticClaim -BusType "iSCSI" + * + * Get-MSDSMGlobalDefaultLoadBalancePolicy + * Set-MSDSMGlobalLoadBalancePolicy -Policy RR + * + * synology woes: + * - https://community.spiceworks.com/topic/2279882-synology-iscsi-will-not-disconnect-using-powershell-commands + * - https://support.hpe.com/hpesc/public/docDisplay?docId=c01880810&docLocale=en_US + * - https://askubuntu.com/questions/1159103/why-is-iscsi-trying-to-connect-on-ipv6-at-boot + */ +class Windows { + constructor() { + this.ps = new Powershell(); + } + + resultToArray(result) { + if (!result.parsed) { + result.parsed = []; + } + if (!Array.isArray(result.parsed)) { + result.parsed = [result.parsed]; + } + } + + uncPathToShare(path) { + // UNC\\[\\] + if (path.startsWith("UNC")) { + path = path.replace("UNC", "\\"); + } + + if (!path.startsWith("\\\\")) { + path = `\\\\${path}`; + } + + let parts = path.split("\\"); + return `\\\\${parts[2]}\\${parts[3]}`; + } + + async GetRealTarget(path) { + let item; + let target; + + do { + item = await this.GetItem(path); + path = null; + + target = _.get(item, "Target.[0]", ""); + if (target.startsWith("UNC")) { + let parts = target.split("\\", 3); + return `\\\\${parts[1]}\\${parts[2]}`; + } else if (target.startsWith("Volume")) { + return `\\\\?\\${target}`; + } else { + path = target; + } + } while (path); + } + + async GetItem(localPath) { + let command; + let result; + command = 'Get-Item "$Env:localpath" | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: { + localpath: localPath, + }, + }); + return result.parsed; + } catch (err) {} + } + + async GetSmbGlobalMapping(remotePath) { + let command; + // cannot have trailing slash nor a path + // must be \\\ + remotePath = this.uncPathToShare(remotePath); + command = + "Get-SmbGlobalMapping -RemotePath $Env:smbremotepath | ConvertTo-Json"; + try { + return await this.ps.exec(command, { + env: { + smbremotepath: remotePath, + }, + }); + } catch (err) {} + } + + /** + * Global in this context is allowed access by all users + * + * @param {*} remotePath + * @param {*} username + * @param {*} password + */ + async NewSmbGlobalMapping(remotePath, username, password) { + let result; + let command; + // -UseWriteThrough $true + // cannot have trailing slash nor a path + // must be \\\ + remotePath = this.uncPathToShare(remotePath); + command = + "$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential -RequirePrivacy $true"; + + result = await this.GetSmbGlobalMapping(remotePath); + if (!result) { + await this.ps.exec(command, { + env: { + smbuser: username, + smbpassword: password, + smbremotepath: remotePath, + }, + }); + } + } + + async RemoveSmbGlobalMapping(remotePath) { + let result; + let command; + // cannot have trailing slash nor a path + // must be \\\ + remotePath = this.uncPathToShare(remotePath); + command = "Remove-SmbGlobalMapping -RemotePath $Env:smbremotepath -Force"; + + do { + result = await this.GetSmbGlobalMapping(remotePath); + if (result) { + await this.ps.exec(command, { + env: { + smbremotepath: remotePath, + }, + }); + } + } while (result); + } + + async NewSmbLink(remotePath, localPath) { + let command; + // trailing slash required + // may include subdirectories on the share if desired + if (!remotePath.endsWith("\\")) { + remotePath = `${remotePath}\\`; + } + + command = + "New-Item -ItemType SymbolicLink $Env:smblocalPath -Target $Env:smbremotepath"; + await this.ps.exec(command, { + env: { + smblocalpath: localPath, + smbremotepath: remotePath, + }, + }); + } + + async NewIscsiTargetPortal(address, port) { + let command; + command = + "New-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port}"; + await this.ps.exec(command, { + env: { + iscsi_tp_address: address, + iscsi_tp_port: port, + }, + }); + } + + async RemoveIscsiTargetPortalByTargetPortalAddress(targetPortalAddress) { + let command; + command = `Remove-IscsiTargetPortal -TargetPortalAddress ${targetPortalAddress} -Confirm:$false`; + await this.ps.exec(command); + } + + async RemoveIscsiTargetPortalByTargetPortalAddressTargetPortalPort( + targetPortalAddress, + targetPortalPort + ) { + let command; + command = `Get-IscsiTargetPortal -TargetPortalAddress ${targetPortalAddress} -TargetPortalPortNumber ${targetPortalPort} | Remove-IscsiTargetPortal -Confirm:$false`; + await this.ps.exec(command); + } + + async IscsiTargetIsConnectedByPortalAddressPortalPort(address, port, iqn) { + let sessions = await this.GetIscsiSessionsByTargetNodeAddress(iqn); + for (let session of sessions) { + let connections = await this.GetIscsiConnectionsByIscsiSessionIdentifier( + session.SessionIdentifier + ); + for (let connection of connections) { + if ( + connection.TargetAddress == address && + connection.TargetPortNumber == port + ) { + return true; + } + } + } + + //process.exit(1); + + return false; + } + + /** + * -IsMultipathEnabled + * + * @param {*} address + * @param {*} port + * @param {*} iqn + * @param {*} authType + * @param {*} chapUser + * @param {*} chapSecret + */ + async ConnectIscsiTarget( + address, + port, + iqn, + authType, + chapUser, + chapSecret, + multipath = false + ) { + let is_connected = + await this.IscsiTargetIsConnectedByPortalAddressPortalPort( + address, + port, + iqn + ); + if (is_connected) { + return; + } + + let command; + // -IsMultipathEnabled $([System.Convert]::ToBoolean(${Env:iscsi_is_multipath})) + // -InitiatorPortalAddress + command = + "Connect-IscsiTarget -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port} -NodeAddress ${Env:iscsi_target_iqn} -AuthenticationType ${Env:iscsi_auth_type}"; + + if (chapUser) { + command += " -ChapUsername ${Env:iscsi_chap_user}"; + } + + if (chapSecret) { + command += " -ChapSecret ${Env:iscsi_chap_secret}"; + } + + if (multipath) { + command += + " -IsMultipathEnabled $([System.Convert]::ToBoolean(${Env:iscsi_is_multipath}))"; + } + + try { + await this.ps.exec(command, { + env: { + iscsi_tp_address: address, + iscsi_tp_port: port, + iscsi_target_iqn: iqn, + iscsi_auth_type: authType, + iscsi_chap_user: chapUser, + iscsi_chap_secret: chapSecret, + iscsi_is_multipath: String(multipath), + }, + }); + } catch (err) { + let details = _.get(err, "stderr", ""); + if ( + !details.includes( + "The target has already been logged in via an iSCSI session" + ) + ) { + throw err; + } + } + } + + async GetIscsiTargetsByTargetPortalAddressTargetPortalPort(address, port) { + let command; + let result; + + command = + "Get-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port} | Get-IscsiTarget | ConvertTo-Json"; + result = await this.ps.exec(command, { + env: { + iscsi_tp_address: address, + iscsi_tp_port: port, + }, + }); + this.resultToArray(result); + + return result.parsed; + } + + /** + * This disconnects *all* sessions from the target + * + * @param {*} nodeAddress + */ + async DisconnectIscsiTargetByNodeAddress(nodeAddress) { + let command; + + // https://github.com/PowerShell/PowerShell/issues/17306 + command = `Disconnect-IscsiTarget -NodeAddress ${nodeAddress.toLowerCase()} -Confirm:$false`; + await this.ps.exec(command); + } + + async GetIscsiConnectionsByIscsiSessionIdentifier(iscsiSessionIdentifier) { + let command; + let result; + + command = `Get-IscsiSession -SessionIdentifier ${iscsiSessionIdentifier} | Get-IscsiConnection | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetIscsiSessions() { + let command; + let result; + + command = `Get-IscsiSession | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetIscsiSessionsByDiskNumber(diskNumber) { + let command; + let result; + + command = `Get-Disk -Number ${diskNumber} | Get-IscsiSession | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetIscsiSessionsByVolumeId(volumeId) { + let sessions = []; + let disks = await this.GetDisksByVolumeId(volumeId); + for (let disk of disks) { + let i_sessions = await this.GetIscsiSessionsByDiskNumber(disk.DiskNumber); + sessions.push(...i_sessions); + } + + return sessions; + } + + async GetIscsiSessionsByTargetNodeAddress(targetNodeAddress) { + let sessions = await this.GetIscsiSessions(); + let r_sessions = []; + // Where-Object { $_.TargetNodeAddress -eq ${targetNodeAddress} } + for (let session of sessions) { + if (session.TargetNodeAddress == targetNodeAddress) { + r_sessions.push(session); + } + } + + return r_sessions; + } + + async GetIscsiSessionByIscsiConnectionIdentifier(iscsiConnectionIdentifier) { + let command; + let result; + + command = `Get-IscsiConnection -ConnectionIdentifier ${iscsiConnectionIdentifier} | Get-IscsiSession | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed; + } + + async GetIscsiTargetPortalBySessionId(sessionId) { + let command; + let result; + + command = `Get-IscsiSession -SessionIdentifier ${sessionId} | Get-IscsiTargetPortal | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed; + } + + async UpdateHostStorageCache() { + let command; + command = "Update-HostStorageCache"; + await this.ps.exec(command); + } + + async GetIscsiDisks() { + let command; + let result; + + command = "Get-iSCSISession | Get-Disk | ConvertTo-Json"; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetWin32DiskDrives() { + let command; + let result; + + command = "Get-WmiObject Win32_DiskDrive | ConvertTo-Json"; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetDiskLunByDiskNumber(diskNumber) { + let result; + result = await this.GetWin32DiskDrives(); + for (let drive of result) { + if (drive.Index == diskNumber) { + return drive.SCSILogicalUnit; + } + } + } + + async GetTargetDisks(address, port, iqn) { + let command; + let result; + + // this fails for synology for some reason + //command = + // '$ErrorActionPreference = "Stop"; $tp = Get-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port}; $t = $tp | Get-IscsiTarget | Where-Object { $_.NodeAddress -eq ${Env:iscsi_target_iqn} }; $s = Get-iSCSISession -IscsiTarget $t; $s | Get-Disk | ConvertTo-Json'; + + command = + '$ErrorActionPreference = "Stop"; $s = Get-iSCSISession | Where-Object { $_.TargetNodeAddress -eq ${Env:iscsi_target_iqn} }; $s | Get-Disk | ConvertTo-Json'; + + result = await this.ps.exec(command, { + env: { + iscsi_tp_address: address, + iscsi_tp_port: port, + iscsi_target_iqn: iqn, + }, + }); + this.resultToArray(result); + + return result.parsed; + } + + async GetTargetDisksByIqn(iqn) { + let command; + let result; + + command = + '$ErrorActionPreference = "Stop"; $s = Get-iSCSISession | Where-Object { $_.TargetNodeAddress -eq ${Env:iscsi_target_iqn} }; $s | Get-Disk | ConvertTo-Json'; + + result = await this.ps.exec(command, { + env: { + iscsi_target_iqn: iqn, + }, + }); + this.resultToArray(result); + + return result.parsed; + } + + /** + * This can be multiple when mpio is not configured properly and each + * session creates a new disk + * + * @param {*} iqn + * @param {*} lun + * @returns + */ + async GetTargetDisksByIqnLun(iqn, lun) { + let result; + let dlun; + let disks = []; + + result = await this.GetTargetDisksByIqn(iqn); + for (let disk of result) { + dlun = await this.GetDiskLunByDiskNumber(disk.DiskNumber); + if (dlun == lun) { + disks.push(disk); + } + } + + return disks; + } + + async GetDiskByDiskNumber(diskNumber) { + let command; + let result; + + command = `Get-Disk -Number ${diskNumber} | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed; + } + + async GetDisks() { + let command; + let result; + + command = "Get-Disk | ConvertTo-Json"; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetPartitions() { + let command; + let result; + + command = "Get-Partition | ConvertTo-Json"; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetPartitionsByDiskNumber(diskNumber) { + let command; + let result; + + command = `Get-Disk -Number ${diskNumber} | Get-Partition | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async DiskIsInitialized(diskNumber) { + let disk = await this.GetDiskByDiskNumber(diskNumber); + + return disk.PartitionStyle != "RAW"; + } + + async InitializeDisk(diskNumber) { + let command; + + command = `Initialize-Disk -Number ${diskNumber} -PartitionStyle GPT`; + await this.ps.exec(command); + } + + async DiskHasBasicPartition(diskNumber) { + let command; + let result; + + command = `Get-Partition | Where DiskNumber -eq ${diskNumber} | Where Type -ne Reserved | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed.length > 0; + } + + async NewPartition(diskNumber) { + let command; + + command = `New-Partition -DiskNumber ${diskNumber} -UseMaximumSize`; + await this.ps.exec(command); + } + + async PartitionDisk(diskNumber) { + let is_intialized; + let has_basic_partition; + + is_intialized = await this.DiskIsInitialized(diskNumber); + if (!is_intialized) { + await this.InitializeDisk(diskNumber); + } + + has_basic_partition = await this.DiskHasBasicPartition(diskNumber); + if (!has_basic_partition) { + await this.NewPartition(diskNumber); + } + } + + async GetLastPartitionByDiskNumber(diskNumber) { + let partitions = await this.GetPartitionsByDiskNumber(diskNumber); + let p; + for (let partition of partitions) { + if (!p) { + p = partition; + } + + if (partition.PartitionNumber > p.PartitionNumber) { + p = partition; + } + } + + return p; + } + + async GetVolumesByDiskNumber(diskNumber) { + let command; + command = `Get-Disk -Number ${diskNumber} | Get-Partition | Get-Volume | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetVolumeByDiskNumberPartitionNumber(diskNumber, partitionNumber) { + let command; + let result; + + command = `Get-Disk -Number ${diskNumber} | Get-Partition -PartitionNumber ${partitionNumber} | Get-Volume | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed; + } + + async GetVolumeByVolumeId(volumeId) { + let command; + let result; + + command = `Get-Volume -UniqueId \"${volumeId}\" -ErrorAction Stop | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed; + } + + async GetPartitionsByVolumeId(volumeId) { + let partitions = await this.GetPartitions(); + let p = []; + for (let partition of partitions) { + let paths = _.get(partition, "AccessPaths", []); + if (paths === null) { + paths = []; + } + if (!Array.isArray(paths)) { + paths = []; + } + if (paths.includes(volumeId)) { + p.push(partition); + } + } + return p; + } + + async GetDisksByVolumeId(volumeId) { + let partitions = await this.GetPartitionsByVolumeId(volumeId); + let diskNumbers = new Set(); + for (let parition of partitions) { + diskNumbers.add(parition.DiskNumber); + } + + let disks = []; + let disk; + for (let diskNumber of diskNumbers) { + disk = await this.GetDiskByDiskNumber(diskNumber); + if (disk) { + disks.push(disk); + } + } + + return disks; + } + + async VolumeIsFormatted(volumeId) { + let volume = await this.GetVolumeByVolumeId(volumeId); + let type = volume.FileSystemType || ""; + type = type.toLowerCase().trim(); + if (!type || type == "unknown") { + return false; + } + + return true; + } + + async VolumeIsIscsi(volumeId) { + let disks = await this.GetDisksByVolumeId(volumeId); + for (let disk of disks) { + if (_.get(disk, "BusType", "").toLowerCase() == "iscsi") { + return true; + } + } + + return false; + } + + async FormatVolume(volumeId) { + let command; + command = `Get-Volume -UniqueId \"${volumeId}\" | Format-Volume -FileSystem ntfs -Confirm:$false`; + await this.ps.exec(command); + } + + async ResizeVolume(volumeId, size = 0) { + let command; + let final_size; + + if (!size) { + final_size = await this.GetVolumeMaxSize(volumeId); + } else { + final_size = size; + } + + let current_size = await this.GetVolumeSize(volumeId); + if (current_size >= final_size) { + return; + } + + command = `Get-Volume -UniqueId \"${volumeId}\" | Get-Partition | Resize-Partition -Size ${final_size}`; + try { + await this.ps.exec(command); + } catch (err) { + let details = _.get(err, "stderr", ""); + if ( + !details.includes( + "The size of the extent is less than the minimum of 1MB" + ) + ) { + throw err; + } + } + } + + async GetVolumeMaxSize(volumeId) { + let command; + let result; + + command = `Get-Volume -UniqueId \"${volumeId}\" | Get-partition | Get-PartitionSupportedSize | Select SizeMax | ConvertTo-Json`; + result = await this.ps.exec(command); + return result.parsed.SizeMax; + } + async GetVolumeSize(volumeId) { + let command; + let result; + + command = `Get-Volume -UniqueId \"${volumeId}\" | Get-partition | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed.Size; + } + + async MountVolume(volumeId, path) { + let command; + command = `Get-Volume -UniqueId \"${volumeId}\" | Get-Partition | Add-PartitionAccessPath -AccessPath ${path}`; + + await this.ps.exec(command); + } + + async UnmountVolume(volumeId, path) { + let command; + + // this errors if it does not have a drive letter + if (!GeneralUtils.hasWindowsDriveLetter(path)) { + let item = await this.GetItem(path); + if (!item) { + return; + } + path = item.FullName; + } + + command = `Get-Volume -UniqueId \"${volumeId}\" | Get-Partition | Remove-PartitionAccessPath -AccessPath ${path}`; + + await this.ps.exec(command); + } + + async WriteVolumeCache(volumeId) { + let command; + command = `Get-Volume -UniqueId \"${volumeId}\" | Write-Volumecache`; + + await this.ps.exec(command); + } +} + +module.exports.Windows = Windows; diff --git a/src/utils/zfs.js b/src/utils/zfs.js index f682eb8..a26db89 100644 --- a/src/utils/zfs.js +++ b/src/utils/zfs.js @@ -1147,14 +1147,23 @@ class Zetabyte { if (arguments.length < 4) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { + // specially handle sudo here to avoid the need for using sudo on the whole script + // but rather limit sudo access to only the zfs command + let use_sudo = zb.options.sudo; let args = ["-c"]; let command = []; + if (use_sudo) { + command = command.concat(zb.options.paths.sudo); + } command = command.concat(["zfs", "send"]); command = command.concat(send_options); command.push(source); command.push("|"); + if (use_sudo) { + command = command.concat(zb.options.paths.sudo); + } command = command.concat(["zfs", "receive"]); command = command.concat(receive_options); command.push(target); @@ -1164,7 +1173,7 @@ class Zetabyte { zb.exec( "/bin/sh", args, - { timeout: zb.options.timeout }, + { timeout: zb.options.timeout, sudo: false }, function (error, stdout, stderr) { if (error) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); @@ -1550,7 +1559,12 @@ class Zetabyte { command = zb.options.paths.chroot; } - if (zb.options.sudo) { + let use_sudo = zb.options.sudo; + if (options && options.hasOwnProperty("sudo")) { + use_sudo = options.sudo; + } + + if (use_sudo) { args = args || []; args.unshift(command); command = zb.options.paths.sudo; @@ -1558,11 +1572,13 @@ class Zetabyte { if (zb.options.log_commands) { if (typeof zb.options.logger.verbose != "function") { - zb.options.logger.verbose = function() { + zb.options.logger.verbose = function () { console.debug(...arguments); - } + }; } - zb.options.logger.verbose(`executing zfs command: ${command} ${args.join(" ")}`); + zb.options.logger.verbose( + `executing zfs command: ${command} ${args.join(" ")}` + ); } const child = zb.options.executor.spawn(command, args, options);