Merge pull request #538 from democratic-csi/next

Next
This commit is contained in:
Travis Glenn Hansen 2026-01-07 15:17:05 -07:00 committed by GitHub
commit 3974268272
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
86 changed files with 15037 additions and 3923 deletions

View File

@ -0,0 +1,40 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "democratic-csi",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
// regarding versioning the first number is the devcontainer image version,
// the second is the Node.js version, and the third is the OS version.
"image": "mcr.microsoft.com/devcontainers/typescript-node:4-20-bookworm",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/go:1": {} // To compile csi-sanity during postCreateCommand
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "/bin/bash .devcontainer/postCreate.sh",
// Configure tool-specific properties.
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"waderyan.nodejs-extension-pack",
"ms-vscode.node-debug2",
"christian-kohler.npm-intellisense",
"christian-kohler.path-intellisense",
"HashiCorp.terraform"
]
}
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

12
.devcontainer/postCreate.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/env bash
npm install
git clone https://github.com/kubernetes-csi/csi-test /tmp/csi-test
pushd /tmp/csi-test
make
sudo cp /tmp/csi-test/cmd/csi-sanity/csi-sanity /usr/local/bin
popd
rm -rf /tmp/csi-test
sudo apt update

View File

@ -3,7 +3,7 @@
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
echo "$GHCR_PASSWORD" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin
export DOCKER_ORG="democraticcsi"
export DOCKER_PROJECT="democratic-csi"
@ -16,29 +16,29 @@ 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
# create local manifest to work with
buildah manifest rm "${MANIFEST_NAME}" || true
buildah manifest create "${MANIFEST_NAME}"
# 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}"
# all all the existing linux data to the manifest
buildah manifest add "${MANIFEST_NAME}" --all "${DOCKER_REPO}:${IMAGE_TAG}"
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}
# import pre-built images
buildah pull docker-archive:democratic-csi-windows-ltsc2022.tar
buildah pull docker-archive:democratic-csi-windows-ltsc2025.tar
# cleanup
buildah manifest rm "${MANIFEST_NAME}" || true
# add pre-built images to manifest
buildah manifest add "${MANIFEST_NAME}" democratic-csi-windows:${GITHUB_RUN_ID}-ltsc2022
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
:
fi

12
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly

View File

@ -20,7 +20,7 @@ jobs:
access_token: ${{ github.token }}
build-npm-linux-amd64:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -115,7 +115,7 @@ jobs:
SYNOLOGY_PASSWORD: ${{ secrets.SANITY_SYNOLOGY_PASSWORD }}
SYNOLOGY_VOLUME: ${{ secrets.SANITY_SYNOLOGY_VOLUME }}
csi-sanity-truenas-scale-24_04:
csi-sanity-truenas-scale-25_10:
needs:
- build-npm-linux-amd64
strategy:
@ -123,10 +123,11 @@ jobs:
max-parallel: 1
matrix:
config:
- truenas/scale/24.04/scale-iscsi.yaml
- truenas/scale/24.04/scale-nfs.yaml
- truenas/scale/25.10/scale-iscsi.yaml
- truenas/scale/25.10/scale-nfs.yaml
- truenas/scale/25.10/scale-nvmeof.yaml
# 80 char limit
- truenas/scale/24.04/scale-smb.yaml
- truenas/scale/25.10/scale-smb.yaml
runs-on:
- self-hosted
- Linux
@ -144,7 +145,7 @@ jobs:
ci/bin/run.sh
env:
TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}"
TRUENAS_HOST: ${{ secrets.SANITY_TRUENAS_SCALE_24_04_HOST }}
TRUENAS_HOST: ${{ secrets.SANITY_TRUENAS_SCALE_HOST }}
TRUENAS_USERNAME: ${{ secrets.SANITY_TRUENAS_USERNAME }}
TRUENAS_PASSWORD: ${{ secrets.SANITY_TRUENAS_PASSWORD }}
@ -183,7 +184,7 @@ jobs:
TRUENAS_PASSWORD: ${{ secrets.SANITY_TRUENAS_PASSWORD }}
# ssh-based drivers
csi-sanity-zfs-generic:
csi-sanity-zfs-generic-targetcli:
needs:
- build-npm-linux-amd64
strategy:
@ -191,7 +192,7 @@ jobs:
max-parallel: 1
matrix:
config:
- zfs-generic/iscsi.yaml
- zfs-generic/iscsi-targetcli.yaml
- zfs-generic/nfs.yaml
- zfs-generic/smb.yaml
- zfs-generic/nvmeof.yaml
@ -211,7 +212,36 @@ jobs:
ci/bin/run.sh
env:
TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}"
SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_HOST }}
SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_TARGETCLI_HOST }}
SERVER_USERNAME: ${{ secrets.SANITY_ZFS_GENERIC_USERNAME }}
SERVER_PASSWORD: ${{ secrets.SANITY_ZFS_GENERIC_PASSWORD }}
csi-sanity-zfs-generic-pcs:
needs:
- build-npm-linux-amd64
strategy:
fail-fast: false
max-parallel: 1
matrix:
config:
- zfs-generic/iscsi-pcs.yaml
runs-on:
- self-hosted
- Linux
- X64
- csi-sanity-zfs-generic
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
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_PCS_HOST }}
SERVER_USERNAME: ${{ secrets.SANITY_ZFS_GENERIC_USERNAME }}
SERVER_PASSWORD: ${{ secrets.SANITY_ZFS_GENERIC_PASSWORD }}
@ -435,9 +465,10 @@ jobs:
- determine-image-tag
- csi-sanity-synology-dsm6
- csi-sanity-synology-dsm7
- csi-sanity-truenas-scale-24_04
- csi-sanity-truenas-scale-25_10
- csi-sanity-truenas-core-13_0
- csi-sanity-zfs-generic
- csi-sanity-zfs-generic-targetcli
- csi-sanity-zfs-generic-pcs
- csi-sanity-objectivefs
- csi-sanity-client
- csi-sanity-client-windows
@ -468,16 +499,18 @@ jobs:
GHCR_PASSWORD: ${{ secrets.GHCR_PASSWORD }}
OBJECTIVEFS_DOWNLOAD_ID: ${{ secrets.OBJECTIVEFS_DOWNLOAD_ID }}
DOCKER_CLI_EXPERIMENTAL: enabled
DOCKER_BUILD_PLATFORM: linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le
# DOCKER_BUILD_PLATFORM: linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le
DOCKER_BUILD_PLATFORM: linux/amd64,linux/arm64
IMAGE_TAG: ${{needs.determine-image-tag.outputs.tag}}
build-docker-windows:
needs:
- csi-sanity-synology-dsm6
- csi-sanity-synology-dsm7
- csi-sanity-truenas-scale-24_04
- csi-sanity-truenas-scale-25_10
- csi-sanity-truenas-core-13_0
- csi-sanity-zfs-generic
- csi-sanity-zfs-generic-targetcli
- csi-sanity-zfs-generic-pcs
- csi-sanity-objectivefs
- csi-sanity-client
- csi-sanity-client-windows
@ -487,16 +520,16 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-2019, windows-2022]
os: [windows-2022, windows-2025]
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
- os: windows-2025
core_base_tag: ltsc2025
nano_base_tag: ltsc2025
file: Dockerfile.Windows
steps:
- uses: actions/checkout@v4
- name: docker build
@ -528,10 +561,10 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: democratic-csi-windows-ltsc2019.tar
name: democratic-csi-windows-ltsc2022.tar
- uses: actions/download-artifact@v4
with:
name: democratic-csi-windows-ltsc2022.tar
name: democratic-csi-windows-ltsc2025.tar
- name: push windows images with buildah
run: |
#.github/bin/install_latest_buildah.sh

4
.gitignore vendored
View File

@ -2,3 +2,7 @@
node_modules
dev
/ci/bin/*dev*
.vagrant
hack/*
!hack/run.sh
!hack/build_push.sh

View File

@ -1,3 +1,24 @@
# v1.9.5
Released 2026-01-07
- better support for nixos
- support SCALE-25.04
- support SCALE-25.10
- improved nvmeof support
- added `pcs` share strategy for `zfs-generic-iscsi` (see PR #464)
- added `freenas-nvmeof` and `freenas-api-nvmeof` driver to use the new nmveof features of TrueNAS 25.10+
- support for ENV vars in the configuration yaml `${FOO}` will expand
- improved docker images
- bumped deps and bundled binaries
- support csi versions `v1.10.0` and `v1.11.0`
- new `containerd-oci-ephemeral-inline` driver
- new `vhd-ephemeral-inline` driver
- bump `objectivefs` binary to `7.3`
- possible to set `snapshotProperties` on zfs snapshot just like `datasetProperties`
- limit container images to amd64 and arm64 for now
- improve concurrency logic in the `zf-generic-foo` drivers (see #504)
# v1.9.4
Release 2024-07-06

159
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,159 @@
# Contributing to democratic-csi
## Development Environment Setup
This project uses a hybrid development approach with devcontainers for IDE configuration and Vagrant for system-level testing.
### Prerequisites
Before you begin, ensure you have the following installed:
- [Visual Studio Code](https://code.visualstudio.com/)
- [Docker](https://www.docker.com/get-started)
- [Vagrant](https://www.vagrantup.com/downloads)
- Virtualization Provider:
- For Intel/AMD Machines: VirtualBox
- For Apple Silicon: Qemu (`brew install qemu vagrant` and `vagrant plugin install vagrant-qemu`)
### Development Workflow
#### 1. Local Development with Devcontainers
Devcontainers provide a consistent development environment with:
- Configured VSCode extensions
- Necessary development tools
- Integrated development experience
To use the devcontainer:
1. Open the project in VSCode
2. Install the "Dev Containers" extension
3. Click "Reopen in Container" when prompted
4. Start coding with pre-configured environment
> [!Note]
> For `iSCSI` it's mandatory to use the Vagrant VM, due to the need of a kernel driver.
> However for other tests the container is probably enough. It's possible to run the `hack/run.sh`
> as explained below in the devcontainer and see if it's possible, before spinning up a full VM.
#### 2. System Testing with Vagrant
Vagrant provides a full virtual machine environment for:
- System-level testing
- Running code with kernel dependencies
- Simulating production-like environments
Workflow:
```bash
# Navigate to project directory
cd ~/democratic-csi
# Start the Vagrant VM
vagrant up
# Connect to the VM
vagrant ssh
# Inside the VM, navigate to the project
cd ~/democratic-csi
# Run project tests, the config.yaml can be any from the examples folders
# just configured for your own environment.
# You can also create a file `dev/secrets.env` that has `export VARIABLE=VALUE`
# and reference those in your `config.yaml`
./hack/run.sh -c ./hack/config.yaml
```
>![Note]
> For running tests with democratic-csi the authentication needs to be disabled, as
> it always initiates connections to the share without authentication.
##### Keeping Files in Sync
Use these methods to keep your local files synchronized with the Vagrant VM:
###### Manual Sync
```bash
# Sync files from local to Vagrant VM
vagrant rsync
```
###### Continuous Sync
```bash
# Automatically sync files as they change
vagrant rsync-auto
```
#### 3. Deploy development version to K8s cluster
Deployment provides a good environment for:
- Final testing in a real world scenario
- Run the final version until included in a release
> [!Note]
> Make sure to do the build on the architecture you will be running it.
> For example, don't build in Apple Silicon if your cluster runs in amd64.
1. Login to your github container registry
```bash
docker login ghcr.io
```
> [!Important]
> Login to the container registry is stored plain text, use a PAT instead of your Github password. [Create a PAT with write:packages](https://github.com/settings/tokens/new?scopes=write:packages).
2. Compile and push to your github container registry.
```bash
./hack/build_push.sh
```
3. When you deploy, in the `values.yaml` add the following, using the output from the script
```yaml
controller:
driver:
image: ghcr.io/your_user/democratic-csi:your_branch-fc02fc4
node:
driver:
image: ghcr.io/your_user/democratic-csi:your_branch-fc02fc4
```
4. Make the Image Public
By default, images pushed to GHCR are private. To make it public:
1. Go to GitHub → Your Repository → Packages (or directly github.com/USERNAME?tab=packages)
2. Select the package
3. Click Package Settings
4. Change Visibility to Public
### Best Practices
- Use devcontainer for day-to-day development and coding
- Use Vagrant for comprehensive system testing
- Always run `vagrant rsync` before running tests in the VM
- Commit and push changes frequently
- If encountering issues, try:
1. Recreating the devcontainer
2. Reprovisioning the Vagrant VM with `vagrant reload --provision` or `vagrant destroy -f && vagrant up`
### Troubleshooting
#### Devcontainer Issues
- Ensure Docker is running
- Rebuild the container if extensions fail to load
- Check VSCode Dev Containers extension logs
#### Vagrant Issues
- Verify virtualization is enabled in your BIOS
- Ensure you have the latest Vagrant and virtualization provider
- For Apple Silicon, use Parallels or Lima
### Contribution Guidelines
1. Create a new branch for your feature targetting `next`
2. Write clear, concise commit messages
3. Include coverage for tests of csi-sanity for new functionality
4. Run tests in Vagrant VM
5. Submit a pull request with a clear description of changes
### Contact
For any questions or issues, please [open an issue](https://github.com/democratic-csi/democratic-csi/issues) on the project repository.

View File

@ -1,3 +1,25 @@
# docker build --pull -t foobar .
# docker buildx build --pull -t foobar --platform linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le .
# docker run --rm -ti --user root --entrypoint /bin/bash foobar
######################
# golang builder
######################
# FROM golang:1.25.3-bookworm AS ctrbuilder
#
# # /go/containerd/ctr
# ADD docker/ctr-mount-labels.diff /tmp
# RUN \
# git clone https://github.com/containerd/containerd.git; \
# cd containerd && \
# git checkout v2.0.4 && \
# git apply /tmp/ctr-mount-labels.diff && \
# CGO_ENABLED=0 go build ./cmd/ctr/;
######################
# nodejs builder
######################
FROM debian:12-slim AS build
#FROM --platform=$BUILDPLATFORM debian:10-slim AS build
@ -12,11 +34,11 @@ 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=v20.11.1
ENV NODE_VERSION=v20.19.0
ENV NODE_ENV=production
# install build deps
RUN apt-get update && apt-get install -y python3 make cmake gcc g++
# RUN apt-get update && apt-get install -y python3 make cmake gcc g++
# install node
RUN apt-get update && apt-get install -y wget xz-utils
@ -24,6 +46,13 @@ ADD docker/node-installer.sh /usr/local/sbin
RUN chmod +x /usr/local/sbin/node-installer.sh && node-installer.sh
ENV PATH=/usr/local/lib/nodejs/bin:$PATH
# Workaround for https://github.com/nodejs/node/issues/37219
RUN test $(uname -m) != armv7l || ( \
apt-get update \
&& apt-get install -y libatomic1 \
&& rm -rf /var/lib/apt/lists/* \
)
# Run as a non-root user
RUN useradd --create-home csi \
&& mkdir /home/csi/app \
@ -31,6 +60,11 @@ RUN useradd --create-home csi \
WORKDIR /home/csi/app
USER csi
# prevent need to build re2 module
# https://github.com/uhop/install-artifact-from-github/wiki/Making-local-mirror
ENV RE2_DOWNLOAD_MIRROR="https://grpc-uds-binaries.s3-us-west-2.amazonaws.com/re2"
ENV RE2_DOWNLOAD_SKIP_PATH=1
COPY --chown=csi:csi package*.json ./
RUN npm install --only=production --grpc_node_binary_host_mirror=https://grpc-uds-binaries.s3-us-west-2.amazonaws.com/debian-buster
COPY --chown=csi:csi . .
@ -68,6 +102,9 @@ RUN test $(uname -m) != armv7l || ( \
&& rm -rf /var/lib/apt/lists/* \
)
# install ctr
#COPY --from=ctrbuilder /go/containerd/ctr /usr/local/bin/ctr
# install node
#ENV PATH=/usr/local/lib/nodejs/bin:$PATH
#COPY --from=build /usr/local/lib/nodejs /usr/local/lib/nodejs
@ -80,49 +117,57 @@ RUN apt-get update && \
apt-get install -y wget netbase zip bzip2 socat e2fsprogs exfatprogs xfsprogs btrfs-progs fatresize dosfstools ntfs-3g nfs-common cifs-utils fdisk gdisk cloud-guest-utils sudo rsync procps util-linux nvme-cli fuse3 && \
rm -rf /var/lib/apt/lists/*
ARG RCLONE_VERSION=1.66.0
RUN \
echo '83e7a026-2564-455b-ada6-ddbdaf0bc519' > /etc/nvme/hostid && \
echo 'nqn.2014-08.org.nvmexpress:uuid:941e4f03-2cd6-435e-86df-731b1c573d86' > /etc/nvme/hostnqn
ARG RCLONE_VERSION=1.71.2
ADD docker/rclone-installer.sh /usr/local/sbin
RUN chmod +x /usr/local/sbin/rclone-installer.sh && rclone-installer.sh
ARG RESTIC_VERSION=0.16.4
ARG RESTIC_VERSION=0.18.1
ADD docker/restic-installer.sh /usr/local/sbin
RUN chmod +x /usr/local/sbin/restic-installer.sh && restic-installer.sh
ARG KOPIA_VERSION=0.16.1
ARG KOPIA_VERSION=0.21.1
ADD docker/kopia-installer.sh /usr/local/sbin
RUN chmod +x /usr/local/sbin/kopia-installer.sh && kopia-installer.sh
ARG YQ_VERSION=v4.48.1
ADD docker/yq-installer.sh /usr/local/sbin
RUN chmod +x /usr/local/sbin/yq-installer.sh && yq-installer.sh
ARG CTR_VERSION=v2.0.4
ADD docker/ctr-installer.sh /usr/local/sbin
RUN chmod +x /usr/local/sbin/ctr-installer.sh && ctr-installer.sh
# controller requirements
#RUN apt-get update && \
# apt-get install -y ansible && \
# rm -rf /var/lib/apt/lists/*
# install objectivefs
ARG OBJECTIVEFS_VERSION=7.2
ARG OBJECTIVEFS_VERSION=7.3
ADD docker/objectivefs-installer.sh /usr/local/sbin
RUN chmod +x /usr/local/sbin/objectivefs-installer.sh && objectivefs-installer.sh
# install wrappers
ADD docker/iscsiadm /usr/local/sbin
RUN chmod +x /usr/local/sbin/iscsiadm
ADD docker/multipath /usr/local/sbin
RUN chmod +x /usr/local/sbin/multipath
## USE_HOST_MOUNT_TOOLS=1
ADD docker/mount /usr/local/bin/mount
RUN chmod +x /usr/local/bin/mount
## USE_HOST_MOUNT_TOOLS=1
ADD docker/umount /usr/local/bin/umount
RUN chmod +x /usr/local/bin/umount
ADD docker/zfs /usr/local/bin/zfs
RUN chmod +x /usr/local/bin/zfs
ADD docker/zpool /usr/local/bin/zpool
RUN chmod +x /usr/local/bin/zpool
ADD docker/oneclient /usr/local/bin/oneclient
RUN chmod +x /usr/local/bin/oneclient
RUN chown -R root:root /usr/local/bin/*
RUN chmod +x /usr/local/bin/*
# Run as a non-root user
RUN useradd --create-home csi \

View File

@ -3,9 +3,11 @@
# 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/
#
# https://stackoverflow.com/questions/65104246/how-to-install-powershell-core-in-aspnet-nanoserver-docker-container
#
# 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 -ti --entrypoint cmd foobar
# docker run --rm foobar
# docker save foobar -o foobar.tar
# buildah pull docker-archive:foobar.tar
@ -16,85 +18,98 @@
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
ENV POWERSHELL_TELEMETRY_OPTOUT="1"
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
ARG PS_VERSION=7.5.0
ADD https://github.com/PowerShell/PowerShell/releases/download/v$PS_VERSION/PowerShell-$PS_VERSION-win-x64.zip /PowerShell/powershell.zip
# 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.18.0
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 \
Expand-Archive '/PowerShell/powershell.zip' -DestinationPath '/PowerShell' ; \
cd C:\PowerShell ; \
del powershell.zip ; \
New-Item -ItemType SymbolicLink -Path "powershell.exe" -Target "pwsh.exe"
ENV NODE_VERSION 20.19.0
ENV NODE_ENV=production
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'
Expand-Archive node.zip -DestinationPath C:\ ; \
Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs'
RUN mkdir \usr\local\bin; mkdir \tmp
ARG RCLONE_VERSION=v1.71.2
RUN Invoke-WebRequest "https://github.com/rclone/rclone/releases/download/${env:RCLONE_VERSION}/rclone-${env:RCLONE_VERSION}-windows-amd64.zip" -OutFile '/tmp/rclone.zip' -UseBasicParsing ; \
Expand-Archive C:\tmp\rclone.zip -DestinationPath C:\tmp ; \
Copy-Item $('C:\tmp\rclone-{0}-windows-amd64\rclone.exe' -f $env:RCLONE_VERSION) -Destination "C:\usr\local\bin"
ARG RESTIC_VERSION=0.18.1
RUN Invoke-WebRequest "https://github.com/restic/restic/releases/download/v${env:RESTIC_VERSION}/restic_${env:RESTIC_VERSION}_windows_amd64.zip" -OutFile '/tmp/restic.zip' -UseBasicParsing ; \
Expand-Archive C:\tmp\restic.zip -DestinationPath C:\tmp ; \
Copy-Item $('C:\tmp\restic_{0}_windows_amd64.exe' -f $env:RESTIC_VERSION) -Destination "C:\usr\local\bin\restic.exe"
ARG KOPIA_VERSION=0.21.1
RUN Invoke-WebRequest "https://github.com/kopia/kopia/releases/download/v${env:KOPIA_VERSION}/kopia-${env:KOPIA_VERSION}-windows-x64.zip" -OutFile '/tmp/kopia.zip' -UseBasicParsing ; \
Expand-Archive C:\tmp\kopia.zip -DestinationPath C:\tmp ; \
Copy-Item $('C:\tmp\kopia-{0}-windows-x64\kopia.exe' -f $env:KOPIA_VERSION) -Destination "C:\usr\local\bin"
ARG YQ_VERSION=v4.48.1
RUN Invoke-WebRequest "https://github.com/mikefarah/yq/releases/download/${env:YQ_VERSION}/yq_windows_amd64.zip" -OutFile '/tmp/yq.zip' -UseBasicParsing ; \
Expand-Archive C:\tmp\yq.zip -DestinationPath C:\tmp ; \
Copy-Item $('C:\tmp\yq_windows_amd64.exe') -Destination "C:\usr\local\bin\yq.exe"
RUN Remove-Item C:\tmp\ -Force -Recurse
# install app
#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 . .
######################
# actual image
######################
FROM mcr.microsoft.com/windows/nanoserver:${NANO_BASE_TAG}
#FROM mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0
#SHELL ["cmd.exe", "/s" , "/c"]
#https://github.com/PowerShell/PowerShell-Docker/issues/236
# NOTE: this works for non-host process containers, but host process containers will have specials PATH requirements
# C:\Windows\System32\WindowsPowerShell\v1.0\
#ENV PATH="C:\Windows\system32;C:\Windows;C:\PowerShell;C:\app\bin;"
ENV DEMOCRATIC_CSI_IS_CONTAINER=true
ENV NODE_ENV=production
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
# install powershell
#COPY --from=build /PowerShell /PowerShell
# install app
COPY --from=build /app /app
WORKDIR /app
# this works for both host-process and non-host-process container semantics
ADD https://github.com/democratic-csi/democratic-csi/releases/download/v1.0.0/ctr-v2.0.4-windows-amd64.exe ./bin/ctr.exe
COPY --from=build /nodejs/node.exe ./bin
COPY --from=build /usr/local/bin/ ./bin
ENTRYPOINT [ "bin/node.exe", "--expose-gc", "bin/democratic-csi" ]
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'Continue'; $verbosePreference='Continue';"]
EXPOSE 50051
# this works for both host-process and non-host-process container semantics
#ENTRYPOINT [ "bin/node.exe", "--expose-gc", "bin/democratic-csi" ]
ADD docker/entrypoint.ps1 ./bin
# NOTE: this powershell.exe could be problematic based on overriding PATH in container vs host etc
ENTRYPOINT [ "powershell.exe", "bin/entrypoint.ps1" ]

View File

@ -19,9 +19,11 @@ have access to resizing, snapshots, clones, etc functionality.
- `freenas-nfs` (manages zfs datasets to share over nfs)
- `freenas-iscsi` (manages zfs zvols to share over iscsi)
- `freenas-smb` (manages zfs datasets to share over smb)
- `freenas-nvmeof` (manages zfs zvols to share over nvmeof)
- `freenas-api-nfs` experimental use with SCALE only (manages zfs datasets to share over nfs)
- `freenas-api-iscsi` experimental use with SCALE only (manages zfs zvols to share over iscsi)
- `freenas-api-smb` experimental use with SCALE only (manages zfs datasets to share over smb)
- `freenas-api-nvmeof` experimental use with SCALE only (manages zfs zvols to share over nvmeof)
- `zfs-generic-nfs` (works with any ZoL installation...ie: Ubuntu)
- `zfs-generic-iscsi` (works with any ZoL installation...ie: Ubuntu)
- `zfs-generic-smb` (works with any ZoL installation...ie: Ubuntu)
@ -41,6 +43,8 @@ have access to resizing, snapshots, clones, etc functionality.
- `node-manual` (allows connecting to manually created smb, nfs, lustre,
oneclient, nvmeof, and iscsi volumes, see sample PVs in the `examples`
directory)
- `containerd-oci-ephemeral-inline` (provisions ephemeral rw node-local storage using oci images as a base)
- `vhd-ephemeral-inline` (provisions ephemeral rw node-local storage using vhd images as a base)
- framework for developing `csi` drivers
If you have any interest in providing a `csi` driver, simply open an issue to
@ -58,6 +62,8 @@ Predominantly 3 things are needed:
## Community Guides
Join us in the Home Operations discord server in #democratic-csi
- https://jonathangazeley.com/2021/01/05/using-truenas-to-provide-persistent-storage-for-kubernetes/
- https://www.lisenet.com/2021/moving-to-truenas-and-democratic-csi-for-kubernetes-persistent-storage/
- https://gist.github.com/admun/4372899f20421a947b7544e5fc9f9117 (migrating
@ -65,6 +71,7 @@ Predominantly 3 things are needed:
- https://gist.github.com/deefdragon/d58a4210622ff64088bd62a5d8a4e8cc
(migrating between storage classes using `velero`)
- https://github.com/fenio/k8s-truenas (NFS/iSCSI over API with TrueNAS Scale)
- https://wazaari.dev/blog/truenas-talos-democratic-csi
## Node Prep
@ -328,7 +335,7 @@ Set-MSDSMGlobalLoadBalancePolicy -Policy RR
Server preparation depends slightly on which `driver` you are using.
### FreeNAS (freenas-nfs, freenas-iscsi, freenas-smb, freenas-api-nfs, freenas-api-iscsi, freenas-api-smb)
### FreeNAS (freenas-nfs, freenas-iscsi, freenas-smb, freenas-nvmeof, freenas-api-nfs, freenas-api-iscsi, freenas-api-smb, freenas-api-nvmeof)
The recommended version of FreeNAS is 12.0-U2+, however the driver should work
with much older versions as well.
@ -371,6 +378,8 @@ Ensure the following services are configurged and running:
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
- nvmeof
- ensure you have at least 1 listener/port configured (typcially TCP port 4420)
If you would prefer you can configure `democratic-csi` to use a
non-`root` user when connecting to the FreeNAS server:
@ -464,6 +473,7 @@ passwd smbroot (optional)
smbpasswd -L -a smbroot
####### nvmeof
# apt-get install linux-modules-extra-$(uname -r)
# ensure nvmeof target modules are loaded at startup
cat <<EOF > /etc/modules-load.d/nvmet.conf
nvmet
@ -484,7 +494,8 @@ cd nvmetcli
## install globally
python3 setup.py install --prefix=/usr
pip install configshell_fb
pip install configshell_fb # apt-get install -y pip python3-configshell-fb
## install to root home dir
python3 setup.py install --user

91
Vagrantfile vendored Normal file
View File

@ -0,0 +1,91 @@
Vagrant.configure("2") do |config|
# Check the host's architecture
host_arch = `uname -m`.strip
# Use a different box for ARM vs x86_64
if host_arch == "arm64"
# requires qemu, install qemu and then:
# vagrant plugin install vagrant-qemu
config.vm.box = "perk/ubuntu-24.04-arm64"
else
# Use the x86_64 compatible Ubuntu box
config.vm.box = "ubuntu/jammy64"
end
config.vm.provider "virtualbox" do |vb|
vb.memory = "2048"
vb.cpus = 2
end
config.vm.provision "shell", inline: <<-SHELL
# force version 20.x of nodejs
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get update -y
# for building dependecies and executing node
sudo apt-get install -y nodejs git make
# for app functionality
sudo apt-get install -y netbase socat e2fsprogs xfsprogs fatresize dosfstools nfs-common cifs-utils
# Install the following system packages
sudo apt-get install -y open-iscsi lsscsi sg3-utils multipath-tools scsitools nvme-cli
# Enable multipathing
sudo tee /etc/multipath.conf << EOF
defaults {
user_friendly_names yes
find_multipaths yes
}
EOF
sudo systemctl enable multipath-tools.service
# Enable and start iscsid service
sudo systemctl enable --now iscsid
# Verify installation
systemctl status iscsid --no-pager
####
# Install golang
####
GO_VERSION="1.24.1"
ARCH=$(uname -m)
GO_TAR_URL=""
if [[ "$ARCH" == "aarch64" ]]; then
GO_TAR_URL="https://go.dev/dl/go${GO_VERSION}.linux-arm64.tar.gz"
elif [[ "$ARCH" == "x86_64" ]]; then
GO_TAR_URL="https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz"
else
echo "Unsupported architecture: $ARCH"
exit 1
fi
echo "Downloading Go version $GO_VERSION for $ARCH..."
wget -q "$GO_TAR_URL" -O go.tar.gz
tar -C /usr/local -xzf go.tar.gz
rm go.tar.gz
echo "export PATH=\$PATH:/usr/local/go/bin" >> /etc/profile
source /etc/profile
####
# Install csi-test
####
echo "Installing csi-test"
git clone https://github.com/kubernetes-csi/csi-test /tmp/csi-test
pushd /tmp/csi-test/cmd/csi-sanity
make csi-sanity
sudo cp csi-sanity /usr/local/bin
popd
SHELL
# Sync project directory for seamless workflow
config.vm.synced_folder ".", "/home/vagrant/democratic-csi", type: "rsync",
rsync__exclude: ".git/"
# Allow SSH access with default key
config.ssh.insert_key = false
end

View File

@ -7,10 +7,17 @@
// polyfills
require("../src/utils/polyfills");
const yaml = require("js-yaml");
const cp = require("child_process");
const fs = require("fs");
const { grpc } = require("../src/utils/grpc");
const { stringify, stripWindowsDriveLetter } = require("../src/utils/general");
const {
stringify,
stripWindowsDriveLetter,
expandenv,
} = require("../src/utils/general");
const traverse = require("traverse");
const uuidv4 = require("uuid").v4;
const yaml = require("js-yaml");
let driverConfigFile;
let options;
@ -67,6 +74,8 @@ const args = require("yargs")
"1.7.0",
"1.8.0",
"1.9.0",
"1.10.0",
"1.11.0",
],
})
.demandOption(["csi-version"], "csi-version is required")
@ -106,6 +115,20 @@ if (!args.serverSocket && !args.serverAddress && !args.serverPort) {
process.exit(1);
}
//console.log(JSON.stringify(options, null, 2));
traverse(options).forEach(function (v) {
if (typeof v === "string" || v instanceof String) {
v = expandenv(v);
try {
v = JSON.parse(v);
} catch (e) {
// ignore
}
this.update(v);
}
});
//console.log(JSON.stringify(options, null, 2));
//process.exit(1);
//console.log(args);
//console.log(process.env);
@ -529,6 +552,28 @@ if (process.env.LOG_GRPC_SESSIONS == "1") {
if (require.main === module) {
(async function () {
try {
switch (process.platform) {
case "linux":
const nvme_dir = "/etc/nvme";
// ensure directory
if (!fs.existsSync(nvme_dir)) {
fs.mkdirSync(nvme_dir);
}
//uuidgen > /etc/nvme/hostid
if (!fs.existsSync(`${nvme_dir}/hostid`)) {
fs.writeFileSync(`${nvme_dir}/hostid`, uuidv4() + "\n");
}
//nvme gen-hostnqn > /etc/nvme/hostnqn
if (!fs.existsSync(`${nvme_dir}/hostnqn`)) {
const nqn = String(cp.execSync(`nvme gen-hostnqn`));
fs.writeFileSync(`${nvme_dir}/hostnqn`, nqn);
}
break;
}
if (bindAddress) {
await new Promise((resolve, reject) => {
csiServer.bindAsync(

View File

@ -20,7 +20,7 @@ if [[ -f "node_modules-linux-amd64.tar.gz" && ! -d "node_modules" ]];then
fi
# generate key for paths etc
export CI_BUILD_KEY=$(uuidgen | cut -d "-" -f 1)
export CI_BUILD_KEY=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8)
# launch the server
sudo -E ci/bin/launch-server.sh &

View File

@ -1,31 +0,0 @@
driver: freenas-api-iscsi
httpConnection:
protocol: http
host: ${TRUENAS_HOST}
port: 80
#apiKey:
username: ${TRUENAS_USERNAME}
password: ${TRUENAS_PASSWORD}
zfs:
datasetParentName: tank/ci/${CI_BUILD_KEY}/v
detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s
zvolCompression:
zvolDedup:
zvolEnableReservation: false
zvolBlocksize:
iscsi:
targetPortal: ${TRUENAS_HOST}
interface: ""
namePrefix: "csi-ci-${CI_BUILD_KEY}-"
nameSuffix: ""
targetGroups:
- targetGroupPortalGroup: 1
targetGroupInitiatorGroup: 1
targetGroupAuthType: None
targetGroupAuthGroup:
# 0-100 (0 == ignore)
extentAvailThreshold: 0

View File

@ -1,50 +0,0 @@
driver: freenas-api-smb
httpConnection:
protocol: http
host: ${TRUENAS_HOST}
port: 80
#apiKey:
username: ${TRUENAS_USERNAME}
password: ${TRUENAS_PASSWORD}
zfs:
datasetParentName: tank/ci/${CI_BUILD_KEY}/v
detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s
datasetEnableQuotas: true
datasetEnableReservation: false
datasetPermissionsMode: "0770"
datasetPermissionsUser: 1001
datasetPermissionsGroup: 1001
smb:
shareHost: ${TRUENAS_HOST}
#nameTemplate: ""
namePrefix: "csi-ci-${CI_BUILD_KEY}-"
nameSuffix: ""
shareAuxiliaryConfigurationTemplate: |
#guest ok = yes
#guest only = yes
shareHome: false
shareAllowedHosts: []
shareDeniedHosts: []
#shareDefaultPermissions: true
shareGuestOk: false
#shareGuestOnly: true
#shareShowHiddenFiles: true
shareRecycleBin: false
shareBrowsable: false
shareAccessBasedEnumeration: true
shareTimeMachine: false
#shareStorageTask:
node:
mount:
mount_flags: "username=smbroot,password=smbroot"
_private:
csi:
volume:
idHash:
strategy: crc16

View File

@ -1,29 +0,0 @@
driver: freenas-api-nfs
httpConnection:
protocol: http
host: ${TRUENAS_HOST}
port: 80
#apiKey:
username: ${TRUENAS_USERNAME}
password: ${TRUENAS_PASSWORD}
zfs:
datasetParentName: tank/ci/${CI_BUILD_KEY}/v
detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s
datasetEnableQuotas: true
datasetEnableReservation: false
datasetPermissionsMode: "0777"
datasetPermissionsUser: 0
datasetPermissionsGroup: 0
nfs:
shareHost: ${TRUENAS_HOST}
shareAlldirs: false
shareAllowedHosts: []
shareAllowedNetworks: []
shareMaprootUser: root
shareMaprootGroup: root
shareMapallUser: ""
shareMapallGroup: ""

View File

@ -1,29 +0,0 @@
driver: freenas-api-nfs
httpConnection:
protocol: http
host: ${TRUENAS_HOST}
port: 80
#apiKey:
username: ${TRUENAS_USERNAME}
password: ${TRUENAS_PASSWORD}
zfs:
datasetParentName: tank/ci/${CI_BUILD_KEY}/v
detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s
datasetEnableQuotas: true
datasetEnableReservation: false
datasetPermissionsMode: "0777"
datasetPermissionsUser: 0
datasetPermissionsGroup: 0
nfs:
shareHost: ${TRUENAS_HOST}
shareAlldirs: false
shareAllowedHosts: []
shareAllowedNetworks: []
shareMaprootUser: root
shareMaprootGroup: root
shareMapallUser: ""
shareMapallGroup: ""

View File

@ -1,50 +0,0 @@
driver: freenas-api-smb
httpConnection:
protocol: http
host: ${TRUENAS_HOST}
port: 80
#apiKey:
username: ${TRUENAS_USERNAME}
password: ${TRUENAS_PASSWORD}
zfs:
datasetParentName: tank/ci/${CI_BUILD_KEY}/v
detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s
datasetEnableQuotas: true
datasetEnableReservation: false
datasetPermissionsMode: "0770"
datasetPermissionsUser: 1001
datasetPermissionsGroup: 1001
smb:
shareHost: ${TRUENAS_HOST}
#nameTemplate: ""
namePrefix: "csi-ci-${CI_BUILD_KEY}-"
nameSuffix: ""
shareAuxiliaryConfigurationTemplate: |
#guest ok = yes
#guest only = yes
shareHome: false
shareAllowedHosts: []
shareDeniedHosts: []
#shareDefaultPermissions: true
shareGuestOk: false
#shareGuestOnly: true
#shareShowHiddenFiles: true
shareRecycleBin: false
shareBrowsable: false
shareAccessBasedEnumeration: true
shareTimeMachine: false
#shareStorageTask:
node:
mount:
mount_flags: "username=smbroot,password=smbroot"
_private:
csi:
volume:
idHash:
strategy: crc16

View File

@ -1,38 +0,0 @@
driver: freenas-api-iscsi
httpConnection:
protocol: http
host: ${TRUENAS_HOST}
port: 80
#apiKey:
username: ${TRUENAS_USERNAME}
password: ${TRUENAS_PASSWORD}
zfs:
datasetParentName: tank/ci/${CI_BUILD_KEY}/v
detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s
zvolCompression:
zvolDedup:
zvolEnableReservation: false
zvolBlocksize:
iscsi:
targetPortal: ${TRUENAS_HOST}
interface: ""
namePrefix: "csi-ci-${CI_BUILD_KEY}-"
nameSuffix: ""
targetGroups:
- targetGroupPortalGroup: 1
targetGroupInitiatorGroup: 1
targetGroupAuthType: None
targetGroupAuthGroup:
# 0-100 (0 == ignore)
extentAvailThreshold: 0
# https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203
_private:
csi:
volume:
idHash:
strategy: crc16

View File

@ -1,29 +0,0 @@
driver: freenas-api-nfs
httpConnection:
protocol: http
host: ${TRUENAS_HOST}
port: 80
#apiKey:
username: ${TRUENAS_USERNAME}
password: ${TRUENAS_PASSWORD}
zfs:
datasetParentName: tank/ci/${CI_BUILD_KEY}/v
detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s
datasetEnableQuotas: true
datasetEnableReservation: false
datasetPermissionsMode: "0777"
datasetPermissionsUser: 0
datasetPermissionsGroup: 0
nfs:
shareHost: ${TRUENAS_HOST}
shareAlldirs: false
shareAllowedHosts: []
shareAllowedNetworks: []
shareMaprootUser: root
shareMaprootGroup: root
shareMapallUser: ""
shareMapallGroup: ""

View File

@ -1,50 +0,0 @@
driver: freenas-api-smb
httpConnection:
protocol: http
host: ${TRUENAS_HOST}
port: 80
#apiKey:
username: ${TRUENAS_USERNAME}
password: ${TRUENAS_PASSWORD}
zfs:
datasetParentName: tank/ci/${CI_BUILD_KEY}/v
detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s
datasetEnableQuotas: true
datasetEnableReservation: false
datasetPermissionsMode: "0770"
datasetPermissionsUser: 1001
datasetPermissionsGroup: 1001
smb:
shareHost: ${TRUENAS_HOST}
#nameTemplate: ""
namePrefix: "csi-ci-${CI_BUILD_KEY}-"
nameSuffix: ""
shareAuxiliaryConfigurationTemplate: |
#guest ok = yes
#guest only = yes
shareHome: false
shareAllowedHosts: []
shareDeniedHosts: []
#shareDefaultPermissions: true
shareGuestOk: false
#shareGuestOnly: true
#shareShowHiddenFiles: true
shareRecycleBin: false
shareBrowsable: false
shareAccessBasedEnumeration: true
shareTimeMachine: false
#shareStorageTask:
node:
mount:
mount_flags: "username=smbroot,password=smbroot"
_private:
csi:
volume:
idHash:
strategy: crc16

View File

@ -4,7 +4,7 @@ httpConnection:
protocol: http
host: ${TRUENAS_HOST}
port: 80
#apiKey:
#apiKey:
username: ${TRUENAS_USERNAME}
password: ${TRUENAS_PASSWORD}
@ -24,7 +24,7 @@ iscsi:
nameSuffix: ""
targetGroups:
- targetGroupPortalGroup: 1
targetGroupInitiatorGroup: 1
targetGroupInitiatorGroup: 3
targetGroupAuthType: None
targetGroupAuthGroup:
# 0-100 (0 == ignore)

View File

@ -1,10 +1,10 @@
driver: freenas-api-iscsi
driver: freenas-api-nvmeof
httpConnection:
protocol: http
host: ${TRUENAS_HOST}
port: 80
#apiKey:
#apiKey:
username: ${TRUENAS_USERNAME}
password: ${TRUENAS_PASSWORD}
@ -17,18 +17,12 @@ zfs:
zvolEnableReservation: false
zvolBlocksize:
iscsi:
targetPortal: ${TRUENAS_HOST}
interface: ""
nvmeof:
transports:
- tcp://${TRUENAS_HOST}:4420
namePrefix: "csi-ci-${CI_BUILD_KEY}-"
nameSuffix: ""
targetGroups:
- targetGroupPortalGroup: 1
targetGroupInitiatorGroup: 1
targetGroupAuthType: None
targetGroupAuthGroup:
# 0-100 (0 == ignore)
extentAvailThreshold: 0
ports:
- 1
# https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203
_private:

View File

@ -4,7 +4,7 @@ httpConnection:
protocol: http
host: ${TRUENAS_HOST}
port: 80
#apiKey:
#apiKey:
username: ${TRUENAS_USERNAME}
password: ${TRUENAS_PASSWORD}
@ -14,10 +14,10 @@ zfs:
datasetEnableQuotas: true
datasetEnableReservation: false
datasetPermissionsMode: "0770"
datasetPermissionsUser: 1001
datasetPermissionsGroup: 1001
#datasetPermissionsMode: "0770"
#datasetPermissionsUser: 1001
#datasetPermissionsGroup: 1001
smb:
shareHost: ${TRUENAS_HOST}
#nameTemplate: ""

View File

@ -0,0 +1,28 @@
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: "pcs"
shareStrategyPcs:
pcs_group: "group-nas"
basename: "iqn.2003-01.org.linux-iscsi.ubuntu-19.x8664"
auth:
enabled: 0

2103
csi_proto/csi-v1.10.0.proto Normal file

File diff suppressed because it is too large Load Diff

2078
csi_proto/csi-v1.11.0.proto Normal file

File diff suppressed because it is too large Load Diff

42
docker/ctr-installer.sh Executable file
View File

@ -0,0 +1,42 @@
#!/bin/bash
set -e
set -x
PLATFORM_TYPE=${1}
if [[ "${PLATFORM_TYPE}" == "build" ]]; then
PLATFORM=$BUILDPLATFORM
else
PLATFORM=$TARGETPLATFORM
fi
if [[ "x${PLATFORM}" == "x" ]]; then
PLATFORM="linux/amd64"
fi
# these come from the --platform option of buildx, indirectly from DOCKER_BUILD_PLATFORM in main.yaml
# linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le
if [ "$PLATFORM" = "linux/amd64" ]; then
export PLATFORM_ARCH="amd64"
elif [ "$PLATFORM" = "linux/arm64" ]; then
export PLATFORM_ARCH="arm64"
elif [ "$PLATFORM" = "linux/arm/v7" ]; then
export PLATFORM_ARCH="arm"
elif [ "$PLATFORM" = "linux/s390x" ]; then
export PLATFORM_ARCH="s390x"
elif [ "$PLATFORM" = "linux/ppc64le" ]; then
export PLATFORM_ARCH="ppc64le"
else
echo "unsupported/unknown ctr PLATFORM ${PLATFORM}"
exit 0
fi
echo "I am installing ctr $CTR_VERSION"
export CTR_FILE="ctr-${CTR_VERSION}-linux-${PLATFORM_ARCH}"
wget -O "${CTR_FILE}" "https://github.com/democratic-csi/democratic-csi/releases/download/v1.0.0/${CTR_FILE}"
mv ${CTR_FILE} /usr/local/bin/ctr
chown root:root /usr/local/bin/ctr
chmod +x /usr/local/bin/ctr

View File

@ -0,0 +1,31 @@
diff --git a/cmd/ctr/commands/images/mount.go b/cmd/ctr/commands/images/mount.go
index c97954267..63c5a7746 100644
--- a/cmd/ctr/commands/images/mount.go
+++ b/cmd/ctr/commands/images/mount.go
@@ -25,6 +25,7 @@ import (
"github.com/containerd/containerd/v2/cmd/ctr/commands"
"github.com/containerd/containerd/v2/core/leases"
"github.com/containerd/containerd/v2/core/mount"
+ "github.com/containerd/containerd/v2/core/snapshots"
"github.com/containerd/containerd/v2/defaults"
"github.com/containerd/errdefs"
"github.com/containerd/platforms"
@@ -114,11 +115,16 @@ When you are done, use the unmount command.
s := client.SnapshotService(snapshotter)
+ labels := commands.LabelArgs(cliContext.StringSlice("label"))
+ opts := []snapshots.Opt{
+ snapshots.WithLabels(labels),
+ }
+
var mounts []mount.Mount
if cliContext.Bool("rw") {
- mounts, err = s.Prepare(ctx, target, chainID)
+ mounts, err = s.Prepare(ctx, target, chainID, opts...)
} else {
- mounts, err = s.View(ctx, target, chainID)
+ mounts, err = s.View(ctx, target, chainID, opts...)
}
if err != nil {
if errdefs.IsAlreadyExists(err) {

6
docker/entrypoint.ps1 Normal file
View File

@ -0,0 +1,6 @@
write-host "starting democratic-csi via entrypoint.ps1"
$env:Path = "${pwd}\bin;${env:Path}"
.\bin\node.exe --expose-gc .\bin\democratic-csi @args
Exit $LASTEXITCODE

View File

@ -1,14 +1,30 @@
#!/bin/bash
#!/usr/bin/env bash
: "${ISCSIADM_HOST_STRATEGY:=chroot}"
: "${ISCSIADM_HOST_PATH:=iscsiadm}"
echoerr() { printf "%s\n" "$*" >&2; }
P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin"
case ${ISCSIADM_HOST_STRATEGY} in
chroot)
# https://engineering.docker.com/2019/07/road-to-containing-iscsi/
chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" ${ISCSIADM_HOST_PATH} "${@:1}"
# https://www.docker.com/blog/road-to-containing-iscsi/
if [[ "${ISCSIADM_HOST_PATH}" =~ ^\/ && -f "/host${ISCSIADM_HOST_PATH}" ]]; then
chroot /host "${ISCSIADM_HOST_PATH}" "${@:1}"
exit $?
fi
for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do
if [[ -f "/host${p}/iscsiadm" ]]; then
chroot /host "${p}/iscsiadm" "${@:1}"
exit $?
fi
done
chroot /host /usr/bin/env -i PATH="${P}" ${ISCSIADM_HOST_PATH} "${@:1}"
exit $?
;;
nsenter)
@ -19,6 +35,7 @@ case ${ISCSIADM_HOST_STRATEGY} in
exit 1
fi
nsenter --mount="/proc/${iscsid_pid}/ns/mnt" --net="/proc/${iscsid_pid}/ns/net" -- ${ISCSIADM_HOST_PATH} "${@:1}"
exit $?
;;
*)

View File

@ -1,4 +1,7 @@
#!/bin/bash
#!/usr/bin/env bash
P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin"
PL="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin"
container_supported_filesystems=(
"ext2"
@ -31,7 +34,15 @@ while getopts "t:" opt; do
done
if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then
chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" mount "${@:1}"
for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do
if [[ -f "/host${p}/mount" ]]; then
chroot /host "${p}/mount" "${@:1}"
exit $?
fi
done
chroot /host /usr/bin/env -i PATH="${P}" mount "${@:1}"
exit $?
else
/usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" mount "${@:1}"
/usr/bin/env -i PATH="${PL}" mount "${@:1}"
exit $?
fi

View File

@ -1,3 +1,13 @@
#!/bin/bash
#!/usr/bin/env bash
chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" multipath "${@:1}"
P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin"
for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do
if [[ -f "/host${p}/multipath" ]]; then
chroot /host "${p}/multipath" "${@:1}"
exit $?
fi
done
chroot /host /usr/bin/env -i PATH="${P}" multipath "${@:1}"
echo $?

View File

@ -1,3 +1,13 @@
#!/bin/bash
#!/usr/bin/env bash
chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" oneclient "${@:1}"
P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin"
for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do
if [[ -f "/host${p}/oneclient" ]]; then
chroot /host "${p}/oneclient" "${@:1}"
exit $?
fi
done
chroot /host /usr/bin/env -i PATH="${P}" oneclient "${@:1}"
exit $?

View File

@ -1,4 +1,7 @@
#!/bin/bash
#!/usr/bin/env bash
P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin"
PL="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin"
container_supported_filesystems=(
"ext2"
@ -31,7 +34,15 @@ while getopts "t:" opt; do
done
if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then
chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" umount "${@:1}"
for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do
if [[ -f "/host${p}/umount" ]]; then
chroot /host "${p}/umount" "${@:1}"
exit $?
fi
done
chroot /host /usr/bin/env -i PATH="${P}" umount "${@:1}"
exit $?
else
/usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" umount "${@:1}"
/usr/bin/env -i PATH="${PL}" umount "${@:1}"
exit $?
fi

38
docker/yq-installer.sh Executable file
View File

@ -0,0 +1,38 @@
#!/bin/bash
set -e
set -x
PLATFORM_TYPE=${1}
if [[ "${PLATFORM_TYPE}" == "build" ]]; then
PLATFORM=$BUILDPLATFORM
else
PLATFORM=$TARGETPLATFORM
fi
if [[ "x${PLATFORM}" == "x" ]]; then
PLATFORM="linux/amd64"
fi
# these come from the --platform option of buildx, indirectly from DOCKER_BUILD_PLATFORM in main.yaml
if [ "$PLATFORM" = "linux/amd64" ]; then
export PLATFORM_ARCH="amd64"
elif [ "$PLATFORM" = "linux/arm64" ]; then
export PLATFORM_ARCH="arm64"
elif [ "$PLATFORM" = "linux/arm/v7" ]; then
export PLATFORM_ARCH="arm"
elif [ "$PLATFORM" = "linux/s390x" ]; then
export PLATFORM_ARCH="s390x"
elif [ "$PLATFORM" = "linux/ppc64le" ]; then
export PLATFORM_ARCH="ppc64le"
else
echo "unsupported/unknown yq PLATFORM ${PLATFORM}"
exit 0
fi
echo "I am installing yq $YQ_VERSION"
wget https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_${PLATFORM_ARCH} -O /usr/local/bin/yq
chmod +x /usr/local/bin/yq

View File

@ -1,3 +1,13 @@
#!/bin/bash
#!/usr/bin/env bash
chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" zfs "${@:1}"
P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin"
for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do
if [[ -f "/host${p}/zfs" ]]; then
chroot /host "${p}/zfs" "${@:1}"
exit $?
fi
done
chroot /host /usr/bin/env -i PATH="${P}" zfs "${@:1}"
exit $?

View File

@ -1,3 +1,13 @@
#!/bin/bash
#!/usr/bin/env bash
chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" zpool "${@:1}"
P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin"
for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do
if [[ -f "/host${p}/zpool" ]]; then
chroot /host "${p}/zpool" "${@:1}"
exit $?
fi
done
chroot /host /usr/bin/env -i PATH="${P}" zpool "${@:1}"
exit $?

View File

@ -4,63 +4,67 @@ Some drivers support different settings for volumes. These can be configured via
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
- ...
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
# 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
# Only for thick provisioned volumes. Known values:
# 0: Buffered Writes
# 3: Direct Write
direct_io_pattern: 0
# Use this to disable authentication. To configure authentication see below
auth_type: 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:
(with the `StorageClass` taking precedence). The following `dev_attribs` are known to work:
- `emulate_tpws`: Hardware-assisted zeroing
- `emulate_caw`: Hardware-assisted locking
@ -71,6 +75,7 @@ of the global configuration and in the `lunTemplate` of the `StorageClass`. If b
- `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
@ -82,18 +87,18 @@ 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.
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.
@ -123,12 +128,17 @@ 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
lunTemplate: |
...
targetTemplate: |
# Client Credentials
user: client
password: MySecretPassword
# Mutual CHAP Credentials. If these are specified mutual CHAP will be enabled.
mutualUser: server
mutualPassword: MyOtherPassword
lunSnapshotTemplate: |
...
```
Note that CHAP authentication will only be enabled if the secret contains a username and password. If e.g. a password is

View File

@ -0,0 +1,37 @@
kind: Pod
apiVersion: v1
metadata:
name: some-oci-pod-windows
spec:
nodeSelector:
kubernetes.io/os: windows
containers:
- name: hello
image: mcr.microsoft.com/windows/servercore:ltsc2022
command:
- powershell.exe
- -command
- while ($true) { Start-Sleep -Seconds 1 }
resources:
requests:
memory: "128Mi"
cpu: "500m"
limits:
memory: "128Mi"
cpu: "500m"
volumeMounts:
- name: oci
mountPath: /mnt/oci
volumes:
- name: oci
csi:
driver: org.democratic-csi.containerd-oci-inline-ephemeral
volumeAttributes:
# "image.reference": "ubuntu:24.04"
"image.reference": "democraticcsi/csi-grpc-proxy"
# NOTE: windows is incapable of using linux-based platform images
# windows/amd64
# NOTE: linux seemingly can mount windows-based images however
# "image.platform": "linux/amd64"
# "image.pullPolicy": "Always",
"snapshot.label.containerd.io/snapshot/windows/rootfs.sizebytes": "107374182400"

View File

@ -0,0 +1,29 @@
kind: Pod
apiVersion: v1
metadata:
name: some-oci-pod
spec:
nodeName: node01
containers:
- name: hello
image: busybox:1.37
command: ["sh", "-c", 'echo "Hello, Kubernetes!" && sleep Infinity']
resources:
requests:
memory: "128Mi"
cpu: "500m"
limits:
memory: "128Mi"
cpu: "500m"
volumeMounts:
- name: oci
mountPath: /mnt/oci
volumes:
- name: oci
csi:
driver: org.democratic-csi.containerd-oci-inline-ephemeral
volumeAttributes:
"image.reference": "ubuntu:24.04"
# "image.platform": ""
# "image.pullPolicy": "Always",
# "snapshot.label.containerd.io/snapshot/windows/rootfs.sizebytes": "107374182400"

View File

@ -0,0 +1,8 @@
driver: containerd-oci-ephemeral-inline
containerd:
#address: /run/containerd/containerd.sock
#windowsAddress: \\\\.\\pipe\\containerd-containerd
# use k8s.io to use the k8s ns
#namespace: default
#creds encryption key

View File

@ -33,6 +33,8 @@ zfs:
# "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
# "org.freenas:test": "{{ parameters.foo }}"
# "org.freenas:test2": "some value"
# snapshotProperties:
# "org.freenas:key": "value"
# total volume name (zvol/<datasetParentName>/<pvc name>) length cannot exceed 63 chars
# https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab

View File

@ -33,6 +33,8 @@ zfs:
# "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
# "org.freenas:test": "{{ parameters.foo }}"
# "org.freenas:test2": "some value"
# snapshotProperties:
# "org.freenas:key": "value"
datasetParentName: tank/k8s/a/vols
# do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap

View File

@ -0,0 +1,94 @@
driver: freenas-api-nvmeof
instance_id:
httpConnection:
protocol: http
host: server address
port: 80
# use only 1 of apiKey or username/password
# if both are present, apiKey is preferred
# apiKey is only available starting in TrueNAS-12
#apiKey:
username: root
password:
allowInsecure: true
# use apiVersion 2 for TrueNAS-12 and up (will work on 11.x in some scenarios as well)
# leave unset for auto-detection
#apiVersion: 2
zfs:
# can be used to override defaults if necessary
# the example below is useful for TrueNAS 12
#cli:
# sudoEnabled: true
#
# leave paths unset for auto-detection
# 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:
# "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
# "org.freenas:test": "{{ parameters.foo }}"
# "org.freenas:test2": "some value"
# total volume name (zvol/<datasetParentName>/<pvc name>) length cannot exceed 63 chars
# https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab
# standard volume naming overhead is 46 chars
# datasetParentName should therefore be 17 chars or less when using TrueNAS 12 or below
datasetParentName: tank/k8s/b/vols
# do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap
# they may be siblings, but neither should be nested in the other
# do NOT comment this option out even if you don't plan to use snapshots, just leave it with dummy value
detachedSnapshotsDatasetParentName: tanks/k8s/b/snaps
# "" (inherit), lz4, gzip-9, etc
zvolCompression:
# "" (inherit), on, off, verify
zvolDedup:
zvolEnableReservation: false
# 512, 1K, 2K, 4K, 8K, 16K, 64K, 128K default is 16K
zvolBlocksize:
nvmeof:
# these are for the node/client aspect
transports:
- tcp://server:port
#- "tcp://127.0.0.1:4420?host-iface=eth0"
#- "tcp://[2001:123:456::1]:4420"
#- "rdma://127.0.0.1:4420"
#- "fc://[nn-0x203b00a098cbcac6:pn-0x203d00a098cbcac6]"
# MUST ensure uniqueness
# full iqn limit is 223 bytes, plan accordingly
# default is "{{ name }}"
#nameTemplate: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}-{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
namePrefix: csi-
nameSuffix: "-clustera"
# port IDs to associate to the newly created subsystem
# the ports should be created as a pre-req to using the driver, democratic-csi does NOT manage the ports
#
# http://<IP>/api/docs/current/api_methods_nvmet.port.create.html
#
# curl -v 'http://username:password@IP/api/v2.0/nvmet/port'
#
# curl -v 'http://username:password@IP/api/v2.0/nvmet/port' \
# --header "Content-Type: application/json" \
# --request POST \
# --data '{"addr_trtype": "TCP","addr_trsvcid": 4420,"addr_traddr": "<YOUR NAS IP HERE>","addr_adrfam": "IPV4"}'
ports:
- <your port ID here>
# http://<ip>/api/docs/current/api_methods_nvmet.subsys.create.html
subsystemTemplate:
pi_enable: true
qid_max:
ieee_oui:
ana:
# http://<ip>/api/docs/current/api_methods_nvmet.namespace.create.html
# currently none of the fields can be tweaked so leave empty for now
namespaceTemplate:

View File

@ -33,6 +33,8 @@ zfs:
# "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
# "org.freenas:test": "{{ parameters.foo }}"
# "org.freenas:test2": "some value"
# snapshotProperties:
# "org.freenas:key": "value"
# these are managed automatically via the volume creation process when flagged as an smb volume
#datasetProperties:

View File

@ -43,6 +43,8 @@ zfs:
# "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
# "org.freenas:test": "{{ parameters.foo }}"
# "org.freenas:test2": "some value"
# snapshotProperties:
# "org.freenas:key": "value"
# total volume name (zvol/<datasetParentName>/<pvc name>) length cannot exceed 63 chars
# https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab

View File

@ -43,6 +43,8 @@ zfs:
# "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
# "org.freenas:test": "{{ parameters.foo }}"
# "org.freenas:test2": "some value"
# snapshotProperties:
# "org.freenas:key": "value"
datasetParentName: tank/k8s/a/vols
# do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap

View File

@ -0,0 +1,104 @@
driver: freenas-nvmeof
instance_id:
httpConnection:
protocol: http
host: server address
port: 80
# use only 1 of apiKey or username/password
# if both are present, apiKey is preferred
# apiKey is only available starting in TrueNAS-12
#apiKey:
username: root
password:
allowInsecure: true
# use apiVersion 2 for TrueNAS-12 and up (will work on 11.x in some scenarios as well)
# leave unset for auto-detection
#apiVersion: 2
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
#
# leave paths unset for auto-detection
# 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:
# "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
# "org.freenas:test": "{{ parameters.foo }}"
# "org.freenas:test2": "some value"
# total volume name (zvol/<datasetParentName>/<pvc name>) length cannot exceed 63 chars
# https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab
# standard volume naming overhead is 46 chars
# datasetParentName should therefore be 17 chars or less when using TrueNAS 12 or below
datasetParentName: tank/k8s/b/vols
# do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap
# they may be siblings, but neither should be nested in the other
# do NOT comment this option out even if you don't plan to use snapshots, just leave it with dummy value
detachedSnapshotsDatasetParentName: tanks/k8s/b/snaps
# "" (inherit), lz4, gzip-9, etc
zvolCompression:
# "" (inherit), on, off, verify
zvolDedup:
zvolEnableReservation: false
# 512, 1K, 2K, 4K, 8K, 16K, 64K, 128K default is 16K
zvolBlocksize:
nvmeof:
# these are for the node/client aspect
transports:
- tcp://server:port
#- "tcp://127.0.0.1:4420?host-iface=eth0"
#- "tcp://[2001:123:456::1]:4420"
#- "rdma://127.0.0.1:4420"
#- "fc://[nn-0x203b00a098cbcac6:pn-0x203d00a098cbcac6]"
# MUST ensure uniqueness
# full iqn limit is 223 bytes, plan accordingly
# default is "{{ name }}"
#nameTemplate: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}-{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
namePrefix: csi-
nameSuffix: "-clustera"
# port IDs to associate to the newly created subsystem
# the ports should be created as a pre-req to using the driver, democratic-csi does NOT manage the ports
#
# http://<IP>/api/docs/current/api_methods_nvmet.port.create.html
#
# curl -v 'http://username:password@IP/api/v2.0/nvmet/port'
#
# curl -v 'http://username:password@IP/api/v2.0/nvmet/port' \
# --header "Content-Type: application/json" \
# --request POST \
# --data '{"addr_trtype": "TCP","addr_trsvcid": 4420,"addr_traddr": "<YOUR NAS IP HERE>","addr_adrfam": "IPV4"}'
ports:
- <your port ID here>
# http://<ip>/api/docs/current/api_methods_nvmet.subsys.create.html
subsystemTemplate:
pi_enable: true
qid_max:
ieee_oui:
ana:
# http://<ip>/api/docs/current/api_methods_nvmet.namespace.create.html
# currently none of the fields can be tweaked so leave empty for now
namespaceTemplate:

View File

@ -43,6 +43,8 @@ zfs:
# "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
# "org.freenas:test": "{{ parameters.foo }}"
# "org.freenas:test2": "some value"
# snapshotProperties:
# "org.freenas:key": "value"
datasetProperties:
aclmode: restricted

View File

@ -0,0 +1,30 @@
kind: Pod
apiVersion: v1
metadata:
name: some-vhd-pod-windows
spec:
nodeSelector:
kubernetes.io/os: windows
containers:
- name: hello
image: mcr.microsoft.com/windows/servercore:ltsc2022
command:
- powershell.exe
- -command
- while ($true) { Start-Sleep -Seconds 1 }
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "500m"
volumeMounts:
- name: vhd
mountPath: /mnt/vhd
volumes:
- name: vhd
csi:
driver: org.democratic-csi.vhd-ephemeral-inline
volumeAttributes:
vhd.parentPath: "C:\\some\\host\\path\\to\\SampleDisk.vhdx"

View File

@ -0,0 +1,3 @@
driver: vhd-ephemeral-inline
vhd:
nameTemplate: "csi-ephemeral-inline-{{ volume_id }}"

View File

@ -27,6 +27,8 @@ zfs:
# "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
# "org.freenas:test": "{{ parameters.foo }}"
# "org.freenas:test2": "some value"
# snapshotProperties:
# "org.freenas:key": "value"
datasetParentName: tank/k8s/test
# do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap
@ -44,6 +46,7 @@ zfs:
iscsi:
shareStrategy: "targetCli"
#shareStrategy: "pcs"
# https://kifarunix.com/how-to-install-and-configure-iscsi-storage-server-on-ubuntu-18-04/
# https://kifarunix.com/how-install-and-configure-iscsi-storage-server-on-centos-7/
@ -75,6 +78,17 @@ iscsi:
attributes:
# set to 1 to enable Thin Provisioning Unmap
emulate_tpu: 0
shareStrategyPcs:
#sudoEnabled: true
pcs_group: "group-nas"
basename: "iqn.2003-01.org.linux-iscsi.ubuntu-19.x8664"
auth:
enabled: 0
# CHAP incoming only, mutual not supported by the Pacemaker resource agent
incoming_username: "foo"
incoming_password: "bar"
targetPortal: "server[:port]"
# for multipath
targetPortals: [] # [ "server[:port]", "server[:port]", ... ]

View File

@ -27,6 +27,8 @@ zfs:
# "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
# "org.freenas:test": "{{ parameters.foo }}"
# "org.freenas:test2": "some value"
# snapshotProperties:
# "org.freenas:key": "value"
datasetParentName: tank/k8s/test
# do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap

View File

@ -27,6 +27,8 @@ zfs:
# "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}"
# "org.freenas:test": "{{ parameters.foo }}"
# "org.freenas:test2": "some value"
# snapshotProperties:
# "org.freenas:key": "value"
datasetParentName: tank/k8s/test
# do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap

View File

@ -28,6 +28,8 @@ zfs:
#aclinherit: passthrough
#acltype: nfsv4
casesensitivity: insensitive
# snapshotProperties:
# "org.freenas:key": "value"
datasetParentName: tank/k8s/test
# do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap

View File

@ -6,6 +6,8 @@ zfs:
datasetProperties:
# key: value
snapshotProperties:
# "org.freenas:key": "value"
datasetEnableQuotas: true
datasetEnableReservation: false

View File

@ -6,6 +6,8 @@ zfs:
datasetProperties:
# key: value
snapshotProperties:
# "org.freenas:key": "value"
zvolCompression:
zvolDedup:

18
hack/build_push.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
set -e
set -x
ROOT_DIR="$(dirname "$(realpath "$0")")"
GITHUB_USER=${GITHUB_USER:-$(jq -r '.auths."ghcr.io".auth' ~/.docker/config.json|base64 -d|cut -d':' -f1)}
GITHUB_REPO=${GITHUB_REPO:-$(basename -s .git $(git remote get-url origin))}
DOCKER_TAG=${DOCKER_TAG:-$(git branch --show-current)-$(git rev-parse --short HEAD)}
if [ -z "${GITHUB_USER}" ]; then
echo "Error: Need to login to ghcr.io ; execute docker login ghcr.io"
exit 1
fi
docker build $ROOT_DIR/.. --push -t ghcr.io/${GITHUB_USER}/${GITHUB_REPO}:${DOCKER_TAG}
echo "Image pushed to ghcr.io/${GITHUB_USER}/${GITHUB_REPO}:${DOCKER_TAG}"

34
hack/run.sh Executable file
View File

@ -0,0 +1,34 @@
#!/bin/bash
set -e
set -x
TEMPLATE_CONFIG=""
ROOT_DIR="$(dirname "$(realpath "$0")")"
while [[ "$#" -gt 0 ]]; do
case $1 in
-c|--config) TEMPLATE_CONFIG="$(realpath "$2")"; shift ;;
*) echo "Unknown parameter passed: $1"; exit 1 ;;
esac
shift
done
if [ -z "${TEMPLATE_CONFIG}" ]; then
echo "Error: --config or -c parameter is required."
exit 1
fi
if [ ! -f $ROOT_DIR/secrets.env ]; then
echo "Error: secrets.env file not found."
exit 1
fi
source $ROOT_DIR/secrets.env # needs to have exported variables
# generate key for paths etc
export CI_BUILD_KEY=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8)
export TEMPLATE_CONFIG_FILE=${TEMPLATE_CONFIG}
$ROOT_DIR/../ci/bin/run.sh

3250
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@
"url": "https://github.com/democratic-csi/democratic-csi.git"
},
"dependencies": {
"@codefresh-io/docker-reference": "^0.0.11",
"@grpc/grpc-js": "^1.8.4",
"@grpc/proto-loader": "^0.7.0",
"@kubernetes/client-node": "^0.18.0",
@ -31,11 +32,14 @@
"lodash": "^4.17.21",
"lru-cache": "^7.4.0",
"prompt": "^1.2.2",
"reconnecting-websocket": "^4.4.0",
"semver": "^7.3.4",
"ssh2": "^1.1.0",
"traverse": "^0.6.11",
"uri-js": "^4.4.1",
"uuid": "^9.0.0",
"winston": "^3.6.0",
"ws": "^8.18.0",
"yargs": "^17.0.1"
},
"devDependencies": {

View File

@ -4,6 +4,7 @@ const https = require("https");
const { axios_request, stringify } = require("../../../utils/general");
const Mutex = require("async-mutex").Mutex;
const { GrpcError, grpc } = require("../../../utils/grpc");
const { Registry } = require("../../../utils/registry");
const USER_AGENT = "democratic-csi";
const __REGISTRY_NS__ = "SynologyHttpClient";
@ -84,6 +85,7 @@ class SynologyHttpClient {
this.logger = console;
this.doLoginMutex = new Mutex();
this.apiSerializeMutex = new Mutex();
this.registry = new Registry();
if (false) {
setInterval(() => {
@ -94,7 +96,7 @@ class SynologyHttpClient {
}
getHttpAgent() {
return this.ctx.registry.get(`${__REGISTRY_NS__}:http_agent`, () => {
return this.registry.get(`${__REGISTRY_NS__}:http_agent`, () => {
return new http.Agent({
keepAlive: true,
maxSockets: Infinity,
@ -104,7 +106,7 @@ class SynologyHttpClient {
}
getHttpsAgent() {
return this.ctx.registry.get(`${__REGISTRY_NS__}:https_agent`, () => {
return this.registry.get(`${__REGISTRY_NS__}:https_agent`, () => {
return new https.Agent({
keepAlive: true,
maxSockets: Infinity,

View File

@ -127,7 +127,7 @@ class ControllerSynologyDriver extends CsiBaseDriver {
case "synology-iscsi":
return "volume";
default:
throw new Error("unknown driver: " + this.ctx.args.driver);
throw new Error("unknown driver: " + this.options.driver);
}
}
@ -140,7 +140,7 @@ class ControllerSynologyDriver extends CsiBaseDriver {
case "synology-iscsi":
return "iscsi";
default:
throw new Error("unknown driver: " + this.ctx.args.driver);
throw new Error("unknown driver: " + this.options.driver);
}
}
@ -163,7 +163,7 @@ class ControllerSynologyDriver extends CsiBaseDriver {
parseParameterYamlData(data, fieldHint = "") {
try {
return yaml.load(data);
} catch {
} catch (err) {
if (err instanceof yaml.YAMLException) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,

View File

@ -4,6 +4,7 @@ const { GrpcError, grpc } = require("../../utils/grpc");
const GeneralUtils = require("../../utils/general");
const LocalCliExecClient =
require("../../utils/zfs_local_exec_client").LocalCliClient;
const Mutex = require("async-mutex").Mutex;
const SshClient = require("../../utils/zfs_ssh_exec_client").SshClient;
const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs");
@ -13,6 +14,15 @@ const ISCSI_ASSETS_NAME_PROPERTY_NAME = "democratic-csi:iscsi_assets_name";
const NVMEOF_ASSETS_NAME_PROPERTY_NAME = "democratic-csi:nvmeof_assets_name";
const __REGISTRY_NS__ = "ControllerZfsGenericDriver";
class ControllerZfsGenericDriver extends ControllerZfsBaseDriver {
constructor(ctx, options) {
super(...arguments);
this.targetCliMutex = new Mutex();
this.nvmetCliMutex = new Mutex();
this.spdkCliMutex = new Mutex();
this.pcsMutex = new Mutex();
}
getExecClient() {
return this.ctx.registry.get(`${__REGISTRY_NS__}:exec_client`, () => {
if (this.options.sshConnection) {
@ -38,21 +48,18 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver {
options.executor = execClient;
}
options.idempotent = true;
if (
this.options.zfs.hasOwnProperty("cli") &&
this.options.zfs.cli &&
this.options.zfs.cli.hasOwnProperty("paths")
) {
options.paths = this.options.zfs.cli.paths;
}
options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false);
if (typeof this.setZetabyteCustomOptions === "function") {
await this.setZetabyteCustomOptions(options);
}
options.paths = options.paths || {};
options.paths = Object.assign(
{},
options.paths,
_.get(this.options, "zfs.cli.paths", {})
);
return new Zetabyte(options);
});
}
@ -70,7 +77,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver {
case "zfs-generic-nvmeof":
return "volume";
default:
throw new Error("unknown driver: " + this.ctx.args.driver);
throw new Error("unknown driver: " + this.options.driver);
}
}
@ -297,8 +304,66 @@ create /backstores/block/${assetName}
}
);
break;
case "pcs":
basename = this.options.iscsi.shareStrategyPcs.basename;
let pcs_group = this.options.iscsi.shareStrategyPcs.pcs_group;
let extraTerms = ['group', `${pcs_group}`, '--wait']; // The wait is important to avoid race conditions
let createTargetTerms = [
'resource', 'create', '--future', '--force', `target-${assetName}`, 'ocf:heartbeat:iSCSITarget',
'implementation="lio-t"', 'portals=":::3260"', `iqn="${basename}:${assetName}"`
];
if (this.options.iscsi.shareStrategyPcs.auth.enabled) {
createTargetTerms.push(`incoming_username="${this.options.iscsi.shareStrategyPcs.auth.incoming_username}"`);
createTargetTerms.push(`incoming_password="${this.options.iscsi.shareStrategyPcs.auth.incoming_password}"`);
}
await GeneralUtils.retry(
3,
2000,
async () => {
await this.pcsCommand(createTargetTerms.concat(extraTerms));
},
{
retryCondition: (err) => {
if (err.stdout && err.stdout.includes("Timed Out")) {
return true;
}
return false;
},
}
);
let createLunTerms = [
'resource', 'create', '--future', `lun-${assetName}`, 'ocf:heartbeat:iSCSILogicalUnit',
'implementation="lio-t"', `target_iqn="${basename}:${assetName}"`, 'lun="0"',
`path="/dev/${extentDiskName}"`
];
await GeneralUtils.retry(
3,
2000,
async () => {
await this.pcsCommand(createLunTerms.concat(extraTerms));
},
{
retryCondition: (err) => {
if (err.stdout && err.stdout.includes("Timed Out")) {
return true;
}
return false;
},
}
);
break;
default:
break;
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`invalid configuration: unknown shareStrategy ${this.options.iscsi.shareStrategy}`
);
}
// iqn = target
@ -689,8 +754,54 @@ delete ${assetName}
);
break;
default:
case "pcs":
let deleteLunText = [
'resource', 'delete', `lun-${assetName}`
];
await GeneralUtils.retry(
3,
2000,
async () => {
await this.pcsCommand(deleteLunText);
},
{
retryCondition: (err) => {
if (err.stdout && err.stdout.includes("Timed Out")) {
return true;
}
return false;
},
}
);
let deleteTargetText = [
'resource', 'delete', `target-${assetName}`
];
await GeneralUtils.retry(
3,
2000,
async () => {
await this.pcsCommand(deleteTargetText);
},
{
retryCondition: (err) => {
if (err.stdout && err.stdout.includes("Timed Out")) {
return true;
}
return false;
},
}
);
break;
default:
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`invalid configuration: unknown shareStrategy ${this.options.iscsi.shareStrategy}`
);
}
break;
}
@ -840,6 +951,9 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath}
case "targetCli":
// nothing required, just need to rescan on the node
break;
case "pcs":
// nothing required, just need to rescan on the node
break;
default:
break;
}
@ -850,6 +964,66 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath}
}
}
async pcsCommand(commandTerms) {
const execClient = this.getExecClient();
const driver = this;
let command = "sh";
let args = ["-c"];
let cliArgs = ["pcs"];
if (
_.get(this.options, "iscsi.shareStrategyPcs.sudoEnabled", false)
) {
cliArgs.unshift("sudo");
}
let cliCommand = [];
cliCommand.push(cliArgs.join(" "));
cliCommand.push(commandTerms.join(" "));
args.push("'" + cliCommand.join(" ") + "'");
let logCommandTmp = command + " " + args.join(" ");
let logCommand = "";
logCommandTmp.split(" ").forEach((term) => {
logCommand += " ";
if (term.startsWith("incoming_password=")) {
logCommand += "incoming_password=<redacted>";
} else {
logCommand += term;
}
});
driver.ctx.logger.verbose("pcs command:" + logCommand);
let options = {
pty: true,
};
return driver.pcsMutex.runExclusive(async () => {
let response = await execClient.exec(
execClient.buildCommand(command, args),
options
);
driver.ctx.logger.verbose(
"pcs response: " + JSON.stringify(response)
);
// Handle idempotence for create commands
if (response.code == 1 && response.stdout.includes("already exists")) {
driver.ctx.logger.verbose("pcs resource already exists, ignoring error (setting response.code=0)");
response.code = 0;
}
if (response.code != 0) {
throw response;
}
return response;
});
}
async targetCliCommand(data) {
const execClient = this.getExecClient();
const driver = this;
@ -897,17 +1071,20 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath}
let options = {
pty: true,
};
let response = await execClient.exec(
execClient.buildCommand(command, args),
options
);
driver.ctx.logger.verbose(
"TargetCLI response: " + JSON.stringify(response)
);
if (response.code != 0) {
throw response;
}
return response;
return driver.targetCliMutex.runExclusive(async () => {
let response = await execClient.exec(
execClient.buildCommand(command, args),
options
);
driver.ctx.logger.verbose(
"TargetCLI response: " + JSON.stringify(response)
);
if (response.code != 0) {
throw response;
}
return response;
});
}
async nvmetCliCommand(data) {
@ -985,15 +1162,20 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath}
let options = {
pty: true,
};
let response = await execClient.exec(
execClient.buildCommand(command, args),
options
);
driver.ctx.logger.verbose("nvmetCLI response: " + JSON.stringify(response));
if (response.code != 0) {
throw response;
}
return response;
return driver.nvmetCliMutex.runExclusive(async () => {
let response = await execClient.exec(
execClient.buildCommand(command, args),
options
);
driver.ctx.logger.verbose(
"nvmetCLI response: " + JSON.stringify(response)
);
if (response.code != 0) {
throw response;
}
return response;
});
}
async spdkCliCommand(data) {
@ -1044,15 +1226,20 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath}
let options = {
pty: true,
};
let response = await execClient.exec(
execClient.buildCommand(command, args),
options
);
driver.ctx.logger.verbose("spdkCLI response: " + JSON.stringify(response));
if (response.code != 0) {
throw response;
}
return response;
return driver.spdkCliMutex.runExclusive(async () => {
let response = await execClient.exec(
execClient.buildCommand(command, args),
options
);
driver.ctx.logger.verbose(
"spdkCLI response: " + JSON.stringify(response)
);
if (response.code != 0) {
throw response;
}
return response;
});
}
}

View File

@ -86,7 +86,7 @@ class ControllerZfsLocalDriver extends ControllerZfsBaseDriver {
case "zfs-local-zvol":
return "volume";
default:
throw new Error("unknown driver: " + this.ctx.args.driver);
throw new Error("unknown driver: " + this.options.driver);
}
}

View File

@ -3,10 +3,12 @@ const { CsiBaseDriver } = require("../index");
const { GrpcError, grpc } = require("../../utils/grpc");
const GeneralUtils = require("../../utils/general");
const getLargestNumber = require("../../utils/general").getLargestNumber;
const Mount = require("../../utils/mount").Mount;
const Handlebars = require("handlebars");
const uuidv4 = require("uuid").v4;
const semver = require("semver");
const yaml = require("js-yaml");
// zfs common properties
const MANAGED_PROPERTY_NAME = "democratic-csi:managed_resource";
@ -32,7 +34,7 @@ const VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME =
const MAX_ZVOL_NAME_LENGTH_CACHE_KEY = "controller-zfs:max_zvol_name_length";
/**
* Base driver to provisin zfs assets using zfs cli commands.
* Base driver to provision zfs assets using zfs cli commands.
* Derived drivers only need to implement:
* - getExecClient()
* - async getZetabyte()
@ -640,9 +642,67 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
const execClient = this.getExecClient();
const zb = await this.getZetabyte();
const normalizedParameters = driver.getNormalizedParameters(
call.request.parameters,
driver.options.driver,
driver.options.instance_id
);
let parametersOptions = {};
if (normalizedParameters["config"]) {
try {
parametersOptions = yaml.load(normalizedParameters["config"]);
} catch (err) {
if (err instanceof yaml.YAMLException) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`parameter 'config' not a valid YAML/JSON document.`.trim()
);
} else {
throw err;
}
}
}
let pvcOptions = {};
if (
normalizedParameters["load-config-from-pvc"] == "true" &&
call.request.parameters["csi.storage.k8s.io/pvc/name"] &&
call.request.parameters["csi.storage.k8s.io/pvc/namespace"]
) {
let pvc = await driver.getPersistentVolumeClaim(
call.request.parameters["csi.storage.k8s.io/pvc/name"],
call.request.parameters["csi.storage.k8s.io/pvc/namespace"]
);
if (
_.has(pvc, ["metadata", "annotations", "democratic-csi.org/config"])
) {
try {
pvcOptions = yaml.load(
_.get(pvc, ["metadata", "annotations", "democratic-csi.org/config"])
);
} catch (err) {
if (err instanceof yaml.YAMLException) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`pvc 'democratic-csi.org/config' annotation not a valid YAML/JSON document.`.trim()
);
} else {
throw err;
}
}
}
}
const driverOptions = driver.getMergedDriverOptions([
parametersOptions,
pvcOptions,
]);
let datasetParentName = this.getVolumeParentDatasetName();
let snapshotParentDatasetName = this.getDetachedSnapshotParentDatasetName();
let zvolBlocksize = this.options.zfs.zvolBlocksize || "16K";
let zvolBlocksize = driverOptions.zfs.zvolBlocksize || "16K";
let name = call.request.name;
let volume_id = await driver.getVolumeIdFromCall(call);
let volume_content_source = call.request.volume_content_source;
@ -755,7 +815,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
if (
driverZfsResourceType == "filesystem" &&
this.options.zfs.datasetEnableQuotas
driverOptions.zfs.datasetEnableQuotas
) {
check = true;
}
@ -811,9 +871,9 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
// user-supplied properties
// put early to prevent stupid (user-supplied values overwriting system values)
if (driver.options.zfs.datasetProperties) {
for (let property in driver.options.zfs.datasetProperties) {
let value = driver.options.zfs.datasetProperties[property];
if (driverOptions.zfs.datasetProperties) {
for (let property in driverOptions.zfs.datasetProperties) {
let value = driverOptions.zfs.datasetProperties[property];
const template = Handlebars.compile(value);
volumeProperties[property] = template({
@ -822,13 +882,15 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
}
}
// TODO: add call.request.parameters properties here
volumeProperties[VOLUME_CSI_NAME_PROPERTY_NAME] = name;
volumeProperties[MANAGED_PROPERTY_NAME] = "true";
volumeProperties[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME] =
driver.options.driver;
if (driver.options.instance_id) {
driverOptions.driver;
if (driverOptions.instance_id) {
volumeProperties[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME] =
driver.options.instance_id;
driverOptions.instance_id;
}
// TODO: also set access_mode as property?
@ -837,7 +899,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
// zvol enables reservation by default
// this implements 'sparse' zvols
if (driverZfsResourceType == "volume") {
if (!this.options.zfs.zvolEnableReservation) {
if (!driverOptions.zfs.zvolEnableReservation) {
volumeProperties.refreservation = 0;
}
}
@ -1097,13 +1159,13 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
switch (driverZfsResourceType) {
case "filesystem":
// set quota
if (this.options.zfs.datasetEnableQuotas) {
if (driverOptions.zfs.datasetEnableQuotas) {
setProps = true;
properties.refquota = capacity_bytes;
}
// set reserve
if (this.options.zfs.datasetEnableReservation) {
if (driverOptions.zfs.datasetEnableReservation) {
setProps = true;
properties.refreservation = capacity_bytes;
}
@ -1132,42 +1194,50 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
properties = properties[datasetName];
driver.ctx.logger.debug("zfs props data: %j", properties);
// get mountpoint
let mountpoint = properties.mountpoint.value;
if (mountpoint == "legacy") {
let mount = new Mount();
let mounts = await mount.getDeviceMounts(datasetName);
if (mounts.filesystems[0]) {
mountpoint = mounts.filesystems[0].target;
}
}
// set mode
if (this.options.zfs.datasetPermissionsMode) {
if (driverOptions.zfs.datasetPermissionsMode) {
await driver.setFilesystemMode(
properties.mountpoint.value,
this.options.zfs.datasetPermissionsMode
mountpoint,
driverOptions.zfs.datasetPermissionsMode
);
}
// set ownership
if (
String(_.get(this.options, "zfs.datasetPermissionsUser", "")).length >
0 ||
String(_.get(this.options, "zfs.datasetPermissionsGroup", ""))
String(_.get(driverOptions, "zfs.datasetPermissionsUser", ""))
.length > 0 ||
String(_.get(driverOptions, "zfs.datasetPermissionsGroup", ""))
.length > 0
) {
await driver.setFilesystemOwnership(
properties.mountpoint.value,
this.options.zfs.datasetPermissionsUser,
this.options.zfs.datasetPermissionsGroup
mountpoint,
driverOptions.zfs.datasetPermissionsUser,
driverOptions.zfs.datasetPermissionsGroup
);
}
// set acls
// TODO: this is unsfafe approach, make it better
// probably could see if ^-.*\s and split and then shell escape
if (this.options.zfs.datasetPermissionsAcls) {
if (driverOptions.zfs.datasetPermissionsAcls) {
let aclBinary = _.get(
driver.options,
driverOptions,
"zfs.datasetPermissionsAclsBinary",
"setfacl"
);
for (const acl of this.options.zfs.datasetPermissionsAcls) {
command = execClient.buildCommand(aclBinary, [
acl,
properties.mountpoint.value,
]);
for (const acl of driverOptions.zfs.datasetPermissionsAcls) {
command = execClient.buildCommand(aclBinary, [acl, mountpoint]);
if ((await this.getWhoAmI()) != "root") {
command = (await this.getSudoPath()) + " " + command;
}
@ -1198,21 +1268,21 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
// restore default must use the below
// zfs inherit [-rS] property filesystem|volume|snapshot…
if (
(typeof this.options.zfs.zvolDedup === "string" ||
this.options.zfs.zvolDedup instanceof String) &&
this.options.zfs.zvolDedup.length > 0
(typeof driverOptions.zfs.zvolDedup === "string" ||
driverOptions.zfs.zvolDedup instanceof String) &&
driverOptions.zfs.zvolDedup.length > 0
) {
properties.dedup = this.options.zfs.zvolDedup;
properties.dedup = driverOptions.zfs.zvolDedup;
}
// compression
// lz4, gzip-9, etc
if (
(typeof this.options.zfs.zvolCompression === "string" ||
this.options.zfs.zvolCompression instanceof String) &&
this.options.zfs.zvolCompression > 0
(typeof driverOptions.zfs.zvolCompression === "string" ||
driverOptions.zfs.zvolCompression instanceof String) &&
driverOptions.zfs.zvolCompression > 0
) {
properties.compression = this.options.zfs.zvolCompression;
properties.compression = driverOptions.zfs.zvolCompression;
}
if (setProps) {
@ -1227,10 +1297,10 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
[SHARE_VOLUME_CONTEXT_PROPERTY_NAME]: JSON.stringify(volume_context),
});
volume_context["provisioner_driver"] = driver.options.driver;
if (driver.options.instance_id) {
volume_context["provisioner_driver"] = driverOptions.driver;
if (driverOptions.instance_id) {
volume_context["provisioner_driver_instance_id"] =
driver.options.instance_id;
driverOptions.instance_id;
}
// set this just before sending out response so we know if volume completed
@ -1247,7 +1317,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
volume_id,
//capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0
capacity_bytes:
this.options.zfs.datasetEnableQuotas ||
driverOptions.zfs.datasetEnableQuotas ||
driverZfsResourceType == "volume"
? capacity_bytes
: 0,
@ -1272,6 +1342,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
async DeleteVolume(call) {
const driver = this;
const zb = await this.getZetabyte();
const driverOptions = driver.getMergedDriverOptions([]);
let datasetParentName = this.getVolumeParentDatasetName();
let name = call.request.volume_id;
@ -1318,7 +1389,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
// deleteStrategy
const delete_strategy = _.get(
driver.options,
driverOptions,
"_private.csi.volume.deleteStrategy",
""
);
@ -1361,6 +1432,42 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
}
}
// Explicitly check if we have any managed snapshots
// If a clone has been created from a snapshot, it will fail anyway but if no clones
// have been created the destroy will succeed undesirably
let hasManagedSnapshot = false;
try {
let snapshots = await zb.zfs.list(
datasetName,
[
"name",
// "democratic-csi:csi_snapshot_name",
// "democratic-csi:csi_snapshot_source_volume_id",
MANAGED_PROPERTY_NAME,
],
{ types: ["snapshot"] }
);
hasManagedSnapshot = snapshots.indexed.some((snapshot) => {
return snapshot[MANAGED_PROPERTY_NAME].toLowerCase() == "true";
});
} catch (err) {
// ignore errors when the dataset is already deleted
if (!err.toString().includes("dataset does not exist")) {
throw new GrpcError(
grpc.status.UNKNOWN,
`failed to test for snapshots: ${err.toString()}`
);
}
}
if (hasManagedSnapshot) {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
"filesystem has dependent snapshots"
);
}
// NOTE: -f does NOT allow deletes if dependent filesets exist
// NOTE: -R will recursively delete items + dependent filesets
// delete dataset
@ -1405,6 +1512,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
const driver = this;
const driverZfsResourceType = this.getDriverZfsResourceType();
const zb = await this.getZetabyte();
const driverOptions = driver.getMergedDriverOptions([]);
let datasetParentName = this.getVolumeParentDatasetName();
let name = call.request.volume_id;
@ -1477,13 +1585,13 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
switch (driverZfsResourceType) {
case "filesystem":
// set quota
if (this.options.zfs.datasetEnableQuotas) {
if (driverOptions.zfs.datasetEnableQuotas) {
setProps = true;
properties.refquota = capacity_bytes;
}
// set reserve
if (this.options.zfs.datasetEnableReservation) {
if (driverOptions.zfs.datasetEnableReservation) {
setProps = true;
properties.refreservation = capacity_bytes;
}
@ -1493,7 +1601,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
setProps = true;
// managed automatically for zvols
//if (this.options.zfs.zvolEnableReservation) {
//if (driverOptions.zfs.zvolEnableReservation) {
// properties.refreservation = capacity_bytes;
//}
break;
@ -1507,7 +1615,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
return {
capacity_bytes:
this.options.zfs.datasetEnableQuotas ||
driverOptions.zfs.datasetEnableQuotas ||
driverZfsResourceType == "volume"
? capacity_bytes
: 0,
@ -1523,6 +1631,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
async GetCapacity(call) {
const driver = this;
const zb = await this.getZetabyte();
const driverOptions = driver.getMergedDriverOptions([]);
let datasetParentName = this.getVolumeParentDatasetName();
@ -1573,6 +1682,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
const driver = this;
const driverZfsResourceType = this.getDriverZfsResourceType();
const zb = await this.getZetabyte();
const driverOptions = driver.getMergedDriverOptions([]);
let datasetParentName = this.getVolumeParentDatasetName();
let response;
@ -1654,6 +1764,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
const driver = this;
const driverZfsResourceType = this.getDriverZfsResourceType();
const zb = await this.getZetabyte();
const driverOptions = driver.getMergedDriverOptions([]);
let datasetParentName = this.getVolumeParentDatasetName();
let entries = [];
@ -1794,6 +1905,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
const driver = this;
const driverZfsResourceType = this.getDriverZfsResourceType();
const zb = await this.getZetabyte();
const driverOptions = driver.getMergedDriverOptions([]);
let entries = [];
let entries_length = 0;
@ -2048,6 +2160,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
const driver = this;
const driverZfsResourceType = this.getDriverZfsResourceType();
const zb = await this.getZetabyte();
const driverOptions = driver.getMergedDriverOptions([]);
let size_bytes = 0;
let detachedSnapshot = false;
@ -2106,6 +2219,19 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
);
}
// user-supplied properties
// put early to prevent stupid (user-supplied values overwriting system values)
if (driverOptions.zfs.snapshotProperties) {
for (let property in driverOptions.zfs.snapshotProperties) {
let value = driverOptions.zfs.snapshotProperties[property];
const template = Handlebars.compile(value);
snapshotProperties[property] = template({
parameters: call.request.parameters,
});
}
}
const volumeDatasetName = volumeParentDatasetName + "/" + source_volume_id;
const datasetName = datasetParentName + "/" + source_volume_id;
snapshotProperties[SNAPSHOT_CSI_NAME_PROPERTY_NAME] = name;
@ -2355,6 +2481,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
async DeleteSnapshot(call) {
const driver = this;
const zb = await this.getZetabyte();
const driverOptions = driver.getMergedDriverOptions([]);
const snapshot_id = call.request.snapshot_id;
@ -2426,6 +2553,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
async ValidateVolumeCapabilities(call) {
const driver = this;
const zb = await this.getZetabyte();
const driverOptions = driver.getMergedDriverOptions([]);
const volume_id = call.request.volume_id;
if (!volume_id) {

View File

@ -0,0 +1,497 @@
const _ = require("lodash");
const fs = require("fs");
const CTR = require("../../utils/ctr").CTR;
const { CsiBaseDriver } = require("../index");
const { GrpcError, grpc } = require("../../utils/grpc");
const { Filesystem } = require("../../utils/filesystem");
const { Mount } = require("../../utils/mount");
const semver = require("semver");
const { parseAll } = require("@codefresh-io/docker-reference");
const __REGISTRY_NS__ = "EphemeralInlineContainerDOciDriver";
/**
* https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20190122-csi-inline-volumes.md
* https://kubernetes-csi.github.io/docs/ephemeral-local-volumes.html
*
* Sample calls:
* - https://gcsweb.k8s.io/gcs/kubernetes-jenkins/pr-logs/pull/92387/pull-kubernetes-e2e-gce/1280784994997899264/artifacts/_sig-storage_CSI_Volumes/_Driver_csi-hostpath_/_Testpattern_inline_ephemeral_CSI_volume_ephemeral/should_create_read_write_inline_ephemeral_volume/
* - https://storage.googleapis.com/kubernetes-jenkins/pr-logs/pull/92387/pull-kubernetes-e2e-gce/1280784994997899264/artifacts/_sig-storage_CSI_Volumes/_Driver_csi-hostpath_/_Testpattern_inline_ephemeral_CSI_volume_ephemeral/should_create_read-only_inline_ephemeral_volume/csi-hostpathplugin-0-hostpath.log
*
* inline drivers are assumed to be mount only (no block support)
* purposely there is no native support for size contraints
*
*/
class EphemeralInlineContainerDOciDriver extends CsiBaseDriver {
constructor(ctx, options) {
super(...arguments);
options = options || {};
options.service = options.service || {};
options.service.identity = options.service.identity || {};
options.service.controller = options.service.controller || {};
options.service.node = options.service.node || {};
options.service.identity.capabilities =
options.service.identity.capabilities || {};
options.service.controller.capabilities =
options.service.controller.capabilities || {};
options.service.node.capabilities = options.service.node.capabilities || {};
if (!("service" in options.service.identity.capabilities)) {
this.ctx.logger.debug("setting default identity service caps");
options.service.identity.capabilities.service = [
"UNKNOWN",
//"CONTROLLER_SERVICE"
//"VOLUME_ACCESSIBILITY_CONSTRAINTS"
];
}
if (!("volume_expansion" in options.service.identity.capabilities)) {
this.ctx.logger.debug("setting default identity volume_expansion caps");
options.service.identity.capabilities.volume_expansion = [
"UNKNOWN",
//"ONLINE",
//"OFFLINE"
];
}
if (!("rpc" in options.service.controller.capabilities)) {
this.ctx.logger.debug("setting default controller caps");
options.service.controller.capabilities.rpc = [
//"UNKNOWN",
//"CREATE_DELETE_VOLUME",
//"PUBLISH_UNPUBLISH_VOLUME",
//"LIST_VOLUMES",
//"GET_CAPACITY",
//"CREATE_DELETE_SNAPSHOT",
//"LIST_SNAPSHOTS",
//"CLONE_VOLUME",
//"PUBLISH_READONLY",
//"EXPAND_VOLUME"
];
if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) {
options.service.controller.capabilities.rpc
.push
//"VOLUME_CONDITION",
//"GET_VOLUME"
();
}
if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) {
options.service.controller.capabilities.rpc
.push
//"SINGLE_NODE_MULTI_WRITER"
();
}
}
if (!("rpc" in options.service.node.capabilities)) {
this.ctx.logger.debug("setting default node caps");
options.service.node.capabilities.rpc = [
//"UNKNOWN",
//"STAGE_UNSTAGE_VOLUME",
"GET_VOLUME_STATS",
//"EXPAND_VOLUME",
];
if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) {
//options.service.node.capabilities.rpc.push("VOLUME_CONDITION");
}
if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) {
options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER");
/**
* This is for volumes that support a mount time gid such as smb or fat
*/
//options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP");
}
}
}
/**
* TODO: add Probe here with ctr check to ensure socket is alive
*/
/**
*
* @returns CTR
*/
getCTR() {
return this.ctx.registry.get(`${__REGISTRY_NS__}:ctr`, () => {
const driver = this;
let options = _.get(driver.options, "containerd", {});
options = options || {};
return new CTR(options);
});
}
assertCapabilities(capabilities) {
this.ctx.logger.verbose("validating capabilities: %j", capabilities);
let message = null;
//[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}]
const valid = capabilities.every((capability) => {
if (capability.access_type != "mount") {
message = `invalid access_type ${capability.access_type}`;
return false;
}
if (capability.mount.fs_type) {
message = `invalid fs_type ${capability.mount.fs_type}`;
return false;
}
if (
capability.mount.mount_flags &&
capability.mount.mount_flags.length > 0
) {
message = `invalid mount_flags ${capability.mount.mount_flags}`;
return false;
}
if (
![
"UNKNOWN",
"SINGLE_NODE_WRITER",
"SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0
"SINGLE_NODE_MULTI_WRITER", // added in v1.5.0
"SINGLE_NODE_READER_ONLY",
].includes(capability.access_mode.mode)
) {
message = `invalid access_mode, ${capability.access_mode.mode}`;
return false;
}
return true;
});
return { valid, message };
}
/**
* This should create a dataset with appropriate volume properties, ensuring
* the mountpoint is the target_path
*
* Any volume_context attributes starting with property.<name> will be set as zfs properties
*
* {
"target_path": "/var/lib/kubelet/pods/f8b237db-19e8-44ae-b1d2-740c9aeea702/volumes/kubernetes.io~csi/my-volume-0/mount",
"volume_capability": {
"AccessType": {
"Mount": {}
},
"access_mode": {
"mode": 1
}
},
"volume_context": {
"csi.storage.k8s.io/ephemeral": "true",
"csi.storage.k8s.io/pod.name": "inline-volume-tester-2ptb7",
"csi.storage.k8s.io/pod.namespace": "ephemeral-468",
"csi.storage.k8s.io/pod.uid": "f8b237db-19e8-44ae-b1d2-740c9aeea702",
"csi.storage.k8s.io/serviceAccount.name": "default",
"foo": "bar"
},
"volume_id": "csi-8228252978a824126924de00126e6aec7c989a48a39d577bd3ab718647df5555"
}
*
* @param {*} call
*/
async NodePublishVolume(call) {
const driver = this;
const ctr = driver.getCTR();
const filesystem = new Filesystem();
const mount = new Mount();
const volume_id = call.request.volume_id;
const staging_target_path = call.request.staging_target_path || "";
const target_path = call.request.target_path;
const capability = call.request.volume_capability;
const access_type = capability.access_type || "mount";
const readonly = call.request.readonly;
const volume_context = call.request.volume_context;
let result;
let imageReference;
let imagePullPolicy;
let imagePlatform;
let imageUser;
let labels = {};
Object.keys(volume_context).forEach(function (key) {
switch (key) {
case "image.reference":
imageReference = volume_context[key];
break;
case "image.pullPolicy":
imagePullPolicy = volume_context[key];
break;
case "image.platform":
imagePlatform = volume_context[key];
break;
case "image.user":
imageUser = volume_context[key];
break;
}
if (key.startsWith("snapshot.label.")) {
labels[key.replace(/^snapshot\.label\./, "")] = volume_context[key];
}
});
if (!imageReference) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`image.reference is required`
);
}
if (!volume_id) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`volume_id is required`
);
}
if (!target_path) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`target_path is required`
);
}
if (capability) {
const result = driver.assertCapabilities([capability]);
if (result.valid !== true) {
throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message);
}
}
// create publish directory
if (!fs.existsSync(target_path)) {
fs.mkdirSync(target_path, { recursive: true });
}
if (process.platform != "win32") {
result = await mount.pathIsMounted(target_path);
if (result) {
return {};
}
}
// normalize image reference
let parsedImageReference = parseAll(imageReference);
//console.log(parsedImageReference);
/**
* const typesTemplates = {
'digest': ref => `${ref.digest}`,
'canonical': ref => `${ref.repositoryUrl}@${ref.digest}`,
'repository': ref => `${ref.repositoryUrl}`,
'tagged': ref => `${ref.repositoryUrl}:${ref.tag}`,
'dual': ref => `${ref.repositoryUrl}:${ref.tag}@${ref.digest}`
};
*
*/
switch (parsedImageReference.type) {
// repository is not enough for `ctr`
case "repository":
imageReference = `${imageReference}:latest`;
parsedImageReference = parseAll(imageReference);
break;
case "canonical":
case "digest":
case "dual":
case "tagged":
break;
}
driver.ctx.logger.debug(
`imageReference: ${JSON.stringify(parsedImageReference)}`
);
imageReference = parsedImageReference.toString();
// normalize image pull policy
if (!imagePullPolicy) {
imagePullPolicy =
parsedImageReference.type == "tagged" &&
parsedImageReference.tag == "latest"
? "Always"
: "IfNotPresent";
}
driver.ctx.logger.debug(`effective imagePullPolicy: ${imagePullPolicy}`);
let doPull = true;
switch (String(imagePullPolicy).toLowerCase()) {
case "never":
doPull = false;
break;
case "always":
doPull = true;
break;
case "ifnotpresent":
try {
await ctr.imageInspect(imageReference);
doPull = false;
} catch (err) {}
break;
}
if (doPull) {
let ctr_pull_args = [];
if (imagePlatform) {
ctr_pull_args.push("--platform", imagePlatform);
}
if (imageUser) {
// TODO: decrypt as appropriate
// --user value, -u value User[:password] Registry user and password
ctr_pull_args.push("--user", imageUser);
}
await ctr.imagePull(imageReference, ctr_pull_args);
}
let ctr_mount_args = [];
if (imagePlatform) {
ctr_mount_args.push("--platform", imagePlatform);
}
if (Object.keys(labels).length > 0) {
for (const label in labels) {
ctr_mount_args.push("--label", `${label}=${labels[label]}`);
}
}
// kubelet will manage readonly for us by bind mounting and ro, it is expected that the driver mounts rw
// if (!readonly) {
// ctr_mount_args.push("--rw");
// }
ctr_mount_args.push("--rw");
await ctr.imageMount(imageReference, target_path, ctr_mount_args);
return {};
}
/**
* This should destroy the dataset and remove target_path as appropriate
*
*{
"target_path": "/var/lib/kubelet/pods/f8b237db-19e8-44ae-b1d2-740c9aeea702/volumes/kubernetes.io~csi/my-volume-0/mount",
"volume_id": "csi-8228252978a824126924de00126e6aec7c989a48a39d577bd3ab718647df5555"
}
*
* @param {*} call
*/
async NodeUnpublishVolume(call) {
const driver = this;
const ctr = driver.getCTR();
const volume_id = call.request.volume_id;
const target_path = call.request.target_path;
if (!volume_id) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`volume_id is required`
);
}
if (!target_path) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`target_path is required`
);
}
// unmount
await ctr.imageUnmount(target_path);
// delete snapshot
try {
await ctr.snapshotDelete(target_path);
} catch (err) {
if (!err.stderr.includes("does not exist")) {
throw err;
}
}
// cleanup publish directory
if (fs.existsSync(target_path) && fs.lstatSync(target_path).isDirectory()) {
fs.rmSync(target_path, { recursive: true });
}
return {};
}
/**
* TODO: consider volume_capabilities?
*
* @param {*} call
*/
async GetCapacity(call) {
const driver = this;
const zb = this.getZetabyte();
let datasetParentName = this.getVolumeParentDatasetName();
if (!datasetParentName) {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`invalid configuration: missing datasetParentName`
);
}
if (call.request.volume_capabilities) {
const result = this.assertCapabilities(call.request.volume_capabilities);
if (result.valid !== true) {
return { available_capacity: 0 };
}
}
const datasetName = datasetParentName;
let properties;
properties = await zb.zfs.get(datasetName, ["avail"]);
properties = properties[datasetName];
return { available_capacity: properties.available.value };
}
/**
*
* @param {*} call
*/
async ValidateVolumeCapabilities(call) {
const driver = this;
const result = this.assertCapabilities(call.request.volume_capabilities);
if (result.valid !== true) {
return { message: result.message };
}
return {
confirmed: {
volume_context: call.request.volume_context,
volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested
parameters: call.request.parameters,
},
};
}
}
module.exports.EphemeralInlineContainerDOciDriver =
EphemeralInlineContainerDOciDriver;

View File

@ -0,0 +1,406 @@
const _ = require("lodash");
const fs = require("fs");
const { CsiBaseDriver } = require("../index");
const { GrpcError, grpc } = require("../../utils/grpc");
const Handlebars = require("handlebars");
const path = require("path");
const semver = require("semver");
const WindowsUtils = require("../../utils/windows").Windows;
const wutils = new WindowsUtils();
/**
* https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20190122-csi-inline-volumes.md
* https://kubernetes-csi.github.io/docs/ephemeral-local-volumes.html
*
* Sample calls:
* - https://gcsweb.k8s.io/gcs/kubernetes-jenkins/pr-logs/pull/92387/pull-kubernetes-e2e-gce/1280784994997899264/artifacts/_sig-storage_CSI_Volumes/_Driver_csi-hostpath_/_Testpattern_inline_ephemeral_CSI_volume_ephemeral/should_create_read_write_inline_ephemeral_volume/
* - https://storage.googleapis.com/kubernetes-jenkins/pr-logs/pull/92387/pull-kubernetes-e2e-gce/1280784994997899264/artifacts/_sig-storage_CSI_Volumes/_Driver_csi-hostpath_/_Testpattern_inline_ephemeral_CSI_volume_ephemeral/should_create_read-only_inline_ephemeral_volume/csi-hostpathplugin-0-hostpath.log
*
* inline drivers are assumed to be mount only (no block support)
* purposely there is no native support for size contraints
*
*/
class EphemeralInlineVHDDriver extends CsiBaseDriver {
constructor(ctx, options) {
super(...arguments);
options = options || {};
options.service = options.service || {};
options.service.identity = options.service.identity || {};
options.service.controller = options.service.controller || {};
options.service.node = options.service.node || {};
options.service.identity.capabilities =
options.service.identity.capabilities || {};
options.service.controller.capabilities =
options.service.controller.capabilities || {};
options.service.node.capabilities = options.service.node.capabilities || {};
if (!("service" in options.service.identity.capabilities)) {
this.ctx.logger.debug("setting default identity service caps");
options.service.identity.capabilities.service = [
"UNKNOWN",
//"CONTROLLER_SERVICE"
//"VOLUME_ACCESSIBILITY_CONSTRAINTS"
];
}
if (!("volume_expansion" in options.service.identity.capabilities)) {
this.ctx.logger.debug("setting default identity volume_expansion caps");
options.service.identity.capabilities.volume_expansion = [
"UNKNOWN",
//"ONLINE",
//"OFFLINE"
];
}
if (!("rpc" in options.service.controller.capabilities)) {
this.ctx.logger.debug("setting default controller caps");
options.service.controller.capabilities.rpc = [
//"UNKNOWN",
//"CREATE_DELETE_VOLUME",
//"PUBLISH_UNPUBLISH_VOLUME",
//"LIST_VOLUMES",
//"GET_CAPACITY",
//"CREATE_DELETE_SNAPSHOT",
//"LIST_SNAPSHOTS",
//"CLONE_VOLUME",
//"PUBLISH_READONLY",
//"EXPAND_VOLUME"
];
if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) {
options.service.controller.capabilities.rpc
.push
//"VOLUME_CONDITION",
//"GET_VOLUME"
();
}
if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) {
options.service.controller.capabilities.rpc
.push
//"SINGLE_NODE_MULTI_WRITER"
();
}
}
if (!("rpc" in options.service.node.capabilities)) {
this.ctx.logger.debug("setting default node caps");
options.service.node.capabilities.rpc = [
//"UNKNOWN",
//"STAGE_UNSTAGE_VOLUME",
"GET_VOLUME_STATS",
//"EXPAND_VOLUME",
];
if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) {
//options.service.node.capabilities.rpc.push("VOLUME_CONDITION");
}
if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) {
options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER");
/**
* This is for volumes that support a mount time gid such as smb or fat
*/
//options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP");
}
}
}
assertCapabilities(capabilities) {
this.ctx.logger.verbose("validating capabilities: %j", capabilities);
let message = null;
//[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}]
const valid = capabilities.every((capability) => {
if (capability.access_type != "mount") {
message = `invalid access_type ${capability.access_type}`;
return false;
}
if (capability.mount.fs_type) {
message = `invalid fs_type ${capability.mount.fs_type}`;
return false;
}
if (
capability.mount.mount_flags &&
capability.mount.mount_flags.length > 0
) {
message = `invalid mount_flags ${capability.mount.mount_flags}`;
return false;
}
if (
![
"UNKNOWN",
"SINGLE_NODE_WRITER",
"SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0
"SINGLE_NODE_MULTI_WRITER", // added in v1.5.0
"SINGLE_NODE_READER_ONLY",
].includes(capability.access_mode.mode)
) {
message = `invalid access_mode, ${capability.access_mode.mode}`;
return false;
}
return true;
});
return { valid, message };
}
async Probe(call) {
if (process.platform != "win32") {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`vhd-ephemeral-inline is only available on the windows platform`
);
}
return super.Probe(...arguments);
}
/**
*
* @param {*} call
*/
async NodePublishVolume(call) {
const driver = this;
const volume_id = call.request.volume_id;
const staging_target_path = call.request.staging_target_path || "";
const target_path = call.request.target_path;
const capability = call.request.volume_capability;
const access_type = capability.access_type || "mount";
const readonly = call.request.readonly;
const volume_context = call.request.volume_context;
let result;
let vhdParentPath;
Object.keys(volume_context).forEach(function (key) {
switch (key) {
case "vhd.parentPath":
vhdParentPath = volume_context[key];
break;
}
});
if (!vhdParentPath) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`vhd.parentPath is required`
);
}
if (!volume_id) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`volume_id is required`
);
}
if (!target_path) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`target_path is required`
);
}
if (capability) {
const result = driver.assertCapabilities([capability]);
if (result.valid !== true) {
throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message);
}
}
// sanity check the parent
if (!fs.existsSync(vhdParentPath)) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`vhd.parentPath (${vhdParentPath}) file does not exist`
);
}
// create publish directory
// if (!fs.existsSync(target_path)) {
// await fs.mkdirSync(target_path, { recursive: true });
// }
// get child path name
let vhdParentPathDir = path.dirname(vhdParentPath);
let vhdChildDiskName = volume_id;
if (driver.options.vhd.nameTemplate) {
vhdChildDiskName = Handlebars.compile(driver.options.vhd.nameTemplate)({
// parameters: call.request.parameters,
volume_id,
});
}
let vhdChildPath = `${vhdParentPathDir}${
path.sep
}${vhdChildDiskName}${path.extname(vhdParentPath)}`;
// create vhd
if (!fs.existsSync(vhdChildPath)) {
await wutils.NewVHDDifferencing(vhdParentPath, vhdChildPath);
}
// mount vhd if needed
let disks;
disks = await wutils.GetDisksByLocation(vhdChildPath);
if (disks.length == 0) {
await wutils.MountVHD(vhdChildPath);
}
// ensure disk is mounted
disks = await wutils.GetDisksByLocation(vhdChildPath);
let disk = disks[0];
if (!disk) {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`failed to mount vhd ${vhdParentPath}`
);
}
// ensure the disk is online
if (disk.OperationalStatus != "Online") {
await wutils.OnlineDisk(disk.DiskNumber);
}
// get partition
let partition = await wutils.GetLastPartitionByDiskNumber(disk.DiskNumber);
// get volume
let volume = await wutils.GetVolumeByDiskNumberPartitionNumber(
disk.DiskNumber,
partition.PartitionNumber
);
if (!volume) {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`failed to discover volume for vhd ${vhdParentPath}`
);
}
result = await wutils.GetItem(target_path);
if (!result) {
fs.mkdirSync(target_path, {
recursive: true,
mode: "755",
});
result = await wutils.GetItem(target_path);
}
let targets = result.Target;
if (!Array.isArray(targets)) {
if (targets) {
targets[targets];
} else {
targets = [];
}
}
if (
!targets.some((target) => {
return volume.UniqueId.includes(target);
})
) {
await wutils.MountVolume(volume.UniqueId, target_path);
}
return {};
}
/**
*
* @param {*} call
*/
async NodeUnpublishVolume(call) {
const driver = this;
const volume_id = call.request.volume_id;
const target_path = call.request.target_path;
if (!volume_id) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`volume_id is required`
);
}
if (!target_path) {
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`target_path is required`
);
}
let result;
result = await wutils.GetItem(target_path);
if (result) {
if (result.LinkType == "Junction") {
let volumeId = (await wutils.GetRealTarget(target_path)) || "";
if (volumeId) {
// should only ever have 1
let disks = await wutils.GetDisksByVolumeId(volumeId);
for (const disk of disks) {
if (disk.Location) {
// unmount
await wutils.DismountVHD(disk.Location);
// remove the vhd
fs.rmSync(disk.Location);
}
}
}
}
}
// remove publish folder
await wutils.DeleteItem(target_path);
return {};
}
/**
* TODO: consider volume_capabilities?
*
* @param {*} call
*/
async GetCapacity(call) {
return { available_capacity: 0 };
}
/**
*
* @param {*} call
*/
async ValidateVolumeCapabilities(call) {
const driver = this;
const result = this.assertCapabilities(call.request.volume_capabilities);
if (result.valid !== true) {
return { message: result.message };
}
return {
confirmed: {
volume_context: call.request.volume_context,
volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested
parameters: call.request.parameters,
},
};
}
}
module.exports.EphemeralInlineVHDDriver = EphemeralInlineVHDDriver;

View File

@ -14,6 +14,12 @@ const { ControllerSmbClientDriver } = require("./controller-smb-client");
const { ControllerLustreClientDriver } = require("./controller-lustre-client");
const { ControllerObjectiveFSDriver } = require("./controller-objectivefs");
const { ControllerSynologyDriver } = require("./controller-synology");
const {
EphemeralInlineContainerDOciDriver,
} = require("./ephemeral-inline-containerd-oci");
const {
EphemeralInlineVHDDriver,
} = require("./ephemeral-inline-vhd");
const { NodeManualDriver } = require("./node-manual");
function factory(ctx, options) {
@ -21,13 +27,20 @@ function factory(ctx, options) {
case "freenas-nfs":
case "freenas-smb":
case "freenas-iscsi":
case "freenas-nvmeof":
case "truenas-nfs":
case "truenas-smb":
case "truenas-iscsi":
case "truenas-nvmeof":
return new FreeNASSshDriver(ctx, options);
case "freenas-api-iscsi":
case "freenas-api-nfs":
case "freenas-api-smb":
case "freenas-api-iscsi":
case "freenas-api-nvmeof":
case "truenas-api-nfs":
case "truenas-api-smb":
case "truenas-api-iscsi":
case "truenas-api-nvmeof":
return new FreeNASApiDriver(ctx, options);
case "synology-nfs":
case "synology-smb":
@ -53,6 +66,10 @@ function factory(ctx, options) {
return new ControllerLustreClientDriver(ctx, options);
case "objectivefs":
return new ControllerObjectiveFSDriver(ctx, options);
case "containerd-oci-ephemeral-inline":
return new EphemeralInlineContainerDOciDriver(ctx, options);
case "vhd-ephemeral-inline":
return new EphemeralInlineVHDDriver(ctx, options);
case "node-manual":
return new NodeManualDriver(ctx, options);
default:

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
const _ = require("lodash");
const semver = require("semver");
const { sleep, stringify } = require("../../../utils/general");
const { Zetabyte } = require("../../../utils/zfs");
const { Registry } = require("../../../utils/registry");
// used for in-memory cache of the version info
const FREENAS_SYSTEM_VERSION_CACHE_KEY = "freenas:system_version";
@ -11,6 +13,7 @@ class Api {
this.client = client;
this.cache = cache;
this.options = options;
this.registry = new Registry();
}
async getHttpClient() {
@ -22,7 +25,7 @@ class Api {
* @returns
*/
async getZetabyte() {
return this.ctx.registry.get(`${__REGISTRY_NS__}:zb`, () => {
return this.registry.get(`${__REGISTRY_NS__}:zb`, () => {
return new Zetabyte({
executor: {
spawn: function () {
@ -64,7 +67,7 @@ class Api {
// crude stoppage attempt
let response = await httpClient.get(endpoint, queryParams);
if (lastReponse) {
if (JSON.stringify(lastReponse) == JSON.stringify(response)) {
if (JSON.stringify(lastReponse.body) == JSON.stringify(response.body)) {
break;
}
}
@ -110,60 +113,26 @@ class Api {
}
async getApiVersion() {
const systemVersion = await this.getSystemVersion();
if (systemVersion.v2) {
if ((await this.getSystemVersionMajorMinor()) == 11.2) {
return 1;
}
return 2;
}
if (systemVersion.v1) {
return 1;
}
return 2;
}
async getIsFreeNAS() {
const systemVersion = await this.getSystemVersion();
let version;
if (systemVersion.v2) {
version = systemVersion.v2;
} else {
version = systemVersion.v1.fullversion;
}
if (version.toLowerCase().includes("freenas")) {
return true;
}
return false;
}
async getIsTrueNAS() {
const systemVersion = await this.getSystemVersion();
let version;
if (systemVersion.v2) {
version = systemVersion.v2;
} else {
version = systemVersion.v1.fullversion;
}
if (version.toLowerCase().includes("truenas")) {
return true;
}
return false;
return true;
}
async getIsScale() {
const systemVersion = await this.getSystemVersion();
const major = await this.getSystemVersionMajor();
if (systemVersion.v2 && systemVersion.v2.toLowerCase().includes("scale")) {
// starting with version 25 the version string no longer contains `-SCALE`
if (
systemVersion.v2 &&
(systemVersion.v2.toLowerCase().includes("scale") || Number(major) >= 20)
) {
return true;
}
@ -215,6 +184,12 @@ class Api {
return majorMinor.split(".")[0];
}
async getSystemVersionSemver() {
return semver.coerce(await this.getSystemVersionMajorMinor(), {
loose: true,
});
}
async setVersionInfoCache(versionInfo) {
await this.cache.set(FREENAS_SYSTEM_VERSION_CACHE_KEY, versionInfo, {
ttl: 60 * 1000,
@ -261,28 +236,6 @@ class Api {
versionErrors.v2 = e.toString();
}
httpClient.setApiVersion(1);
/**
* {"fullversion": "FreeNAS-9.3-STABLE-201503200528", "name": "FreeNAS", "version": "9.3"}
* {"fullversion": "FreeNAS-11.2-U5 (c129415c52)", "name": "FreeNAS", "version": ""}
*/
try {
response = await httpClient.get(endpoint, null, { timeout: 5 * 1000 });
versionResponses.v1 = response;
if (response.statusCode == 200 && IsJsonString(response.body)) {
versionInfo.v1 = response.body;
await this.setVersionInfoCache(versionInfo);
// reset apiVersion
httpClient.setApiVersion(startApiVersion);
return versionInfo;
}
} catch (e) {
// if more info is needed use e.stack
versionErrors.v1 = e.toString();
}
// throw error if cannot get v1 or v2 data
// likely bad creds/url
throw new Error(
@ -304,7 +257,7 @@ class Api {
let user_properties = {};
for (const property in properties) {
if (this.getIsUserProperty(property)) {
user_properties[property] = properties[property];
user_properties[property] = String(properties[property]);
}
}
@ -325,7 +278,15 @@ class Api {
getPropertiesKeyValueArray(properties) {
let arr = [];
for (const property in properties) {
arr.push({ key: property, value: properties[property] });
let value = properties[property];
if (
this.getIsUserProperty(property) &&
value != null &&
value !== undefined
) {
value = String(value);
}
arr.push({ key: property, value });
}
return arr;
@ -478,13 +439,13 @@ class Api {
* @param {*} properties
* @returns
*/
async DatasetGet(datasetName, properties) {
async DatasetGet(datasetName, properties, queryParams = {}) {
const httpClient = await this.getHttpClient(false);
let response;
let endpoint;
endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`;
response = await httpClient.get(endpoint);
response = await httpClient.get(endpoint, queryParams);
if (response.statusCode == 200) {
return this.normalizeProperties(response.body, properties);
@ -497,36 +458,75 @@ class Api {
throw new Error(JSON.stringify(response.body));
}
/**
* This is meant to destroy all snapshots on the given dataset
*
* @param {*} datasetName
* @param {*} data
* @returns
*/
async DatasetDestroySnapshots(datasetName, data = {}) {
const httpClient = await this.getHttpClient(false);
let response;
let endpoint;
data.name = datasetName;
const major = await this.getSystemVersionMajor();
if (Number(major) >= 25) {
try {
response = await this.DatasetGet(
datasetName,
["id", "type", "name", "pool", "snapshots"],
{
"extra.snapshots": "true",
"extra.retrieve_children": "false",
}
);
endpoint = "/pool/dataset/destroy_snapshots";
response = await httpClient.post(endpoint, data);
for (const snapshot of _.get(response, "snapshots", [])) {
await this.SnapshotDelete(snapshot.name, {
defer: true,
});
}
} catch (err) {
if (err.toString().includes("dataset does not exist")) {
return;
}
throw err;
}
} else {
data.name = datasetName;
if (response.statusCode == 200) {
return response.body;
endpoint = "/pool/dataset/destroy_snapshots";
response = await httpClient.post(endpoint, data);
if (response.statusCode == 200) {
return response.body;
}
if (
response.statusCode == 422 &&
JSON.stringify(response.body).includes("already exists")
) {
return;
}
throw new Error(JSON.stringify(response.body));
}
if (
response.statusCode == 422 &&
JSON.stringify(response.body).includes("already exists")
) {
return;
}
throw new Error(JSON.stringify(response.body));
}
async SnapshotSet(snapshotName, properties) {
const httpClient = await this.getHttpClient(false);
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = `/pool/snapshot/id/${encodeURIComponent(snapshotName)}`;
} else {
endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`;
}
response = await httpClient.put(endpoint, {
//...this.getSystemProperties(properties),
user_properties_update: this.getPropertiesKeyValueArray(
@ -551,10 +551,17 @@ class Api {
*/
async SnapshotGet(snapshotName, properties) {
const httpClient = await this.getHttpClient(false);
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = `/pool/snapshot/id/${encodeURIComponent(snapshotName)}`;
} else {
endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`;
}
response = await httpClient.get(endpoint);
if (response.statusCode == 200) {
@ -562,7 +569,7 @@ class Api {
}
if (response.statusCode == 404) {
throw new Error("dataset does not exist");
throw new Error("snapshot does not exist");
}
throw new Error(JSON.stringify(response.body));
@ -571,6 +578,7 @@ class Api {
async SnapshotCreate(snapshotName, data = {}) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
@ -581,7 +589,12 @@ class Api {
data.dataset = dataset;
data.name = snapshot;
endpoint = "/zfs/snapshot";
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = "/pool/snapshot";
} else {
endpoint = "/zfs/snapshot";
}
response = await httpClient.post(endpoint, data);
if (response.statusCode == 200) {
@ -601,11 +614,17 @@ class Api {
async SnapshotDelete(snapshotName, data = {}) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = `/pool/snapshot/id/${encodeURIComponent(snapshotName)}`;
} else {
endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`;
}
response = await httpClient.delete(endpoint, data);
if (response.statusCode == 200) {
@ -626,9 +645,360 @@ class Api {
throw new Error(JSON.stringify(response.body));
}
async NvmetSubsysList(data = {}) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = "/nvmet/subsys";
} else {
throw new Error("nvmet is unavailable with TrueNAS versions <25.10");
}
response = await httpClient.get(endpoint, data);
if (response.statusCode == 200) {
return response.body;
}
throw new Error(JSON.stringify(response.body));
}
async NvmetSubsysCreate(subsysName, data = {}) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
data.name = subsysName;
data.allow_any_host = true;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = "/nvmet/subsys";
} else {
throw new Error("nvmet is unavailable with TrueNAS versions <25.10");
}
response = await httpClient.post(endpoint, data);
if (response.statusCode == 200) {
return response.body;
}
if (
response.statusCode == 422 &&
JSON.stringify(response.body).includes("already exists")
) {
return this.NvmetSubsysGetByName(subsysName);
}
throw new Error(JSON.stringify(response.body));
}
async NvmetSubsysGetByName(subsysName, data = {}) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
data.name = subsysName;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = "/nvmet/subsys";
} else {
throw new Error("nvmet is unavailable with TrueNAS versions <25.10");
}
response = await httpClient.get(endpoint, data);
if (response.statusCode == 200) {
for (const subsys of response.body) {
if (subsys.name == subsysName) {
return subsys;
}
}
}
throw new Error(JSON.stringify(response.body));
}
async NvmetSubsysGetById(id, data = {}) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = `/nvmet/subsys/id/${id}`;
} else {
throw new Error("nvmet is unavailable with TrueNAS versions <25.10");
}
response = await httpClient.get(endpoint, data);
if (response.statusCode == 200) {
return response.body;
}
throw new Error(JSON.stringify(response.body));
}
async NvmetSubsysDeleteById(id, data = {}) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = `/nvmet/subsys/id/${id}`;
} else {
throw new Error("nvmet is unavailable with TrueNAS versions <25.10");
}
response = await httpClient.delete(endpoint, data);
if (response.statusCode == 200) {
return;
}
if (
response.statusCode == 422 &&
JSON.stringify(response.body).includes("does not exist")
) {
return;
}
throw new Error(JSON.stringify(response.body));
}
async NvmetPortList(data = {}) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = "/nvmet/port";
} else {
throw new Error("nvmet is unavailable with TrueNAS versions <25.10");
}
response = await httpClient.get(endpoint, data);
if (response.statusCode == 200) {
return response.body;
}
throw new Error(JSON.stringify(response.body));
}
async NvmetPortSubsysList(data = {}) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = "/nvmet/port_subsys";
} else {
throw new Error("nvmet is unavailable with TrueNAS versions <25.10");
}
response = await httpClient.get(endpoint, data);
if (response.statusCode == 200) {
return response.body;
}
throw new Error(JSON.stringify(response.body));
}
async NvmetPortSubsysCreate(port_id, subsys_id) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
let data = {
port_id,
subsys_id,
};
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = "/nvmet/port_subsys";
} else {
throw new Error("nvmet is unavailable with TrueNAS versions <25.10");
}
response = await httpClient.post(endpoint, data);
if (response.statusCode == 200) {
return response.body;
}
//already exists
if (
response.statusCode == 422 &&
JSON.stringify(response.body).includes("already exists")
) {
response = await this.NvmetPortSubsysList({ port_id, subsys_id });
if (Array.isArray(response) && response.length == 1) {
return response[0];
}
}
throw new Error(JSON.stringify(response.body));
}
async NvmetNamespaceCreate(zvol, subsysId, data = {}) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
zvol = String(zvol);
if (zvol.startsWith("/dev/")) {
zvol = zvol.substring(5);
}
if (zvol.startsWith("/")) {
zvol = zvol.substring(1);
}
if (!zvol.startsWith("zvol/")) {
zvol = `zvol/${zvol}`;
}
data.device_type = "ZVOL";
data.device_path = zvol;
data.subsys_id = subsysId;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = "/nvmet/namespace";
} else {
throw new Error("nvmet is unavailable with TrueNAS versions <25.10");
}
response = await httpClient.post(endpoint, data);
if (response.statusCode == 200) {
return response.body;
}
if (
response.statusCode == 422 &&
JSON.stringify(response.body).includes("already exists")
) {
return this.NvmetSubsysGetByName(subsysName);
}
if (
response.statusCode == 422 &&
JSON.stringify(response.body).includes("already used by subsystem")
) {
//This device_path already used by subsystem: csi-pvc-111-clustera
return this.NvmetNamespaceGetByDeivcePath(zvol);
}
throw new Error(JSON.stringify(response.body));
}
async NvmetNamespaceGetByDeivcePath(zvol) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
zvol = String(zvol);
if (zvol.startsWith("/dev/")) {
zvol = zvol.substring(5);
}
if (zvol.startsWith("/")) {
zvol = zvol.substring(1);
}
if (!zvol.startsWith("zvol/")) {
zvol = `zvol/${zvol}`;
}
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = "/nvmet/namespace";
} else {
throw new Error("nvmet is unavailable with TrueNAS versions <25.10");
}
let data = {
device_path: zvol,
};
response = await httpClient.get(endpoint, data);
if (response.statusCode == 200) {
if (Array.isArray(response.body) && response.body.length == 1) {
return response.body[0];
}
}
throw new Error(JSON.stringify(response.body));
}
async NvmetNamespaceDeleteById(id) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = `/nvmet/namespace/id/${id}`;
} else {
throw new Error("nvmet is unavailable with TrueNAS versions <25.10");
}
response = await httpClient.delete(endpoint);
if (response.statusCode == 200) {
return;
}
if (
response.statusCode == 422 &&
JSON.stringify(response.body).includes("does not exist")
) {
return;
}
throw new Error(JSON.stringify(response.body));
}
async CloneCreate(snapshotName, datasetName, data = {}) {
const httpClient = await this.getHttpClient(false);
const zb = await this.getZetabyte();
const systemVersionSemver = await this.getSystemVersionSemver();
let response;
let endpoint;
@ -636,7 +1006,12 @@ class Api {
data.snapshot = snapshotName;
data.dataset_dst = datasetName;
endpoint = "/zfs/snapshot/clone";
if (semver.satisfies(systemVersionSemver, ">=25.10")) {
endpoint = "/pool/snapshot/clone";
} else {
endpoint = "/zfs/snapshot/clone";
}
response = await httpClient.post(endpoint, data);
if (response.statusCode == 200) {

View File

@ -9,11 +9,7 @@ class Client {
constructor(options = {}) {
this.options = JSON.parse(JSON.stringify(options));
this.logger = console;
// default to v1.0 for now
if (!this.options.apiVersion) {
this.options.apiVersion = 2;
}
this.options.apiVersion = 2;
}
getHttpAgent() {

File diff suppressed because it is too large Load Diff

View File

@ -56,7 +56,7 @@ class CsiBaseDriver {
* in order of preference:
* - democratic-csi.org/{instance_id}/{key}
* - democratic-csi.org/{driver}/{key}
* - {key}
* - democratic-csi.org/{key}
*
* @param {*} parameters
* @param {*} key
@ -104,6 +104,32 @@ class CsiBaseDriver {
return normalized;
}
getMergedDriverOptions(optionOverlays = []) {
const driver = this;
let driverOptions = Object.assign({}, driver.options);
const allowedOptionsOverrides = ["zfs.zvolBlocksize"];
optionOverlays.forEach((optionOverlay) => {
allowedOptionsOverrides.forEach((prop) => {
if (_.has(optionOverlay, prop)) {
switch (prop) {
// TODO: specific cases can be added here to do merge/replace logic etc
default:
driverOptions = _.set(
driverOptions,
prop,
_.get(optionOverlay, prop)
);
break;
}
}
});
});
return driverOptions;
}
/**
* Get an instance of the Filesystem class
*
@ -124,10 +150,13 @@ class CsiBaseDriver {
* @returns Mount
*/
getDefaultMountInstance() {
return this.ctx.registry.get(`${__REGISTRY_NS__}:default_mount_instance`, () => {
const filesystem = this.getDefaultFilesystemInstance();
return new Mount({ filesystem });
});
return this.ctx.registry.get(
`${__REGISTRY_NS__}:default_mount_instance`,
() => {
const filesystem = this.getDefaultFilesystemInstance();
return new Mount({ filesystem });
}
);
}
/**
@ -136,9 +165,12 @@ class CsiBaseDriver {
* @returns ISCSI
*/
getDefaultISCSIInstance() {
return this.ctx.registry.get(`${__REGISTRY_NS__}:default_iscsi_instance`, () => {
return new ISCSI();
});
return this.ctx.registry.get(
`${__REGISTRY_NS__}:default_iscsi_instance`,
() => {
return new ISCSI();
}
);
}
/**
@ -148,37 +180,46 @@ class CsiBaseDriver {
*/
getDefaultNVMEoFInstance() {
const driver = this;
return this.ctx.registry.get(`${__REGISTRY_NS__}:default_nvmeof_instance`, () => {
return new NVMEoF({ logger: driver.ctx.logger });
});
return this.ctx.registry.get(
`${__REGISTRY_NS__}:default_nvmeof_instance`,
() => {
return new NVMEoF({ logger: driver.ctx.logger });
}
);
}
getDefaultZetabyteInstance() {
return this.ctx.registry.get(`${__REGISTRY_NS__}:default_zb_instance`, () => {
return new Zetabyte({
idempotent: true,
paths: {
zfs: "zfs",
zpool: "zpool",
sudo: "sudo",
chroot: "chroot",
},
//logger: driver.ctx.logger,
executor: {
spawn: function () {
const command = `${arguments[0]} ${arguments[1].join(" ")}`;
return cp.exec(command);
return this.ctx.registry.get(
`${__REGISTRY_NS__}:default_zb_instance`,
() => {
return new Zetabyte({
idempotent: true,
paths: {
zfs: "zfs",
zpool: "zpool",
sudo: "sudo",
chroot: "chroot",
},
},
log_commands: true,
});
});
//logger: driver.ctx.logger,
executor: {
spawn: function () {
const command = `${arguments[0]} ${arguments[1].join(" ")}`;
return cp.exec(command);
},
},
log_commands: true,
});
}
);
}
getDefaultOneClientInstance() {
return this.ctx.registry.get(`${__REGISTRY_NS__}:default_oneclient_instance`, () => {
return new OneClient();
});
return this.ctx.registry.get(
`${__REGISTRY_NS__}:default_oneclient_instance`,
() => {
return new OneClient();
}
);
}
getDefaultObjectiveFSInstance() {
@ -198,11 +239,14 @@ class CsiBaseDriver {
* @returns CsiProxyClient
*/
getDefaultCsiProxyClientInstance() {
return this.ctx.registry.get(`${__REGISTRY_NS__}:default_csi_proxy_instance`, () => {
const options = {};
options.services = _.get(this.options, "node.csiProxy.services", {});
return new CsiProxyClient(options);
});
return this.ctx.registry.get(
`${__REGISTRY_NS__}:default_csi_proxy_instance`,
() => {
const options = {};
options.services = _.get(this.options, "node.csiProxy.services", {});
return new CsiProxyClient(options);
}
);
}
getDefaultKubernetsConfigInstance() {
@ -216,6 +260,15 @@ class CsiBaseDriver {
);
}
async getPersistentVolumeClaim(name, namespace) {
const driver = this;
const kc = driver.getDefaultKubernetsConfigInstance();
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
let res = await k8sApi.readNamespacedPersistentVolumeClaim(name, namespace);
return res.body;
}
getCsiProxyEnabled() {
const defaultValue = process.platform == "win32";
return _.get(this.options, "node.csiProxy.enabled", defaultValue);
@ -1054,7 +1107,7 @@ class CsiBaseDriver {
for (let nvmeofConnection of nvmeofConnections) {
// connect
try {
await GeneralUtils.retry(15, 2000, async () => {
await GeneralUtils.retry(30, 2000, async () => {
await nvmeof.connectByNQNTransport(
nvmeofConnection.nqn,
nvmeofConnection.transport
@ -1069,15 +1122,36 @@ class CsiBaseDriver {
continue;
}
// wait for connection to actually be connected
try {
await GeneralUtils.retry(30, 2000, async () => {
let state = await nvmeof.getSubsystemStateByNQNTransport(
nvmeofConnection.nqn,
nvmeofConnection.transport
);
if (state != "live") {
throw new Error("nvmeof connection is not live");
}
});
} catch (err) {
driver.ctx.logger.warn(
`error: ${JSON.stringify(
err
)} transport never became live: ${
nvmeofConnection.transport
}`
);
continue;
}
// find controller device
let controllerDevice;
try {
await GeneralUtils.retry(15, 2000, async () => {
await GeneralUtils.retry(30, 2000, async () => {
controllerDevice =
await nvmeof.controllerDevicePathByTransportNQN(
nvmeofConnection.transport,
nvmeofConnection.nqn,
nvmeofConnection.nsid
nvmeofConnection.nqn
);
if (!controllerDevice) {
@ -1488,11 +1562,13 @@ class CsiBaseDriver {
// format
result = await filesystem.deviceIsFormatted(device);
if (!result) {
let formatOptions = _.get(
driver.options.node.format,
[fs_type, "customOptions"],
[]
);
let formatOptions = [
..._.get(
driver.options.node.format,
[fs_type, "customOptions"],
[]
),
];
if (!Array.isArray(formatOptions)) {
formatOptions = [];
}
@ -2042,7 +2118,11 @@ class CsiBaseDriver {
result = await wutils.GetItem(win_staging_target_path);
}
if (!volume.UniqueId.includes(result.Target[0])) {
if (
!result.Target.some((target) => {
return volume.UniqueId.includes(target);
})
) {
// mount up!
await wutils.MountVolume(
volume.UniqueId,
@ -3587,6 +3667,9 @@ class CsiBaseDriver {
if (await wutils.VolumeIsIscsi(target)) {
node_attach_driver = "iscsi";
}
if (await wutils.VolumeIsVHD(target)) {
node_attach_driver = "vhd";
}
}
if (!node_attach_driver) {
@ -3599,6 +3682,7 @@ class CsiBaseDriver {
res.usage = [{ total: 0, unit: "BYTES" }];
break;
case "iscsi":
case "vhd":
let node_volume = await wutils.GetVolumeByVolumeId(target);
res.usage = [
{
@ -3716,10 +3800,14 @@ class CsiBaseDriver {
if (!volume_path) {
throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_path`);
}
const block_path = volume_path + "/block_device";
const capacity_range = call.request.capacity_range;
const volume_capability = call.request.volume_capability;
// placeholder
let capacity_bytes;
switch (driver.__getNodeOsDriver()) {
case NODE_OS_DRIVER_POSIX:
if (
@ -3780,6 +3868,7 @@ class CsiBaseDriver {
await GeneralUtils.sleep(2000);
}
// is_formatted = false;
if (is_formatted && access_type == "mount") {
fs_info = await filesystem.getDeviceFilesystemInfo(device);
fs_type = fs_info.type;
@ -3814,13 +3903,20 @@ class CsiBaseDriver {
);
}
}
result = await mount.getMountDetails(device_path, ["size"]);
capacity_bytes = result.size;
} else {
//block device unformatted
return {};
result = await filesystem.getBlockDevice(device);
capacity_bytes = result.size;
return { capacity_bytes };
}
} else {
// not block device
return {};
result = await mount.getMountDetails(device_path, ["size"]);
capacity_bytes = result.size;
return { capacity_bytes };
}
break;
@ -3985,7 +4081,7 @@ class CsiBaseDriver {
);
}
return {};
return { capacity_bytes };
}
}

View File

@ -140,7 +140,8 @@ class ZfsLocalEphemeralInlineDriver extends CsiBaseDriver {
sshClient = this.getSshClient();
executor = new ZfsSshProcessManager(sshClient);
}
return new Zetabyte({
const options = {
executor,
idempotent: true,
chroot: this.options.zfs.chroot,
@ -148,7 +149,21 @@ class ZfsLocalEphemeralInlineDriver extends CsiBaseDriver {
zpool: "/usr/sbin/zpool",
zfs: "/usr/sbin/zfs",
},
});
};
if (process.env.DEMOCRATIC_CSI_IS_CONTAINER == "true") {
delete options.chroot;
options.paths.zpool = "/usr/local/bin/zpool";
options.paths.zfs = "/usr/local/bin/zfs";
}
options.paths = Object.assign(
{},
options.paths,
_.get(this.options, "zfs.cli.paths", {})
);
return new Zetabyte(options);
});
}

176
src/utils/ctr.js Normal file
View File

@ -0,0 +1,176 @@
const cp = require("child_process");
class CTR {
constructor(options = {}) {
const ctr = this;
ctr.options = options;
options.containerd = options.containerd || {};
if (process.platform != "win32" && options.containerd.address) {
//options.containerd.address = "/run/containerd/containerd.sock";
//options.containerd.address;
}
if (process.platform == "win32" && options.containerd.windowsAddress) {
// --address value, -a value Address for containerd's GRPC server (default: "\\\\.\\pipe\\containerd-containerd") [%CONTAINERD_ADDRESS%]
options.containerd.address = options.containerd.windowsAddress;
}
if (!options.containerd.namespace) {
//options.containerd.namespace = "default";
}
options.paths = options.paths || {};
if (!options.paths.ctr) {
options.paths.ctr = "ctr";
}
if (!options.paths.sudo) {
options.paths.sudo = "/usr/bin/sudo";
}
if (!options.executor) {
options.executor = {
spawn: cp.spawn,
};
}
if (!options.env) {
options.env = {};
}
if (ctr.options.logger) {
ctr.logger = ctr.options.logger;
} else {
ctr.logger = console;
console.verbose = function () {
console.log(...arguments);
};
}
}
async info() {
const ctr = this;
let args = ["info"];
let result = await ctr.exec(ctr.options.paths.ctr, args);
return result.parsed;
}
// ctr images pull "${IMAGE}"
async imagePull(image, args = []) {
const ctr = this;
args.unshift("images", "pull");
args.push(image);
let result = await ctr.exec(ctr.options.paths.ctr, args);
return result.parsed;
}
// ctr images mount --rw "${IMAGE}" "${MOUNT_TARGET}"
async imageMount(image, target, args = []) {
const ctr = this;
args.unshift("images", "mount");
args.push(image, target);
let result = await ctr.exec(ctr.options.paths.ctr, args);
return result;
}
// ctr images unmount "${MOUNT_TARGET}"
async imageUnmount(target, args = []) {
const ctr = this;
args.unshift("images", "unmount");
args.push(target);
let result = await ctr.exec(ctr.options.paths.ctr, args);
return result;
}
// ctr image inspect docker.io/library/ubuntu:latest
async imageInspect(image, args = []) {
const ctr = this;
args.unshift("images", "inspect");
args.push(image);
let result = await ctr.exec(ctr.options.paths.ctr, args);
return result;
}
async snapshotList(args = []) {
const ctr = this;
args.unshift("snapshot", "list");
let result = await ctr.exec(ctr.options.paths.ctr, args);
return result;
}
// ctr snapshots delete [command options] <key> [<key>, ...]
async snapshotDelete(key) {
const ctr = this;
let args = ["snapshot", "delete"];
args.push(key);
let result = await ctr.exec(ctr.options.paths.ctr, args);
return result;
}
exec(command, args, options = {}) {
// if (!options.hasOwnProperty("timeout")) {
// options.timeout = DEFAULT_TIMEOUT;
// }
const ctr = this;
args = args || [];
// --debug
if (process.platform != "win32" && ctr.options.sudo) {
args.unshift(command);
command = ctr.options.paths.sudo;
}
options.env = { ...{}, ...ctr.options.env, ...options.env };
if (ctr.options.containerd.address) {
options.env.CONTAINERD_ADDRESS = ctr.options.containerd.address;
}
if (ctr.options.containerd.namespace) {
options.env.CONTAINERD_NAMESPACE = ctr.options.containerd.namespace;
}
options.env.PATH = process.env.PATH;
ctr.logger.verbose("executing ctr command: %s %s", command, args.join(" "));
return new Promise((resolve, reject) => {
const child = ctr.options.executor.spawn(command, args, options);
let stdout = "";
let stderr = "";
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 };
try {
result.parsed = JSON.parse(result.stdout);
} catch (err) {}
// timeout scenario
if (code === null) {
result.timeout = true;
reject(result);
}
if (code) {
reject(result);
} else {
resolve(result);
}
});
});
}
}
module.exports.CTR = CTR;

View File

@ -272,6 +272,19 @@ async function hostname_lookup(hostname) {
});
}
function expandenv(string, env) {
if (!(typeof string === "string" || string instanceof String)) {
throw new Error("Please pass a string into expandenv");
}
env = env ? env : process.env;
return string.replace(/\$\{?[a-zA-Z_]+[a-zA-Z0-9_]*\}?/g, function (match) {
match = match.replace(/[^A-Za-z0-9_]/g, "");
return env[match] || "";
});
}
module.exports.sleep = sleep;
module.exports.md5 = md5;
module.exports.crc32 = crc32;
@ -292,3 +305,4 @@ module.exports.default_supported_file_filesystems =
module.exports.retry = retry;
module.exports.trimchar = trimchar;
module.exports.hostname_lookup = hostname_lookup;
module.exports.expandenv = expandenv;

View File

@ -613,7 +613,32 @@ class ISCSI {
args.unshift(command);
command = iscsi.options.paths.sudo;
}
console.log("executing iscsi command: %s %s", command, args.join(" "));
// ensure all args are converted to string values
args = args.map(String);
// --name node.session.auth.password --value FOOBAR
let argIndex;
let cleansedArgs = [...args];
argIndex = args.findIndex((value) => {
return value.trim() == "node.session.auth.password";
});
if (argIndex >= 0 && cleansedArgs[argIndex + 1]?.trim() == "--value") {
cleansedArgs[argIndex + 2] = "redacted";
}
// --name node.session.auth.password_id --value FOOBAR
argIndex = args.findIndex((value) => {
return value.trim() == "node.session.auth.password_in";
});
if (argIndex >= 0 && cleansedArgs[argIndex + 1]?.trim() == "--value") {
cleansedArgs[argIndex + 2] = "redacted";
}
const cleansedLog = `${command} ${cleansedArgs.join(" ")}`;
console.log("executing iscsi command: %s", cleansedLog);
return new Promise((resolve, reject) => {
const child = iscsi.options.executor.spawn(command, args, options);

View File

@ -254,6 +254,9 @@ class Kopia {
command = kopia.options.paths.sudo;
}
// ensure all args are converted to string values
args = args.map(String);
options.env = {
...{},
...process.env,

View File

@ -10,7 +10,17 @@ const FINDMNT_COMMON_OPTIONS = [
"--nofsroot", // prevents unwanted behavior with cifs volumes
];
const DEFAULT_TIMEOUT = process.env.MOUNT_DEFAULT_TIMEOUT || 30000;
let DEFAULT_TIMEOUT = 30 * 1000;
if (process.env.MOUNT_DEFAULT_TIMEOUT) {
if (/^\d+$/.test(process.env.MOUNT_DEFAULT_TIMEOUT)) {
DEFAULT_TIMEOUT = parseInt(process.env.MOUNT_DEFAULT_TIMEOUT);
} else {
console.log(
"invalid MOUNT_DEFAULT_TIMEOUT set: " + process.env.MOUNT_DEFAULT_TIMEOUT
);
}
}
class Mount {
constructor(options = {}) {
@ -84,6 +94,37 @@ class Mount {
return true;
}
/**
* findmnt --source <device> --output source,target,fstype,label,options,avail,size,used -b -J
*
* @param {*} device
*/
async getDeviceMounts(device) {
const mount = this;
const filesystem = await mount.getFilesystemInstance();
if (device.startsWith("/")) {
device = await filesystem.realpath(device);
}
let args = [];
args = args.concat(["--source", device]);
args = args.concat(FINDMNT_COMMON_OPTIONS);
let result;
try {
result = await mount.exec(mount.options.paths.findmnt, args);
} catch (err) {
// no results
if (err.code == 1) {
return { filesystems: [] };
} else {
throw err;
}
}
return JSON.parse(result.stdout);
}
/**
* findmnt --mountpoint / --output source,target,fstype,label,options,avail,size,used -b -J
*
@ -387,7 +428,7 @@ class Mount {
exec(command, args, options = {}) {
if (!options.hasOwnProperty("timeout")) {
options.timeout = DEFAULT_TIMEOUT;
options.timeout = parseInt(DEFAULT_TIMEOUT) || 30 * 1000;
}
const mount = this;

View File

@ -29,9 +29,9 @@ class NVMEoF {
nvmeof.logger = nvmeof.options.logger;
} else {
nvmeof.logger = console;
console.verbose = function() {
console.verbose = function () {
console.log(...arguments);
}
};
}
}
@ -112,7 +112,7 @@ class NVMEoF {
if (!arg.startsWith("-")) {
arg = `--${arg}`;
}
transport_args.push(arg, value);
}
}
@ -122,9 +122,11 @@ class NVMEoF {
try {
await nvmeof.exec(nvmeof.options.paths.nvme, args);
} catch (err) {
// already connnected - is mispelled in older versions so we include both
if (
err.stderr &&
(err.stderr.includes("already connected") ||
err.stderr.includes("already connnected") ||
err.stderr.includes("Operation already in progress"))
) {
// idempotent
@ -216,6 +218,50 @@ class NVMEoF {
return false;
}
async parseTransportFromPath(path) {
let address;
let service;
switch (path.Transport) {
case "fc":
case "rdma":
case "tcp":
let controllerAddress = path.Address;
/**
* For backwards compatibility with older nvme-cli versions (at least < 2.2.1)
* old: "Address":"traddr=127.0.0.1 trsvcid=4420"
* new: "Address":"traddr=127.0.0.1,trsvcid=4420"
*/
controllerAddress = controllerAddress.replace(
new RegExp(/ ([a-z_]*=)/, "g"),
",$1"
);
let parts = controllerAddress.split(",");
for (let i_part of parts) {
let i_parts = i_part.split("=");
switch (i_parts[0].trim()) {
case "traddr":
address = i_parts[1].trim();
break;
case "trsvcid":
service = i_parts[1].trim();
break;
}
}
break;
case "pcie":
address = path.Address;
break;
}
return {
type: path.Transport,
address,
service,
};
}
async parseTransport(transport) {
if (typeof transport === "object") {
return transport;
@ -279,9 +325,7 @@ class NVMEoF {
async pathExists(path) {
const nvmeof = this;
try {
await nvmeof.exec("stat", [
path,
]);
await nvmeof.exec("stat", [path]);
return true;
} catch (err) {
return false;
@ -302,7 +346,7 @@ class NVMEoF {
}
throw err;
}
return result.stdout.trim() == "Y";
}
@ -338,11 +382,17 @@ class NVMEoF {
async controllerDevicePathByTransportNQN(transport, nqn) {
const nvmeof = this;
transport = await nvmeof.parseTransport(transport);
let controller = await nvmeof.getControllerByTransportNQN(transport, nqn);
if (controller) {
return `/dev/${controller.Controller}`;
let path = await nvmeof.getSubsystemPathByNQNTransport(nqn, transport);
if (path) {
return `/dev/${path.Name}`;
}
// let controller = await nvmeof.getControllerByTransportNQN(transport, nqn);
// if (controller) {
// return `/dev/${controller.Controller}`;
// }
}
async getSubsystems() {
@ -396,7 +446,7 @@ class NVMEoF {
for (let subsystem of subsystems) {
if (subsystem.Namespaces) {
for (let namespace of subsystem.Namespaces) {
if (namespace.NameSpace == name) {
if (namespace.NameSpace == name && subsystem.Controllers) {
return subsystem.Controllers;
}
}
@ -433,37 +483,18 @@ class NVMEoF {
continue;
}
let controllerAddress = controller.Address;
/**
* For backwards compatibility with older nvme-cli versions (at least < 2.2.1)
* old: "Address":"traddr=127.0.0.1 trsvcid=4420"
* new: "Address":"traddr=127.0.0.1,trsvcid=4420"
*/
controllerAddress = controllerAddress.replace(
new RegExp(/ ([a-z_]*=)/, "g"),
",$1"
let controller_transport = await nvmeof.parseTransportFromPath(
controller
);
let parts = controllerAddress.split(",");
let traddr;
let trsvcid;
for (let i_part of parts) {
let i_parts = i_part.split("=");
switch (i_parts[0].trim()) {
case "traddr":
traddr = i_parts[1].trim();
break;
case "trsvcid":
trsvcid = i_parts[1].trim();
break;
}
}
if (traddr != transport.address) {
if (controller_transport.address != transport.address) {
continue;
}
if (transport.service && trsvcid != transport.service) {
if (
transport.service &&
controller_transport.service != transport.service
) {
continue;
}
@ -515,6 +546,39 @@ class NVMEoF {
nvmeof.logger.warn(`failed to find nqn for device: ${name}`);
}
async getSubsystemStateByNQNTransport(nqn, transport) {
const nvmeof = this;
transport = await nvmeof.parseTransport(transport);
const path = await nvmeof.getSubsystemPathByNQNTransport(nqn, transport);
return path?.State;
}
async getSubsystemPathByNQNTransport(nqn, transport) {
const nvmeof = this;
transport = await nvmeof.parseTransport(transport);
const subsysList = await nvmeof.listSubsys(["-v"]);
host_label: for (const host of subsysList) {
subsys_label: for (const subsys of host.Subsystems) {
if (subsys.NQN != nqn) {
continue;
}
path_label: for (const path of subsys.Paths) {
let parsed_path_transport = await nvmeof.parseTransportFromPath(path);
for (const key of Object.keys(transport)) {
if (
["type", "address", "service"].includes(key) &&
transport[key] != parsed_path_transport[key]
) {
break path_label;
}
}
return path;
}
}
}
}
devicePathByModelNumberSerialNumber(modelNumber, serialNumber) {
modelNumber = modelNumber.replaceAll(" ", "_");
serialNumber = serialNumber.replaceAll(" ", "_");

View File

@ -89,6 +89,24 @@ class Windows {
} catch (err) {}
}
async DeleteItem(localPath) {
let command;
let result;
command = '(Get-Item "$Env:localpath").Delete() | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: {
localpath: localPath,
},
});
} catch (err) {
let details = _.get(err, "stderr", "");
if (!details.includes("does not exist")) {
throw err;
}
}
}
async GetSmbGlobalMapping(remotePath) {
let command;
// cannot have trailing slash nor a path
@ -423,13 +441,30 @@ class Windows {
let command;
let result;
command = "Get-WmiObject Win32_DiskDrive | ConvertTo-Json";
//command = "Get-WmiObject Win32_DiskDrive | ConvertTo-Json";
command = "Get-CimInstance Win32_DiskDrive | ConvertTo-Json";
result = await this.ps.exec(command);
this.resultToArray(result);
return result.parsed;
}
async GetWin32DiskDriveByDiskNumber(diskNumber) {
let result;
result = await this.GetWin32DiskDrives();
for (let drive of result) {
if (drive.Index == diskNumber) {
return drive;
}
}
}
async GetWin32DiskDriveByUniqueId(uniqueId) {
let result;
result = await this.GetDiskByUniqueId(uniqueId);
return this.GetWin32DiskDriveByDiskNumber(result.DiskNumber);
}
async GetDiskLunByDiskNumber(diskNumber) {
let result;
result = await this.GetWin32DiskDrives();
@ -514,6 +549,75 @@ class Windows {
return result.parsed;
}
async GetDiskByUniqueId(uniqueId) {
let command;
let result;
command = 'Get-Disk -UniqueId "$Env:uniqueid" | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: {
uniqueid: uniqueId,
},
});
return result.parsed;
} catch (err) {
throw err;
}
}
async GetDisksByFriendlyName(friendlyName) {
let command;
let result;
command = 'Get-Disk -FriendlyName "$Env:friendlyname" | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: {
friendlyname: friendlyName,
},
});
this.resultToArray(result);
return result.parsed;
} catch (err) {
throw err;
}
}
async GetDisksByBusType(busTpe) {
let command;
let result;
command =
'Get-Disk | Where-Object { $_.BusType -eq "$Env:bustype" } | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: {
bustype: busTpe,
},
});
this.resultToArray(result);
return result.parsed;
} catch (err) {
throw err;
}
}
async GetDisksByLocation(location) {
let command;
let result;
command =
'Get-Disk | Where-Object { $_.Location -eq "$Env:location" } | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: {
location,
},
});
this.resultToArray(result);
return result.parsed;
} catch (err) {
throw err;
}
}
async GetDisks() {
let command;
let result;
@ -560,6 +664,20 @@ class Windows {
await this.ps.exec(command);
}
async OnlineDisk(diskNumber) {
let command;
command = `Set-Disk -Number ${diskNumber} -IsOffline $false`;
await this.ps.exec(command);
}
async OfflineDisk(diskNumber) {
let command;
command = `Set-Disk -Number ${diskNumber} -IsOffline $true`;
await this.ps.exec(command);
}
async DiskHasBasicPartition(diskNumber) {
let command;
let result;
@ -632,6 +750,8 @@ class Windows {
let command;
let result;
// NOTE: this syntax is more forgiving
// Get-Volume | Where-Object { $_.UniqueId -match "Volume{74798398-bb39-11f0-af08-00155dab0c98}\\" }
command = `Get-Volume -UniqueId \"${volumeId}\" -ErrorAction Stop | ConvertTo-Json`;
result = await this.ps.exec(command);
@ -697,6 +817,20 @@ class Windows {
return false;
}
async VolumeIsVHD(volumeId) {
let disks = await this.GetDisksByVolumeId(volumeId);
for (let disk of disks) {
if (
_.get(disk, "BusType", "").toLowerCase() ==
"File Backed Virtual".toLowerCase()
) {
return true;
}
}
return false;
}
async FormatVolume(volumeId) {
let command;
command = `Get-Volume -UniqueId \"${volumeId}\" | Format-Volume -FileSystem ntfs -Confirm:$false`;
@ -781,6 +915,332 @@ class Windows {
await this.ps.exec(command);
}
async GetStoragePoolByFriendlyName(friendlyName) {
let command;
let result;
command =
'Get-StoragePool -FriendlyName "$Env:friendlyname" | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: {
friendlyname: friendlyName,
},
});
this.resultToArray(result);
return result.parsed;
} catch (err) {
throw err;
}
}
async GetVirtualDisksByFriendlyName(friendlyName) {
let command;
let result;
command =
'Get-VirtualDisk -FriendlyName "$Env:friendlyname" | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: {
friendlyname: friendlyName,
},
});
this.resultToArray(result);
return result.parsed;
} catch (err) {
throw err;
}
}
async GetVirtualDiskByUniqueId(uniqueId) {
let command;
let result;
command = 'Get-VirtualDisk -UniqueId "$Env:uniqueid" | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: {
uniqueid: uniqueId,
},
});
return result.parsed;
} catch (err) {
throw err;
}
}
async RemoveVirtualDisksByFriendlyName(friendlyName) {
let command;
command =
'Remove-VirtualDisk -Confirm:$false -FriendlyName "$Env:friendlyname"';
try {
await this.ps.exec(command, {
env: {
friendlyname: friendlyName,
},
});
} catch (err) {
let details = _.get(err, "stderr", "");
if (details.includes("No MSFT_VirtualDisk objects found")) {
return;
}
throw err;
}
}
async RemoveVirtualDiskByUniqueId(uniqueId) {
let command;
command = 'Remove-VirtualDisk -Confirm:$false -UniqueId "$Env:uniqueid"';
try {
await this.ps.exec(command, {
env: {
uniqueid: uniqueId,
},
});
} catch (err) {
let details = _.get(err, "stderr", "");
if (details.includes("No MSFT_VirtualDisk objects found")) {
return;
}
throw err;
}
}
async ResizeVirtualDisksByFriendlyName(friendlyName, size) {
let command;
command = `Resize-VirtualDisk -Confirm:$false -FriendlyName "$Env:friendlyname" -Size ${size}`;
try {
await this.ps.exec(command, {
env: {
friendlyname: friendlyName,
},
});
} catch (err) {
throw err;
}
}
async ResizeVirtualDiskByUniqueId(uniqueId, size) {
let command;
command = `Remove-VirtualDisk -Confirm:$false -UniqueId "$Env:uniqueid" -Size ${size}`;
try {
await this.ps.exec(command, {
env: {
uniqueid: uniqueId,
},
});
} catch (err) {
throw err;
}
}
async NewVirtualDisk(
storagePoolFriendlyName,
friendlyName,
size,
extraArgs = []
) {
/**
* -ProvisioningType Thin|Fixed
* -ResiliencySettingName Simple|Mirror|Parity
* -Usage Data
*/
let command;
let result;
extraArgs.push("-ResiliencySettingName", '"Simple"');
extraArgs.push("-ProvisioningType", '"Thin"');
command = `New-VirtualDisk -StoragePoolFriendlyName "$Env:storagepoolfriendlyname" -FriendlyName "$Env:friendlyname" -Size ${size} ${extraArgs.join(
" "
)} | ConvertTo-Json`;
try {
result = await this.ps.exec(command, {
env: {
storagepoolfriendlyname: storagePoolFriendlyName,
friendlyname: friendlyName,
size,
},
});
return result.parsed;
} catch (err) {
throw err;
}
}
async NewVirtualDiskCloneByFriendlyName(
storagePoolFriendlyName,
virutalDiskFriendlyName,
friendlyName
) {
let command;
let result;
command = `New-VirtualDiskClone -FriendlyName "$Env:friendlyname" -VirtualDiskFriendlyName "$Env:virutaldiskfriendlyname" -TargetStoragePoolName "$Env:storagepoolfriendlyname" | ConvertTo-Json`;
try {
result = await this.ps.exec(command, {
env: {
storagepoolfriendlyname: storagePoolFriendlyName,
friendlyname: friendlyName,
virutaldiskfriendlyname: virutalDiskFriendlyName,
},
});
return result.parsed;
} catch (err) {
throw err;
}
}
async NewVirtualDiskCloneByUniqueId(
storagePoolFriendlyName,
uniqueId,
friendlyName
) {
let command;
let result;
command = `New-VirtualDiskClone -FriendlyName "$Env:friendlyname" -VirtualDiskUniqueId "$Env:uniqueid" -TargetStoragePoolName "$Env:storagepoolfriendlyname" | ConvertTo-Json`;
try {
result = await this.ps.exec(command, {
env: {
storagepoolfriendlyname: storagePoolFriendlyName,
friendlyname: friendlyName,
uniqueid: uniqueId,
},
});
return result.parsed;
} catch (err) {
throw err;
}
}
async NewVirtualDiskSnapshotByFriendlyName(
storagePoolFriendlyName,
virutalDiskFriendlyName,
friendlyName
) {
let command;
let result;
command = `New-VirtualDiskSnapshot -FriendlyName "$Env:friendlyname" -VirtualDiskFriendlyName "$Env:virutaldiskfriendlyname" -TargetStoragePoolName "$Env:storagepoolfriendlyname" | ConvertTo-Json`;
try {
result = await this.ps.exec(command, {
env: {
storagepoolfriendlyname: storagePoolFriendlyName,
friendlyname: friendlyName,
virutaldiskfriendlyname: virutalDiskFriendlyName,
},
});
return result.parsed;
} catch (err) {
throw err;
}
}
async NewVirtualDiskCloneByUniqueId(
storagePoolFriendlyName,
uniqueId,
friendlyName
) {
let command;
let result;
command = `New-VirtualDiskSnapshot -FriendlyName "$Env:friendlyname" -VirtualDiskUniqueId "$Env:uniqueid" -TargetStoragePoolName "$Env:storagepoolfriendlyname" | ConvertTo-Json`;
try {
result = await this.ps.exec(command, {
env: {
storagepoolfriendlyname: storagePoolFriendlyName,
friendlyname: friendlyName,
uniqueid: uniqueId,
},
});
return result.parsed;
} catch (err) {
throw err;
}
}
// $volumeinfo = GWMI -namespace root\cimv2 -class win32_volume
// $volumeid = $volumeinfo[1].deviceid
// $taskname = "ShadowCopyVolume" + $volumeid.replace("\","").replace("?Volume","")
// $taskrun = "C:\Windows\system32\vssadmin.exe Create Shadow /AutoRetry=15 /For=$volumeid"
// Get-CimInstance Win32_ShadowCopy | ConvertTo-Json
async VssCreateShadowByVolumeId(
storagePoolFriendlyName,
uniqueId,
friendlyName
) {
let command;
let result;
command = `New-VirtualDiskSnapshot -FriendlyName \"$Env:friendlyname" -VirtualDiskUniqueId "$Env:uniqueid" -TargetStoragePoolName "$Env:storagepoolfriendlyname" | ConvertTo-Json`;
try {
result = await this.ps.exec(command, {
env: {
storagepoolfriendlyname: storagePoolFriendlyName,
friendlyname: friendlyName,
uniqueid: uniqueId,
},
});
return result.parsed;
} catch (err) {
throw err;
}
}
async NewVHDDifferencing(parentPath, childPath) {
let command;
let result;
command =
'New-VHD -ParentPath "${Env:parentpath}" -Path "${Env:childpath}" -Differencing | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: Object.assign({}, process.env, {
parentpath: parentPath,
childpath: childPath,
}),
});
return result.parsed;
} catch (err) {
throw err;
}
}
async MountVHD(path) {
let command;
let result;
command =
'Mount-VHD -NoDriveLetter -Path "${Env:vhdpath}" | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: Object.assign({}, process.env, {
vhdpath: path,
}),
});
return result.parsed;
} catch (err) {
throw err;
}
}
async DismountVHD(path) {
let command;
let result;
command = 'Dismount-VHD -Path "${Env:vhdpath}" | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: Object.assign({}, process.env, {
vhdpath: path,
}),
});
return result.parsed;
} catch (err) {
throw err;
}
}
}
module.exports.Windows = Windows;