commit
3974268272
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,3 +2,7 @@
|
|||
node_modules
|
||||
dev
|
||||
/ci/bin/*dev*
|
||||
.vagrant
|
||||
hack/*
|
||||
!hack/run.sh
|
||||
!hack/build_push.sh
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
71
Dockerfile
71
Dockerfile
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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" ]
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 &
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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: ""
|
||||
|
|
@ -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: ""
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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: ""
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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:
|
||||
|
|
@ -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: ""
|
||||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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
|
||||
|
|
@ -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 $?
|
||||
;;
|
||||
|
||||
*)
|
||||
|
|
|
|||
17
docker/mount
17
docker/mount
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 $?
|
||||
|
|
|
|||
|
|
@ -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 $?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
14
docker/zfs
14
docker/zfs
|
|
@ -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 $?
|
||||
|
|
|
|||
14
docker/zpool
14
docker/zpool
|
|
@ -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 $?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
driver: vhd-ephemeral-inline
|
||||
vhd:
|
||||
nameTemplate: "csi-ephemeral-inline-{{ volume_id }}"
|
||||
|
|
@ -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]", ... ]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ zfs:
|
|||
|
||||
datasetProperties:
|
||||
# key: value
|
||||
snapshotProperties:
|
||||
# "org.freenas:key": "value"
|
||||
|
||||
datasetEnableQuotas: true
|
||||
datasetEnableReservation: false
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ zfs:
|
|||
|
||||
datasetProperties:
|
||||
# key: value
|
||||
snapshotProperties:
|
||||
# "org.freenas:key": "value"
|
||||
|
||||
zvolCompression:
|
||||
zvolDedup:
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(" ", "_");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue