diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe5f63e..921aab2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -115,38 +115,6 @@ jobs: SYNOLOGY_PASSWORD: ${{ secrets.SANITY_SYNOLOGY_PASSWORD }} SYNOLOGY_VOLUME: ${{ secrets.SANITY_SYNOLOGY_VOLUME }} - csi-sanity-truenas-scale-22_12: - needs: - - build-npm-linux-amd64 - strategy: - fail-fast: false - matrix: - config: - - truenas/scale/22.12/scale-iscsi.yaml - - truenas/scale/22.12/scale-nfs.yaml - # 80 char limit - - truenas/scale/22.12/scale-smb.yaml - runs-on: - - self-hosted - - Linux - - X64 - #- csi-sanity-truenas - - csi-sanity-zfs-generic - steps: - - uses: actions/checkout@v3 - - uses: actions/download-artifact@v3 - with: - name: node-modules-linux-amd64 - - name: csi-sanity - run: | - # run tests - ci/bin/run.sh - env: - TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" - TRUENAS_HOST: ${{ secrets.SANITY_TRUENAS_SCALE_22_12_HOST }} - TRUENAS_USERNAME: ${{ secrets.SANITY_TRUENAS_USERNAME }} - TRUENAS_PASSWORD: ${{ secrets.SANITY_TRUENAS_PASSWORD }} - csi-sanity-truenas-scale-23_10: needs: - build-npm-linux-amd64 @@ -179,6 +147,38 @@ jobs: TRUENAS_USERNAME: ${{ secrets.SANITY_TRUENAS_USERNAME }} TRUENAS_PASSWORD: ${{ secrets.SANITY_TRUENAS_PASSWORD }} + csi-sanity-truenas-scale-24_04: + needs: + - build-npm-linux-amd64 + strategy: + fail-fast: false + matrix: + config: + - truenas/scale/24.04/scale-iscsi.yaml + - truenas/scale/24.04/scale-nfs.yaml + # 80 char limit + - truenas/scale/24.04/scale-smb.yaml + runs-on: + - self-hosted + - Linux + - X64 + #- csi-sanity-truenas + - csi-sanity-zfs-generic + steps: + - uses: actions/checkout@v3 + - uses: actions/download-artifact@v3 + with: + name: node-modules-linux-amd64 + - name: csi-sanity + run: | + # run tests + ci/bin/run.sh + env: + TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" + TRUENAS_HOST: ${{ secrets.SANITY_TRUENAS_SCALE_24_04_HOST }} + TRUENAS_USERNAME: ${{ secrets.SANITY_TRUENAS_USERNAME }} + TRUENAS_PASSWORD: ${{ secrets.SANITY_TRUENAS_PASSWORD }} + # ssh-based drivers csi-sanity-truenas-core-13_0: needs: @@ -244,6 +244,41 @@ jobs: SERVER_USERNAME: ${{ secrets.SANITY_ZFS_GENERIC_USERNAME }} SERVER_PASSWORD: ${{ secrets.SANITY_ZFS_GENERIC_PASSWORD }} + # client drivers + csi-sanity-objectivefs: + needs: + - build-npm-linux-amd64 + strategy: + fail-fast: false + matrix: + config: + - objectivefs/objectivefs.yaml + runs-on: + - self-hosted + - Linux + - X64 + - csi-sanity-client + steps: + - uses: actions/checkout@v3 + - uses: actions/download-artifact@v3 + with: + name: node-modules-linux-amd64 + - name: csi-sanity + run: | + # run tests + ci/bin/run.sh + env: + TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" + OBJECTIVEFS_POOL: ${{ secrets.SANITY_OBJECTIVEFS_POOL }} + OBJECTIVEFS_LICENSE: ${{ secrets.SANITY_OBJECTIVEFS_LICENSE }} + OBJECTIVEFS_OBJECTSTORE: ${{ secrets.SANITY_OBJECTIVEFS_OBJECTSTORE }} + OBJECTIVEFS_ENDPOINT: ${{ secrets.SANITY_OBJECTIVEFS_ENDPOINT }} + OBJECTIVEFS_SECRET_KEY: ${{ secrets.SANITY_OBJECTIVEFS_SECRET_KEY }} + OBJECTIVEFS_ACCESS_KEY: ${{ secrets.SANITY_OBJECTIVEFS_ACCESS_KEY }} + OBJECTIVEFS_PASSPHRASE: ${{ secrets.SANITY_OBJECTIVEFS_PASSPHRASE }} + + CSI_SANITY_SKIP: "should fail when requesting to create a snapshot with already existing name and different source volume ID|should fail when requesting to create a volume with already existing name and different capacity" + # client drivers csi-sanity-client: needs: @@ -425,10 +460,11 @@ jobs: - determine-image-tag - csi-sanity-synology-dsm6 - csi-sanity-synology-dsm7 - - csi-sanity-truenas-scale-22_12 - csi-sanity-truenas-scale-23_10 + - csi-sanity-truenas-scale-24_04 - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic + - csi-sanity-objectivefs - csi-sanity-client - csi-sanity-client-windows - csi-sanity-zfs-local @@ -464,10 +500,11 @@ jobs: needs: - csi-sanity-synology-dsm6 - csi-sanity-synology-dsm7 - - csi-sanity-truenas-scale-22_12 - csi-sanity-truenas-scale-23_10 + - csi-sanity-truenas-scale-24_04 - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic + - csi-sanity-objectivefs - csi-sanity-client - csi-sanity-client-windows - csi-sanity-zfs-local diff --git a/Dockerfile b/Dockerfile index f6f2076..be2e622 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y locales && rm -rf /var/lib/apt/lists/* && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 ENV LANG=en_US.utf8 -ENV NODE_VERSION=v16.18.0 +ENV NODE_VERSION=v20.11.1 ENV NODE_ENV=production # install build deps @@ -75,7 +75,7 @@ COPY --from=build /usr/local/lib/nodejs/bin/node /usr/local/bin/node # netbase is required by rpcbind/rpcinfo to work properly # /etc/{services,rpc} are required RUN apt-get update && \ - apt-get install -y netbase socat e2fsprogs exfatprogs xfsprogs btrfs-progs fatresize dosfstools ntfs-3g nfs-common cifs-utils fdisk gdisk cloud-guest-utils sudo rsync procps util-linux nvme-cli && \ + apt-get install -y wget netbase 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 fuse && \ rm -rf /var/lib/apt/lists/* # controller requirements @@ -83,6 +83,11 @@ RUN apt-get update && \ # apt-get install -y ansible && \ # rm -rf /var/lib/apt/lists/* +# install objectivefs +ENV OBJECTIVEFS_VERSION=7.1 +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 diff --git a/bin/democratic-csi b/bin/democratic-csi index 52e0a34..cfc8c33 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -397,10 +397,58 @@ logger.info( bindSocket ); -[`SIGINT`, `SIGUSR1`, `SIGUSR2`, `uncaughtException`, `SIGTERM`].forEach( +const signalMapping = { + 1: "SIGHUP", + 2: "SIGINT", + 3: "SIGQUIT", + 4: "SIGILL", + 5: "SIGTRAP", + 6: "SIGABRT", + 7: "SIGEMT", + 8: "SIGFPE", + 9: "SIGKILL", + 10: "SIGBUS", + 11: "SIGSEGV", + 12: "SIGSYS", + 13: "SIGPIPE", + 14: "SIGALRM", + 15: "SIGTERM", + 16: "SIGURG", + 17: "SIGSTOP", + 18: "SIGTSTP", + 19: "SIGCONT", + 20: "SIGCHLD", + 21: "SIGTTIN", + 22: "SIGTTOU", + 23: "SIGIO", + 24: "SIGXCPU", + 25: "SIGXFSZ", + 26: "SIGVTALRM", + 27: "SIGPROF", + 28: "SIGWINCH", + 29: "SIGINFO", + 30: "SIGUSR1", + 31: "SIGUSR2", +}; + +[(`SIGINT`, `SIGUSR1`, `SIGUSR2`, `uncaughtException`, `SIGTERM`)].forEach( (eventType) => { process.on(eventType, async (code) => { - console.log(`running server shutdown, exit code: ${code}`); + let codeNumber = null; + let codeName = null; + if (code > 0) { + codeNumber = code; + codeName = signalMapping[code]; + } else { + codeNumber = Object.keys(signalMapping).find( + (key) => signalMapping[key] === code + ); + codeName = code; + } + + console.log( + `running server shutdown, exit code: ${codeNumber} (${codeName})` + ); // attempt clean shutdown of in-flight requests try { @@ -431,7 +479,7 @@ logger.info( } console.log("server fully shutdown, exiting"); - process.exit(code); + process.exit(codeNumber); }); } ); diff --git a/ci/configs/objectivefs/objectivefs.yaml b/ci/configs/objectivefs/objectivefs.yaml new file mode 100644 index 0000000..4d3c801 --- /dev/null +++ b/ci/configs/objectivefs/objectivefs.yaml @@ -0,0 +1,19 @@ +driver: objectivefs + +objectivefs: + pool: ${OBJECTIVEFS_POOL} + cli: + sudoEnabled: false + env: + OBJECTIVEFS_LICENSE: ${OBJECTIVEFS_LICENSE} + OBJECTSTORE: ${OBJECTIVEFS_OBJECTSTORE} + ENDPOINT: ${OBJECTIVEFS_ENDPOINT} + SECRET_KEY: ${OBJECTIVEFS_SECRET_KEY} + ACCESS_KEY: ${OBJECTIVEFS_ACCESS_KEY} + OBJECTIVEFS_PASSPHRASE: ${OBJECTIVEFS_PASSPHRASE} + +_private: + csi: + volume: + idHash: + strategy: crc32 diff --git a/ci/configs/truenas/scale/24.04/scale-iscsi.yaml b/ci/configs/truenas/scale/24.04/scale-iscsi.yaml new file mode 100644 index 0000000..a2f7d04 --- /dev/null +++ b/ci/configs/truenas/scale/24.04/scale-iscsi.yaml @@ -0,0 +1,38 @@ +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 diff --git a/ci/configs/truenas/scale/24.04/scale-nfs.yaml b/ci/configs/truenas/scale/24.04/scale-nfs.yaml new file mode 100644 index 0000000..42818ae --- /dev/null +++ b/ci/configs/truenas/scale/24.04/scale-nfs.yaml @@ -0,0 +1,29 @@ +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: "" diff --git a/ci/configs/truenas/scale/24.04/scale-smb.yaml b/ci/configs/truenas/scale/24.04/scale-smb.yaml new file mode 100644 index 0000000..2a8861e --- /dev/null +++ b/ci/configs/truenas/scale/24.04/scale-smb.yaml @@ -0,0 +1,50 @@ +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 diff --git a/docker/objectivefs-installer.sh b/docker/objectivefs-installer.sh new file mode 100755 index 0000000..1f5e74f --- /dev/null +++ b/docker/objectivefs-installer.sh @@ -0,0 +1,34 @@ +#!/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 OBJECTIVEFS_ARCH="amd64" +elif [ "$PLATFORM" = "linux/arm64" ]; then + export OBJECTIVEFS_ARCH="arm64" +else + echo "unsupported/unknown PLATFORM ${PLATFORM}" +fi + +export DEB_FILE="objectivefs_${OBJECTIVEFS_VERSION}_${OBJECTIVEFS_ARCH}.deb" + +echo "I am installing objectivefs $OBJECTIVEFS_VERSION" + +wget "https://objectivefs.com/user/download/ac24htfht/${DEB_FILE}" +dpkg -i "${DEB_FILE}" + +rm "${DEB_FILE}" diff --git a/package-lock.json b/package-lock.json index 73dbf66..bd38910 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "democratic-csi", - "version": "1.8.4", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "democratic-csi", - "version": "1.8.4", + "version": "1.9.0", "license": "MIT", "dependencies": { "@grpc/grpc-js": "^1.8.4", @@ -85,9 +85,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -108,18 +108,18 @@ } }, "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@grpc/grpc-js": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.9.tgz", - "integrity": "sha512-vQ1qwi/Kiyprt+uhb1+rHMpyk4CVRMTGNUGGPRGS7pLNfWkdCHrGEnT6T3/JyC2VZgoOX/X1KwdoU0WYQAeYcQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.1.tgz", + "integrity": "sha512-55ONqFytZExfOIjF1RjXPcVmT/jJqFzbbDqxK9jmRV4nxiYWtL9hENSW1Jfx0SdZfrvoqd44YJ/GJTqfRrawSQ==", "dependencies": { "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" @@ -146,13 +146,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -173,9 +173,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@kubernetes/client-node": { @@ -205,9 +205,9 @@ } }, "node_modules/@kubernetes/client-node/node_modules/@types/node": { - "version": "18.18.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", - "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", + "version": "18.19.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.18.tgz", + "integrity": "sha512-80CP7B8y4PzZF0GWx15/gVWRrB5y/bIjNI84NK3cmQJu0WZwvmj2WMA5LcofQFVfLqqCSp545+U2LsrVzX36Zg==", "dependencies": { "undici-types": "~5.26.4" } @@ -312,9 +312,9 @@ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" }, "node_modules/@types/node": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", - "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "dependencies": { "undici-types": "~5.26.4" } @@ -341,9 +341,9 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, "node_modules/@types/ws": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", - "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dependencies": { "@types/node": "*" } @@ -355,9 +355,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -439,9 +439,9 @@ "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" }, "node_modules/async-mutex": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz", - "integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", "dependencies": { "tslib": "^2.4.0" } @@ -465,11 +465,11 @@ "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, "node_modules/axios": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", - "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -818,9 +818,9 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } @@ -838,16 +838,16 @@ } }, "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -1017,9 +1017,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -1059,9 +1059,9 @@ } }, "node_modules/flat-cache": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", - "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { "flatted": "^3.2.9", @@ -1069,13 +1069,13 @@ "rimraf": "^3.0.2" }, "engines": { - "node": ">=12.0.0" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/fn.name": { @@ -1084,9 +1084,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -1124,9 +1124,9 @@ } }, "node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1211,9 +1211,9 @@ } }, "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1301,9 +1301,9 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -1688,9 +1688,9 @@ } }, "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "optional": true, "engines": { "node": "*" @@ -1817,12 +1817,12 @@ } }, "node_modules/openid-client": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", - "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.4.tgz", + "integrity": "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==", "optional": true, "dependencies": { - "jose": "^4.15.1", + "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -1982,9 +1982,9 @@ } }, "node_modules/protobufjs": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", - "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -2243,9 +2243,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -2305,9 +2305,9 @@ } }, "node_modules/ssh2": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.14.0.tgz", - "integrity": "sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz", + "integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==", "hasInstallScript": true, "dependencies": { "asn1": "^0.2.6", @@ -2317,8 +2317,8 @@ "node": ">=10.16.0" }, "optionalDependencies": { - "cpu-features": "~0.0.8", - "nan": "^2.17.0" + "cpu-features": "~0.0.9", + "nan": "^2.18.0" } }, "node_modules/sshpk": { @@ -2644,9 +2644,9 @@ } }, "node_modules/winston-transport": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.6.0.tgz", - "integrity": "sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", + "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", @@ -2691,9 +2691,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 6b4d1ad..099fd4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "democratic-csi", - "version": "1.8.4", + "version": "1.9.0", "description": "kubernetes csi driver framework", "main": "bin/democratic-csi", "scripts": { diff --git a/src/driver/controller-objectivefs/index.js b/src/driver/controller-objectivefs/index.js new file mode 100644 index 0000000..4aff227 --- /dev/null +++ b/src/driver/controller-objectivefs/index.js @@ -0,0 +1,740 @@ +const _ = require("lodash"); +const { CsiBaseDriver } = require("../index"); +const { GrpcError, grpc } = require("../../utils/grpc"); +const GeneralUtils = require("../../utils/general"); +const { ObjectiveFS } = require("../../utils/objectivefs"); +const registry = require("../../utils/registry"); +const semver = require("semver"); +const uuidv4 = require("uuid").v4; + +const __REGISTRY_NS__ = "ControllerZfsLocalDriver"; +const MAX_VOLUME_NAME_LENGTH = 63; + +class ControllerObjectiveFSDriver 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"); + } + } + } + + async getObjectiveFSClient() { + const driver = this; + return registry.getAsync( + `${__REGISTRY_NS__}:objectivefsclient`, + async () => { + const options = {}; + options.sudo = _.get( + driver.options, + "objectivefs.cli.sudoEnabled", + false + ); + + return new ObjectiveFS({ + ...options, + env: _.get(driver.options, "objectivefs.env", {}), + }); + } + ); + } + + /** + * + * @returns Array + */ + getAccessModes(capability) { + let access_modes = _.get(this.options, "csi.access_modes", null); + if (access_modes !== null) { + return access_modes; + } + + access_modes = [ + "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", + "MULTI_NODE_READER_ONLY", + "MULTI_NODE_SINGLE_WRITER", + "MULTI_NODE_MULTI_WRITER", + ]; + + if ( + capability.access_type == "block" && + !access_modes.includes("MULTI_NODE_MULTI_WRITER") + ) { + access_modes.push("MULTI_NODE_MULTI_WRITER"); + } + + return access_modes; + } + + getFsTypes() { + return ["fuse.objectivefs", "objectivefs"]; + } + + assertCapabilities(capabilities) { + const driver = this; + this.ctx.logger.verbose("validating capabilities: %j", capabilities); + + let message = null; + let fs_types = driver.getFsTypes(); + const valid = capabilities.every((capability) => { + if (capability.access_type != "mount") { + message = `invalid access_type ${capability.access_type}`; + return false; + } + + if ( + capability.mount.fs_type && + !fs_types.includes(capability.mount.fs_type) + ) { + message = `invalid fs_type ${capability.mount.fs_type}`; + return false; + } + + if ( + !this.getAccessModes(capability).includes(capability.access_mode.mode) + ) { + message = `invalid access_mode, ${capability.access_mode.mode}`; + return false; + } + + return true; + }); + + return { valid, message }; + } + + async getVolumeStatus(entry) { + const driver = this; + const object_store = _.get(driver.options, "objectivefs.env.OBJECTSTORE"); + const volume_id = entry.NAME.replace(object_store, "").split("/")[1]; + + if (!!!semver.satisfies(driver.ctx.csiVersion, ">=1.2.0")) { + return; + } + + let abnormal = false; + let message = "OK"; + let volume_status = {}; + + //LIST_VOLUMES_PUBLISHED_NODES + if ( + semver.satisfies(driver.ctx.csiVersion, ">=1.2.0") && + driver.options.service.controller.capabilities.rpc.includes( + "LIST_VOLUMES_PUBLISHED_NODES" + ) + ) { + // TODO: let drivers fill this in + volume_status.published_node_ids = []; + } + + //VOLUME_CONDITION + if ( + semver.satisfies(driver.ctx.csiVersion, ">=1.3.0") && + driver.options.service.controller.capabilities.rpc.includes( + "VOLUME_CONDITION" + ) + ) { + // TODO: let drivers fill ths in + volume_condition = { abnormal, message }; + volume_status.volume_condition = volume_condition; + } + + return volume_status; + } + + async populateCsiVolumeFromData(entry) { + const driver = this; + const object_store = _.get(driver.options, "objectivefs.env.OBJECTSTORE"); + let filesystem = entry.NAME.replace(object_store, ""); + + let volume_content_source; + let volume_context = { + provisioner_driver: driver.options.driver, + node_attach_driver: "objectivefs", + filesystem, + object_store, + "env.OBJECTSTORE": object_store, + }; + + if (driver.options.instance_id) { + volume_context["provisioner_driver_instance_id"] = + driver.options.instance_id; + } + let accessible_topology; + + let volume = { + volume_id: filesystem.split("/")[1], + capacity_bytes: 0, + content_source: volume_content_source, + volume_context, + accessible_topology, + }; + + return volume; + } + + /** + * Ensure sane options are used etc + * true = ready + * false = not ready, but progressiong towards ready + * throw error = faulty setup + * + * @param {*} call + */ + async Probe(call) { + const driver = this; + const pool = _.get(driver.options, "objectivefs.pool"); + const object_store = _.get(driver.options, "objectivefs.env.OBJECTSTORE"); + + if (driver.ctx.args.csiMode.includes("controller")) { + if (!pool) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `objectivefs.pool not configured` + ); + } + + if (!object_store) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `env.OBJECTSTORE not configured` + ); + } + + return { ready: { value: true } }; + } else { + return { ready: { value: true } }; + } + } + + /** + * Create an objectivefs filesystem as a new volume + * + * @param {*} call + */ + async CreateVolume(call) { + const driver = this; + const ofsClient = await driver.getObjectiveFSClient(); + const pool = _.get(driver.options, "objectivefs.pool"); + const object_store = _.get(driver.options, "objectivefs.env.OBJECTSTORE"); + const parameters = call.request.parameters; + + if (!pool) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `objectivefs.pool not configured` + ); + } + + if (!object_store) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `env.OBJECTSTORE not configured` + ); + } + + const context_env = {}; + for (const key in parameters) { + if (key.startsWith("env.")) { + context_env[key] = parameters[key]; + } + } + context_env["env.OBJECTSTORE"] = object_store; + + // filesystem names are always lower-cased by ofs + let volume_id = await driver.getVolumeIdFromCall(call); + let volume_content_source = call.request.volume_content_source; + volume_id = volume_id.toLowerCase(); + const filesystem = `${pool}/${volume_id}`; + + if (volume_id.length >= MAX_VOLUME_NAME_LENGTH) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `derived volume_id ${volume_id} is too long for objectivefs` + ); + } + + if ( + call.request.volume_capabilities && + call.request.volume_capabilities.length > 0 + ) { + const result = this.assertCapabilities(call.request.volume_capabilities); + if (result.valid !== true) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); + } + } else { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + "missing volume_capabilities" + ); + } + + if ( + !call.request.capacity_range || + Object.keys(call.request.capacity_range).length === 0 + ) { + call.request.capacity_range = { + required_bytes: 1073741824, // meaningless + }; + } + + if ( + call.request.capacity_range.required_bytes > 0 && + call.request.capacity_range.limit_bytes > 0 && + call.request.capacity_range.required_bytes > + call.request.capacity_range.limit_bytes + ) { + throw new GrpcError( + grpc.status.OUT_OF_RANGE, + `required_bytes is greather than limit_bytes` + ); + } + + let capacity_bytes = + call.request.capacity_range.required_bytes || + call.request.capacity_range.limit_bytes; + + if (!capacity_bytes) { + //should never happen, value must be set + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `volume capacity is required (either required_bytes or limit_bytes)` + ); + } + + // ensure *actual* capacity is not greater than limit + if ( + call.request.capacity_range.limit_bytes && + call.request.capacity_range.limit_bytes > 0 && + capacity_bytes > call.request.capacity_range.limit_bytes + ) { + throw new GrpcError( + grpc.status.OUT_OF_RANGE, + `required volume capacity is greater than limit` + ); + } + + if (volume_content_source) { + //should never happen, cannot clone with this driver + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `cloning is not enabled` + ); + } + + await ofsClient.create({}, filesystem, ["-f"]); + + let volume_context = { + provisioner_driver: driver.options.driver, + node_attach_driver: "objectivefs", + filesystem, + ...context_env, + }; + + if (driver.options.instance_id) { + volume_context["provisioner_driver_instance_id"] = + driver.options.instance_id; + } + + const res = { + volume: { + volume_id, + //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 + capacity_bytes: 0, + content_source: volume_content_source, + volume_context, + }, + }; + + return res; + } + + /** + * Delete a volume + * + * Deleting a volume consists of the following steps: + * 1. delete directory + * + * @param {*} call + */ + async DeleteVolume(call) { + const driver = this; + const ofsClient = await driver.getObjectiveFSClient(); + const pool = _.get(driver.options, "objectivefs.pool"); + + let volume_id = call.request.volume_id; + if (!volume_id) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `volume_id is required` + ); + } + + volume_id = volume_id.toLowerCase(); + const filesystem = `${pool}/${volume_id}`; + await ofsClient.destroy({}, filesystem, []); + + return {}; + } + + /** + * + * @param {*} call + */ + async ControllerExpandVolume(call) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `operation not supported by driver` + ); + } + + /** + * TODO: consider volume_capabilities? + * + * @param {*} call + */ + async GetCapacity(call) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `operation not supported by driver` + ); + } + + /** + * + * TODO: check capability to ensure not asking about block volumes + * + * @param {*} call + */ + async ListVolumes(call) { + const driver = this; + const ofsClient = await driver.getObjectiveFSClient(); + const pool = _.get(driver.options, "objectivefs.pool"); + + let entries = []; + let entries_length = 0; + let next_token; + let uuid; + let response; + + const max_entries = call.request.max_entries; + const starting_token = call.request.starting_token; + + // get data from cache and return immediately + if (starting_token) { + let parts = starting_token.split(":"); + uuid = parts[0]; + let start_position = parseInt(parts[1]); + let end_position; + if (max_entries > 0) { + end_position = start_position + max_entries; + } + entries = this.ctx.cache.get(`ListVolumes:result:${uuid}`); + if (entries) { + entries_length = entries.length; + entries = entries.slice(start_position, end_position); + if (max_entries > 0 && end_position > entries_length) { + next_token = `${uuid}:${end_position}`; + } else { + next_token = null; + } + const data = { + entries: entries, + next_token: next_token, + }; + + return data; + } else { + throw new GrpcError( + grpc.status.ABORTED, + `invalid starting_token: ${starting_token}` + ); + } + } + + entries = []; + const list_entries = await ofsClient.list({}); + for (const entry of list_entries) { + if (entry.KIND != "ofs") { + continue; + } + + let volume = await driver.populateCsiVolumeFromData(entry); + if (volume) { + let status = await driver.getVolumeStatus(entry); + entries.push({ + volume, + status, + }); + } + } + + if (max_entries && entries.length > max_entries) { + uuid = uuidv4(); + this.ctx.cache.set(`ListVolumes:result:${uuid}`, entries); + next_token = `${uuid}:${max_entries}`; + entries = entries.slice(0, max_entries); + } + + const data = { + entries: entries, + next_token: next_token, + }; + + return data; + } + + /** + * + * @param {*} call + */ + async ListSnapshots(call) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `operation not supported by driver` + ); + } + + /** + * + * @param {*} call + */ + async CreateSnapshot(call) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `operation not supported by driver` + ); + + const driver = this; + + // both these are required + let source_volume_id = call.request.source_volume_id; + let name = call.request.name; + + if (!source_volume_id) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `snapshot source_volume_id is required` + ); + } + + source_volume_id = source_volume_id.toLowerCase(); + + if (!name) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `snapshot name is required` + ); + } + + driver.ctx.logger.verbose("requested snapshot name: %s", name); + + let invalid_chars; + invalid_chars = name.match(/[^a-z0-9_\-:.+]+/gi); + if (invalid_chars) { + invalid_chars = String.prototype.concat( + ...new Set(invalid_chars.join("")) + ); + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `snapshot name contains invalid characters: ${invalid_chars}` + ); + } + + // https://stackoverflow.com/questions/32106243/regex-to-remove-all-non-alpha-numeric-and-replace-spaces-with/32106277 + name = name.replace(/[^a-z0-9_\-:.+]+/gi, ""); + + driver.ctx.logger.verbose("cleansed snapshot name: %s", name); + + const snapshot_id = `${source_volume_id}-${name}`; + const volume_path = driver.getControllerVolumePath(source_volume_id); + const snapshot_path = driver.getControllerSnapshotPath(snapshot_id); + + // do NOT overwrite existing snapshot + if (!(await driver.directoryExists(snapshot_path))) { + await driver.cloneDir(volume_path, snapshot_path); + } + + let size_bytes = await driver.getDirectoryUsage(snapshot_path); + return { + snapshot: { + /** + * The purpose of this field is to give CO guidance on how much space + * is needed to create a volume from this snapshot. + */ + size_bytes, + snapshot_id, + source_volume_id: source_volume_id, + //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto + creation_time: { + seconds: Math.round(new Date().getTime() / 1000), + nanos: 0, + }, + ready_to_use: true, + }, + }; + } + + /** + * In addition, if clones have been created from a snapshot, then they must + * be destroyed before the snapshot can be destroyed. + * + * @param {*} call + */ + async DeleteSnapshot(call) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `operation not supported by driver` + ); + + const driver = this; + + const snapshot_id = call.request.snapshot_id; + + if (!snapshot_id) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `snapshot_id is required` + ); + } + + return {}; + } + + /** + * + * @param {*} call + */ + async ValidateVolumeCapabilities(call) { + const driver = this; + const ofsClient = await driver.getObjectiveFSClient(); + const pool = _.get(driver.options, "objectivefs.pool"); + + const volume_id = call.request.volume_id; + if (!volume_id) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); + } + + const filesystem = `${pool}/${volume_id}`; + const entries = await ofsClient.list({}, filesystem); + const exists = entries.some((entry) => { + return entry.NAME.endsWith(filesystem) && entry.KIND == "ofs"; + }); + + if (!exists) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `invalid volume_id: ${volume_id}` + ); + } + + const capabilities = call.request.volume_capabilities; + if (!capabilities || capabilities.length === 0) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capabilities`); + } + + 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.ControllerObjectiveFSDriver = ControllerObjectiveFSDriver; diff --git a/src/driver/factory.js b/src/driver/factory.js index ea62157..0235758 100644 --- a/src/driver/factory.js +++ b/src/driver/factory.js @@ -12,6 +12,7 @@ const { const { ControllerNfsClientDriver } = require("./controller-nfs-client"); const { ControllerSmbClientDriver } = require("./controller-smb-client"); const { ControllerLustreClientDriver } = require("./controller-lustre-client"); +const { ControllerObjectiveFSDriver } = require("./controller-objectivefs"); const { ControllerSynologyDriver } = require("./controller-synology"); const { NodeManualDriver } = require("./node-manual"); @@ -50,6 +51,8 @@ function factory(ctx, options) { return new ControllerLocalHostpathDriver(ctx, options); case "lustre-client": return new ControllerLustreClientDriver(ctx, options); + case "objectivefs": + return new ControllerObjectiveFSDriver(ctx, options); case "node-manual": return new NodeManualDriver(ctx, options); default: diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 3517503..42087c5 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -183,8 +183,17 @@ class FreeNASApiDriver extends CsiBaseDriver { const apiVersion = httpClient.getApiVersion(); const zb = await this.getZetabyte(); const truenasVersion = semver.coerce( - await httpApiClient.getSystemVersionMajorMinor() + await httpApiClient.getSystemVersionMajorMinor(), + { loose: true } ); + + if (!truenasVersion) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unable to detect TrueNAS version` + ); + } + const isScale = await httpApiClient.getIsScale(); let volume_context; diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 7535eff..3bb5635 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -231,8 +231,17 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { const apiVersion = httpClient.getApiVersion(); const zb = await this.getZetabyte(); const truenasVersion = semver.coerce( - await httpApiClient.getSystemVersionMajorMinor() + await httpApiClient.getSystemVersionMajorMinor(), + { loose: true } ); + + if (!truenasVersion) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unable to detect TrueNAS version` + ); + } + const isScale = await httpApiClient.getIsScale(); let volume_context; @@ -1996,7 +2005,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { this.ctx.logger.debug("zfs props data: %j", properties); let iscsiName = properties[FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME].value; - + // name correlates to the extent NOT the target let kName = iscsiName.replaceAll(".", "_"); diff --git a/src/driver/index.js b/src/driver/index.js index bac0290..566d5b7 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -7,6 +7,7 @@ const k8s = require("@kubernetes/client-node"); const { GrpcError, grpc } = require("../utils/grpc"); const Handlebars = require("handlebars"); const { Mount } = require("../utils/mount"); +const { ObjectiveFS } = require("../utils/objectivefs"); const { OneClient } = require("../utils/oneclient"); const { Filesystem } = require("../utils/filesystem"); const { ISCSI } = require("../utils/iscsi"); @@ -181,6 +182,15 @@ class CsiBaseDriver { }); } + getDefaultObjectiveFSInstance() { + return registry.get( + `${__REGISTRY_NS__}:default_objectivefs_instance`, + () => { + return new ObjectiveFS(); + } + ); + } + /** * * @returns CsiProxyClient @@ -456,6 +466,9 @@ class CsiBaseDriver { /** * technically zfs allows `:` and `.` in addition to `_` and `-` + * TODO: make this more specific to each driver + * in particular Nomad per-alloc feature uses names with -[] syntax so square brackets are present + * TODO: allow for replacing chars vs absolute failure? */ let invalid_chars; invalid_chars = volume_id.match(/[^a-z0-9_\-]/gi); @@ -728,6 +741,7 @@ class CsiBaseDriver { } switch (node_attach_driver) { + case "objectivefs": case "oneclient": // move along break; @@ -1246,6 +1260,65 @@ class CsiBaseDriver { return {}; } + break; + case "objectivefs": + let objectivefs = driver.getDefaultObjectiveFSInstance(); + let ofs_filesystem = volume_context.filesystem; + let env = {}; + for (const key in volume_context) { + if (key.startsWith("env.")) { + env[key.substr("env.".length)] = volume_context[key]; + } + } + + for (const key in normalizedSecrets) { + if (key.startsWith("env.")) { + env[key.substr("env.".length)] = normalizedSecrets[key]; + } + } + + let ofs_object_store = env["OBJECTSTORE"]; + + if (!ofs_filesystem) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `missing ofs volume filesystem` + ); + } + + if (!ofs_object_store) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `missing required ofs volume env.OBJECTSTORE` + ); + } + + device = `${ofs_object_store}${ofs_filesystem}`; + result = await mount.deviceIsMountedAtPath( + device, + staging_target_path + ); + + if (result) { + return {}; + } + + result = await objectivefs.mount( + env, + ofs_filesystem, + staging_target_path, + mount_flags + ); + + if (result) { + return {}; + } + + throw new GrpcError( + grpc.status.UNKNOWN, + `failed to mount objectivefs: ${device}` + ); + break; case "oneclient": let oneclient = driver.getDefaultOneClientInstance(); @@ -2932,6 +3005,7 @@ class CsiBaseDriver { case "nfs": case "smb": case "lustre": + case "objectivefs": case "oneclient": case "hostpath": case "iscsi": diff --git a/src/driver/node-manual/index.js b/src/driver/node-manual/index.js index 96d6722..c55915e 100644 --- a/src/driver/node-manual/index.js +++ b/src/driver/node-manual/index.js @@ -121,6 +121,10 @@ class NodeManualDriver extends CsiBaseDriver { driverResourceType = "filesystem"; fs_types = ["lustre"]; break; + case "objectivefs": + driverResourceType = "filesystem"; + fs_types = ["objectivefs", "fuse.objectivefs"]; + break; case "oneclient": driverResourceType = "filesystem"; fs_types = ["oneclient", "fuse.oneclient"]; diff --git a/src/utils/objectivefs.js b/src/utils/objectivefs.js new file mode 100644 index 0000000..e9feada --- /dev/null +++ b/src/utils/objectivefs.js @@ -0,0 +1,320 @@ +const cp = require("child_process"); + +const DEFAULT_TIMEOUT = process.env.MOUNT_DEFAULT_TIMEOUT || 30000; + +const EXIT_CODE_64 = "administrator can not mount filesystems"; +const EXIT_CODE_78 = "missing or invalid passphrase"; + +/** + * https://objectivefs.com/ + */ +class ObjectiveFS { + constructor(options = {}) { + const objectivefs = this; + objectivefs.options = options; + + options.paths = options.paths || {}; + if (!options.paths.objectivefs) { + options.paths.objectivefs = "mount.objectivefs"; + } + + if (!options.paths.sudo) { + options.paths.sudo = "/usr/bin/sudo"; + } + + if (!options.paths.chroot) { + options.paths.chroot = "/usr/sbin/chroot"; + } + + if (!options.env) { + options.env = {}; + } + + if (!options.executor) { + options.executor = { + spawn: cp.spawn, + //spawn: cp.execFile, + }; + } + } + + /** + * mount.objectivefs [-o [,]..] + * + * @param {*} env + * @param {*} filesystem + * @param {*} target + * @param {*} options + */ + async mount(env, filesystem, target, options = []) { + if (!env) { + env = {}; + } + const objectivefs = this; + let args = []; + args = args.concat(options); + args = args.concat([filesystem, target]); + + let result; + try { + result = await objectivefs.exec( + objectivefs.options.paths.objectivefs, + args, + { env, operation: "mount" } + ); + return result; + } catch (err) { + throw err; + } + } + + /** + * mount.objectivefs create + * mount.objectivefs create -f / + * + * @param {*} env + * @param {*} filesystem + * @param {*} options + */ + async create(env, filesystem, options = []) { + if (!env) { + env = {}; + } + const objectivefs = this; + let args = ["create"]; + args = args.concat(options); + args = args.concat([filesystem]); + + let result; + try { + result = await objectivefs.exec( + objectivefs.options.paths.objectivefs, + args, + { env } + ); + return result; + } catch (err) { + if (err.code == 1 && err.stderr.includes("filesystem already exists")) { + return; + } + throw err; + } + } + + /** + * echo 'y' | mount.objectivefs destroy / + * + * @param {*} env + * @param {*} filesystem + * @param {*} options + */ + async destroy(env, filesystem, options = []) { + if (!env) { + env = {}; + } + const objectivefs = this; + let args = ["destroy"]; + args = args.concat(options); + args = args.concat([filesystem]); + + let result; + try { + result = await objectivefs.exec( + "/bin/bash", + [ + "-c", + `echo y | ${objectivefs.options.paths.objectivefs} ${args.join(" ")}`, + ], + { env } + ); + + return result; + } catch (err) { + if ( + err.code == 68 && + err.stdout.includes("does not look like an ObjectiveFS filesystem") + ) { + return; + } + throw err; + } + } + + parseListOutput(data) { + const lines = data.split("\n"); + let headers = []; + let entries = []; + lines.forEach((line, i) => { + if (line.length < 1) { + return; + } + const parts = line.split("\t"); + if (i == 0) { + headers = parts.map((header) => { + return header.trim(); + }); + return; + } + + let entry = {}; + headers.forEach((name, index) => { + entry[name.trim()] = parts[index].trim(); + }); + + entries.push(entry); + }); + + return entries; + } + + /** + * mount.objectivefs list [-asvz] [[@