diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index b47286b..1e83022 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -181,10 +181,7 @@ class FreeNASApiDriver extends CsiBaseDriver { const httpApiClient = await this.getTrueNASHttpApiClient(); const apiVersion = httpClient.getApiVersion(); const zb = await this.getZetabyte(); - const truenasVersion = semver.coerce( - await httpApiClient.getSystemVersionMajorMinor(), - { loose: true } - ); + const truenasVersion = await httpApiClient.getSystemVersionSemver(); if (!truenasVersion) { throw new GrpcError( @@ -506,6 +503,40 @@ class FreeNASApiDriver extends CsiBaseDriver { } } + if (isScale && semver.satisfies(truenasVersion, ">=25.10")) { + let topLevelProperties = [ + "purpose", + "name", + "path", + "enabled", + "comment", + "readonly", + "browsable", + "access_based_share_enumeration", + "audit", + ]; + let disallowedOptions = ["abe"]; + share.purpose = "LEGACY_SHARE"; + share.options = { + purpose: "LEGACY_SHARE", + }; + for (const key in share) { + switch (key) { + case "options": + // ignore + break; + default: + if (!topLevelProperties.includes(key)) { + if (!disallowedOptions.includes(key)) { + share.options[key] = share[key]; + } + delete share[key]; + } + break; + } + } + } + switch (apiVersion) { case 1: endpoint = "/sharing/cifs"; @@ -2819,13 +2850,13 @@ class FreeNASApiDriver extends CsiBaseDriver { // set quota if (this.options.zfs.datasetEnableQuotas) { setProps = true; - properties.refquota = capacity_bytes; + properties.refquota = Number(capacity_bytes); } // set reserve if (this.options.zfs.datasetEnableReservation) { setProps = true; - properties.refreservation = capacity_bytes; + properties.refreservation = Number(capacity_bytes); } // quota for dataset and all children @@ -2933,7 +2964,7 @@ class FreeNASApiDriver extends CsiBaseDriver { // this should be already set, but when coming from a volume source // it may not match that of the source - properties.volsize = capacity_bytes; + properties.volsize = Number(capacity_bytes); // dedup // on, off, verify @@ -3221,17 +3252,17 @@ class FreeNASApiDriver extends CsiBaseDriver { // set quota if (this.options.zfs.datasetEnableQuotas) { setProps = true; - properties.refquota = capacity_bytes; + properties.refquota = Number(capacity_bytes); } // set reserve if (this.options.zfs.datasetEnableReservation) { setProps = true; - properties.refreservation = capacity_bytes; + properties.refreservation = Number(capacity_bytes); } break; case "volume": - properties.volsize = capacity_bytes; + properties.volsize = Number(capacity_bytes); setProps = true; // managed automatically for zvols @@ -3532,6 +3563,7 @@ class FreeNASApiDriver extends CsiBaseDriver { const httpClient = await this.getHttpClient(); const httpApiClient = await this.getTrueNASHttpApiClient(); const zb = await this.getZetabyte(); + const truenasVersion = await httpApiClient.getSystemVersionSemver(); let entries = []; let entries_length = 0; @@ -3693,11 +3725,19 @@ class FreeNASApiDriver extends CsiBaseDriver { response = await httpClient.get(endpoint, { "extra.snapshots": 1, "extra.snapshots_properties": JSON.stringify(zfsProperties), + //"extra.snapshots_properties": "null", }); if (response.statusCode == 404) { throw new Error("dataset does not exist"); } else if (response.statusCode == 200) { for (let snapshot of response.body.snapshots) { + if (semver.satisfies(truenasVersion, ">=25.10")) { + // request the snapshot because fetching properties is broken with the dataset is broken + snapshot.properties = await httpApiClient.SnapshotGet( + snapshot.id, + zfsProperties + ); + } let row = {}; for (let p in snapshot.properties) { row[p] = snapshot.properties[p].rawvalue; @@ -3716,12 +3756,20 @@ class FreeNASApiDriver extends CsiBaseDriver { response = await httpClient.get(endpoint, { "extra.snapshots": 1, "extra.snapshots_properties": JSON.stringify(zfsProperties), + //"extra.snapshots_properties": "null", }); if (response.statusCode == 404) { throw new Error("dataset does not exist"); } else if (response.statusCode == 200) { for (let child of response.body.children) { for (let snapshot of child.snapshots) { + if (semver.satisfies(truenasVersion, ">=25.10")) { + // request the snapshot because fetching properties is broken with the dataset is broken + snapshot.properties = await httpApiClient.SnapshotGet( + snapshot.id, + zfsProperties + ); + } let row = {}; for (let p in snapshot.properties) { row[p] = snapshot.properties[p].rawvalue; diff --git a/src/driver/freenas/http/api.js b/src/driver/freenas/http/api.js index 79d7311..a5bb236 100644 --- a/src/driver/freenas/http/api.js +++ b/src/driver/freenas/http/api.js @@ -1,4 +1,5 @@ const _ = require("lodash"); +const semver = require("semver"); const { sleep, stringify } = require("../../../utils/general"); const { Zetabyte } = require("../../../utils/zfs"); const { Registry } = require("../../../utils/registry"); @@ -183,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, @@ -250,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]); } } @@ -271,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; @@ -501,10 +516,17 @@ class Api { 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( @@ -529,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) { @@ -540,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)); @@ -549,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; @@ -559,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) { @@ -579,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) { @@ -607,6 +648,7 @@ class Api { 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; @@ -614,7 +656,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) { diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 7163d7f..27324a5 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -269,10 +269,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { const httpApiClient = await this.getTrueNASHttpApiClient(); const apiVersion = httpClient.getApiVersion(); const zb = await this.getZetabyte(); - const truenasVersion = semver.coerce( - await httpApiClient.getSystemVersionMajorMinor(), - { loose: true } - ); + const truenasVersion = await httpApiClient.getSystemVersionSemver(); if (!truenasVersion) { throw new GrpcError( @@ -594,6 +591,40 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { } } + if (isScale && semver.satisfies(truenasVersion, ">=25.10")) { + let topLevelProperties = [ + "purpose", + "name", + "path", + "enabled", + "comment", + "readonly", + "browsable", + "access_based_share_enumeration", + "audit", + ]; + let disallowedOptions = ["abe"]; + share.purpose = "LEGACY_SHARE"; + share.options = { + purpose: "LEGACY_SHARE", + }; + for (const key in share) { + switch (key) { + case "options": + // ignore + break; + default: + if (!topLevelProperties.includes(key)) { + if (!disallowedOptions.includes(key)) { + share.options[key] = share[key]; + } + delete share[key]; + } + break; + } + } + } + switch (apiVersion) { case 1: endpoint = "/sharing/cifs";