Merge pull request #130 from democratic-csi/next

Next
This commit is contained in:
Travis Glenn Hansen 2021-12-01 22:26:50 -07:00 committed by GitHub
commit 09d55a9653
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 900 additions and 938 deletions

View File

@ -1,3 +1,15 @@
# v1.4.3
Released 2021-12-01
- more appropriate handling of `size_bytes` for snapshots
- more robust handling of `NodePublishVolume` to ensure the staging path is
actually mounted
- allow control of the `mount` / `umount` / `findmnt` command timeout via
`MOUNT_DEFAULT_TIMEOUT` env var
- minor fix for `zfs-generic-iscsi` with `targetCli` to work-around Ubuntu
18:04 bug (see #127)
# v1.4.2 # v1.4.2
Released 2021-09-29 Released 2021-09-29

14
docs/Nomad/README.md Normal file
View File

@ -0,0 +1,14 @@
# setup
```
cat <<EOF > /etc/nomad.d/csi.hcl
plugin "docker" {
config {
allow_privileged = true
volumes {
# required for bind mounting host directories
enabled = true
}
}
}
```

View File

@ -0,0 +1,43 @@
job "democratic-csi-iscsi-controller" {
datacenters = ["dc1"]
group "controller" {
task "plugin" {
driver = "docker"
config {
image = "docker.io/democraticcsi/democratic-csi:latest"
args = [
"--csi-version=1.5.0",
# must match the csi_plugin.id attribute below
"--csi-name=org.democratic-csi.iscsi",
"--driver-config-file=${NOMAD_TASK_DIR}/driver-config-file.yaml",
"--log-level=info",
"--csi-mode=controller",
"--server-socket=/csi/csi.sock",
]
}
template {
destination = "${NOMAD_TASK_DIR}/driver-config-file.yaml"
data = <<EOH
config
EOH
}
csi_plugin {
# must match --csi-name arg
id = "org.democratic-csi.iscsi"
type = "controller"
mount_dir = "/csi"
}
resources {
cpu = 500
memory = 256
}
}
}
}

View File

@ -0,0 +1,64 @@
job "democratic-csi-iscsi-node" {
datacenters = ["dc1"]
# you can run node plugins as service jobs as well, but this ensures
# that all nodes in the DC have a copy.
type = "system"
group "nodes" {
task "plugin" {
driver = "docker"
env {
CSI_NODE_ID = "${attr.unique.hostname}"
}
config {
image = "docker.io/democraticcsi/democratic-csi:latest"
args = [
"--csi-version=1.5.0",
# must match the csi_plugin.id attribute below
"--csi-name=org.democratic-csi.iscsi",
"--driver-config-file=${NOMAD_TASK_DIR}/driver-config-file.yaml",
"--log-level=info",
"--csi-mode=node",
"--server-socket=/csi/csi.sock",
]
# node plugins must run as privileged jobs because they
# mount disks to the host
privileged = true
ipc_mode = "host"
network_mode = "host"
mount {
type = "bind"
target = "/host"
source = "/"
readonly=false
}
}
template {
destination = "${NOMAD_TASK_DIR}/driver-config-file.yaml"
data = <<EOH
config
EOH
}
csi_plugin {
# must match --csi-name arg
id = "org.democratic-csi.iscsi"
type = "node"
mount_dir = "/csi"
}
resources {
cpu = 500
memory = 256
}
}
}
}

View File

@ -0,0 +1,43 @@
job "democratic-csi-nfs-controller" {
datacenters = ["dc1"]
group "controller" {
task "plugin" {
driver = "docker"
config {
image = "docker.io/democraticcsi/democratic-csi:latest"
args = [
"--csi-version=1.5.0",
# must match the csi_plugin.id attribute below
"--csi-name=org.democratic-csi.nfs",
"--driver-config-file=${NOMAD_TASK_DIR}/driver-config-file.yaml",
"--log-level=info",
"--csi-mode=controller",
"--server-socket=/csi/csi.sock",
]
}
template {
destination = "${NOMAD_TASK_DIR}/driver-config-file.yaml"
data = <<EOH
config
EOH
}
csi_plugin {
# must match --csi-name arg
id = "org.democratic-csi.nfs"
type = "controller"
mount_dir = "/csi"
}
resources {
cpu = 500
memory = 256
}
}
}
}

View File

@ -0,0 +1,57 @@
job "democratic-csi-nfs-node" {
datacenters = ["dc1"]
# you can run node plugins as service jobs as well, but this ensures
# that all nodes in the DC have a copy.
type = "system"
group "nodes" {
task "plugin" {
driver = "docker"
env {
CSI_NODE_ID = "${attr.unique.hostname}"
}
config {
image = "docker.io/democraticcsi/democratic-csi:latest"
args = [
"--csi-version=1.5.0",
# must match the csi_plugin.id attribute below
"--csi-name=org.democratic-csi.nfs",
"--driver-config-file=${NOMAD_TASK_DIR}/driver-config-file.yaml",
"--log-level=info",
"--csi-mode=node",
"--server-socket=/csi/csi.sock",
]
# node plugins must run as privileged jobs because they
# mount disks to the host
privileged = true
ipc_mode = "host"
network_mode = "host"
}
template {
destination = "${NOMAD_TASK_DIR}/driver-config-file.yaml"
data = <<EOH
config
EOH
}
csi_plugin {
# must match --csi-name arg
id = "org.democratic-csi.nfs"
type = "node"
mount_dir = "/csi"
}
resources {
cpu = 500
memory = 256
}
}
}
}

View File

@ -0,0 +1,66 @@
job "mysql-server" {
datacenters = ["dc1"]
type = "service"
group "mysql-server" {
count = 1
volume "mysql" {
type = "csi"
read_only = false
# iscsi
source = "csi-volume-iscsi"
access_mode = "single-node-writer"
attachment_mode = "file-system"
# nfs
#source = "csi-volume-nfs"
#access_mode = "multi-node-multi-writer"
#attachment_mode = "file-system"
}
network {
port "db" {
static = 3306
}
}
restart {
attempts = 10
interval = "5m"
delay = "25s"
mode = "delay"
}
task "mysql-server" {
driver = "docker"
volume_mount {
volume = "mysql"
destination = "/srv"
read_only = false
}
env {
MYSQL_ROOT_PASSWORD = "password"
}
config {
image = "hashicorp/mysql-portworx-demo:latest"
args = ["--datadir", "/srv/mysql"]
ports = ["db"]
}
resources {
cpu = 500
memory = 1024
}
service {
name = "mysql-server"
port = "db"
}
}
}
}

View File

@ -0,0 +1,11 @@
type = "csi"
id = "csi-volume-iscsi"
name = "csi-volume-iscsi"
plugin_id = "org.democratic-csi.iscsi"
capacity_min = "1GiB"
capacity_max = "1GiB"
capability {
access_mode = "single-node-writer"
attachment_mode = "file-system"
}

View File

@ -0,0 +1,11 @@
type = "csi"
id = "csi-volume-nfs"
name = "csi-volume-nfs"
plugin_id = "org.democratic-csi.nfs"
capacity_min = "1GiB"
capacity_max = "1GiB"
capability {
access_mode = "multi-node-multi-writer"
attachment_mode = "file-system"
}

1208
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "democratic-csi", "name": "democratic-csi",
"version": "1.4.2", "version": "1.4.3",
"description": "kubernetes csi driver framework", "description": "kubernetes csi driver framework",
"main": "bin/democratic-csi", "main": "bin/democratic-csi",
"scripts": { "scripts": {
@ -20,7 +20,7 @@
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.3.6", "@grpc/grpc-js": "^1.3.6",
"@grpc/proto-loader": "^0.6.0", "@grpc/proto-loader": "^0.6.0",
"@kubernetes/client-node": "^0.15.1", "@kubernetes/client-node": "^0.16.1",
"async-mutex": "^0.3.1", "async-mutex": "^0.3.1",
"bunyan": "^1.8.15", "bunyan": "^1.8.15",
"grpc-uds": "^0.1.6", "grpc-uds": "^0.1.6",
@ -38,6 +38,6 @@
"yargs": "^17.0.1" "yargs": "^17.0.1"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.22.0" "eslint": "^8.1.0"
} }
} }

View File

@ -228,6 +228,12 @@ class ControllerClientCommonDriver extends CsiBaseDriver {
return this.getControllerSnapshotBasePath() + "/" + snapshot_id; return this.getControllerSnapshotBasePath() + "/" + snapshot_id;
} }
async getDirectoryUsage(path) {
let result = await this.exec("du", ["-s", "--block-size=1", path]);
let size = result.stdout.split("\t", 1)[0];
return size;
}
exec(command, args, options = {}) { exec(command, args, options = {}) {
args = args || []; args = args || [];
@ -664,13 +670,14 @@ class ControllerClientCommonDriver extends CsiBaseDriver {
await driver.cloneDir(volume_path, snapshot_path); await driver.cloneDir(volume_path, snapshot_path);
} }
let size_bytes = await driver.getDirectoryUsage(snapshot_path);
return { return {
snapshot: { snapshot: {
/** /**
* The purpose of this field is to give CO guidance on how much space * The purpose of this field is to give CO guidance on how much space
* is needed to create a volume from this snapshot. * is needed to create a volume from this snapshot.
*/ */
size_bytes: 0, size_bytes,
snapshot_id, snapshot_id,
source_volume_id: source_volume_id, source_volume_id: source_volume_id,
//https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto

View File

@ -20,9 +20,10 @@ class SynologyHttpClient {
async login() { async login() {
if (!this.sid) { if (!this.sid) {
// See https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf
const data = { const data = {
api: "SYNO.API.Auth", api: "SYNO.API.Auth",
version: "2", version: "6",
method: "login", method: "login",
account: this.options.username, account: this.options.username,
passwd: this.options.password, passwd: this.options.password,

View File

@ -830,7 +830,7 @@ class ControllerSynologyDriver extends CsiBaseDriver {
`invalid source_volume_id: ${source_volume_id}` `invalid source_volume_id: ${source_volume_id}`
); );
} }
// check for other snapshopts with the same name on other volumes and fail as appropriate // check for other snapshopts with the same name on other volumes and fail as appropriate
// TODO: technically this should only be checking lun/snapshots relevant to this specific install of the driver // TODO: technically this should only be checking lun/snapshots relevant to this specific install of the driver
// but alas an isolation/namespacing mechanism does not exist in synology // but alas an isolation/namespacing mechanism does not exist in synology
@ -845,47 +845,35 @@ class ControllerSynologyDriver extends CsiBaseDriver {
} }
// check for already exists // check for already exists
let snapshot = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name); let snapshot;
if (snapshot) { snapshot = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name);
return { if (!snapshot) {
snapshot: { let data = Object.assign({}, driver.options.iscsi.lunSnapshotTemplate, {
/** src_lun_uuid: lun.uuid,
* The purpose of this field is to give CO guidance on how much space taken_by: "democratic-csi",
* is needed to create a volume from this snapshot. description: name, //check
*/ });
size_bytes: 0,
snapshot_id: `/lun/${lun.lun_id}/${snapshot.uuid}`, // add shanpshot_uuid //fixme await httpClient.CreateSnapshot(data);
source_volume_id: source_volume_id, snapshot = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name);
//https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto
creation_time: { if (!snapshot) {
seconds: snapshot.time, throw new Error(`failed to create snapshot`);
nanos: 0, }
},
ready_to_use: true,
},
};
} }
let data = Object.assign({}, driver.options.iscsi.lunSnapshotTemplate, {
src_lun_uuid: lun.uuid,
taken_by: "democratic-csi",
description: name, //check
});
let response = await httpClient.CreateSnapshot(data);
return { return {
snapshot: { snapshot: {
/** /**
* The purpose of this field is to give CO guidance on how much space * The purpose of this field is to give CO guidance on how much space
* is needed to create a volume from this snapshot. * is needed to create a volume from this snapshot.
*/ */
size_bytes: 0, size_bytes: snapshot.total_size,
snapshot_id: `/lun/${lun.lun_id}/${response.body.data.snapshot_uuid}`, snapshot_id: `/lun/${lun.lun_id}/${snapshot.uuid}`, // add shanpshot_uuid //fixme
source_volume_id: source_volume_id, source_volume_id: source_volume_id,
//https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto
creation_time: { creation_time: {
seconds: Math.round(new Date().getTime() / 1000), seconds: snapshot.time,
nanos: 0, nanos: 0,
}, },
ready_to_use: true, ready_to_use: true,

View File

@ -353,7 +353,21 @@ delete ${iscsiName}
driver.ctx.logger.verbose("TargetCLI command: " + logCommand); driver.ctx.logger.verbose("TargetCLI command: " + logCommand);
let response = await sshClient.exec(sshClient.buildCommand(command, args)); // https://github.com/democratic-csi/democratic-csi/issues/127
// https://bugs.launchpad.net/ubuntu/+source/python-configshell-fb/+bug/1776761
// can apply the linked patch with some modifications to overcome the
// KeyErrors or we can simply start a fake tty which does not seem to have
// a detrimental effect, only affects Ubuntu 18.04 and older
let options = {
pty: true,
};
let response = await sshClient.exec(
sshClient.buildCommand(command, args),
options
);
if (response.code != 0) {
throw new Error(response.stderr);
}
driver.ctx.logger.verbose( driver.ctx.logger.verbose(
"TargetCLI response: " + JSON.stringify(response) "TargetCLI response: " + JSON.stringify(response)
); );

View File

@ -2,6 +2,7 @@ const { CsiBaseDriver } = require("../index");
const SshClient = require("../../utils/ssh").SshClient; const SshClient = require("../../utils/ssh").SshClient;
const { GrpcError, grpc } = require("../../utils/grpc"); const { GrpcError, grpc } = require("../../utils/grpc");
const sleep = require("../../utils/general").sleep; const sleep = require("../../utils/general").sleep;
const getLargestNumber = require("../../utils/general").getLargestNumber;
const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs");
@ -1248,7 +1249,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
let sleep_time = 3000; let sleep_time = 3000;
let current_try = 1; let current_try = 1;
let success = false; let success = false;
while(!success && current_try <= max_tries) { while (!success && current_try <= max_tries) {
try { try {
await zb.zfs.destroy(datasetName, { recurse: true, force: true }); await zb.zfs.destroy(datasetName, { recurse: true, force: true });
success = true; success = true;
@ -1374,9 +1375,10 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
properties.volsize = capacity_bytes; properties.volsize = capacity_bytes;
setProps = true; setProps = true;
if (this.options.zfs.zvolEnableReservation) { // managed automatically for zvols
properties.refreservation = capacity_bytes; //if (this.options.zfs.zvolEnableReservation) {
} // properties.refreservation = capacity_bytes;
//}
break; break;
} }
@ -1800,6 +1802,9 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
"refquota", "refquota",
"avail", "avail",
"used", "used",
"volsize",
"referenced",
"logicalreferenced",
VOLUME_CSI_NAME_PROPERTY_NAME, VOLUME_CSI_NAME_PROPERTY_NAME,
SNAPSHOT_CSI_NAME_PROPERTY_NAME, SNAPSHOT_CSI_NAME_PROPERTY_NAME,
MANAGED_PROPERTY_NAME, MANAGED_PROPERTY_NAME,
@ -1864,6 +1869,20 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
return; return;
} }
// TODO: properly handle use-case where datasetEnableQuotas is not turned on
let size_bytes = 0;
if (driverZfsResourceType == "filesystem") {
// independent of detached snapshots when creating a volume from a 'snapshot'
// we could be using detached clones (ie: send/receive)
// so we must be cognizant and use the highest possible value here
// note that whatever value is returned here can/will essentially impact the refquota
// value of a derived volume
size_bytes = getLargestNumber(row.referenced, row.logicalreferenced);
} else {
// get the size of the parent volume
size_bytes = row.volsize;
}
if (source_volume_id) if (source_volume_id)
entries.push({ entries.push({
snapshot: { snapshot: {
@ -1874,7 +1893,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
* In that vein, I think it's best to return 0 here given the * In that vein, I think it's best to return 0 here given the
* unknowns of 'cow' implications. * unknowns of 'cow' implications.
*/ */
size_bytes: 0, size_bytes,
// remove parent dataset details // remove parent dataset details
snapshot_id: row["name"].replace( snapshot_id: row["name"].replace(
@ -1920,6 +1939,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
const driverZfsResourceType = this.getDriverZfsResourceType(); const driverZfsResourceType = this.getDriverZfsResourceType();
const zb = await this.getZetabyte(); const zb = await this.getZetabyte();
let size_bytes = 0;
let detachedSnapshot = false; let detachedSnapshot = false;
try { try {
let tmpDetachedSnapshot = JSON.parse( let tmpDetachedSnapshot = JSON.parse(
@ -1976,6 +1996,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
); );
} }
const volumeDatasetName = volumeParentDatasetName + "/" + source_volume_id;
const datasetName = datasetParentName + "/" + source_volume_id; const datasetName = datasetParentName + "/" + source_volume_id;
snapshotProperties[SNAPSHOT_CSI_NAME_PROPERTY_NAME] = name; snapshotProperties[SNAPSHOT_CSI_NAME_PROPERTY_NAME] = name;
snapshotProperties[SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME] = snapshotProperties[SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME] =
@ -2062,12 +2083,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
if (detachedSnapshot) { if (detachedSnapshot) {
tmpSnapshotName = tmpSnapshotName =
volumeParentDatasetName + volumeDatasetName + "@" + VOLUME_SOURCE_DETACHED_SNAPSHOT_PREFIX + name;
"/" +
source_volume_id +
"@" +
VOLUME_SOURCE_DETACHED_SNAPSHOT_PREFIX +
name;
snapshotDatasetName = datasetName + "/" + name; snapshotDatasetName = datasetName + "/" + name;
await zb.zfs.create(datasetName, { parents: true }); await zb.zfs.create(datasetName, { parents: true });
@ -2123,11 +2139,17 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
force: true, force: true,
defer: true, defer: true,
}); });
// let things settle down
//await sleep(3000);
} else { } else {
try { try {
await zb.zfs.snapshot(fullSnapshotName, { await zb.zfs.snapshot(fullSnapshotName, {
properties: snapshotProperties, properties: snapshotProperties,
}); });
// let things settle down
//await sleep(3000);
} catch (err) { } catch (err) {
if (err.toString().includes("dataset does not exist")) { if (err.toString().includes("dataset does not exist")) {
throw new GrpcError( throw new GrpcError(
@ -2140,6 +2162,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
} }
} }
// TODO: let things settle to ensure proper size_bytes is reported
// sysctl -d vfs.zfs.txg.timeout # vfs.zfs.txg.timeout: Max seconds worth of delta per txg
let properties; let properties;
properties = await zb.zfs.get( properties = await zb.zfs.get(
fullSnapshotName, fullSnapshotName,
@ -2150,6 +2174,11 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
"refquota", "refquota",
"avail", "avail",
"used", "used",
"volsize",
"referenced",
"refreservation",
"logicalused",
"logicalreferenced",
VOLUME_CSI_NAME_PROPERTY_NAME, VOLUME_CSI_NAME_PROPERTY_NAME,
SNAPSHOT_CSI_NAME_PROPERTY_NAME, SNAPSHOT_CSI_NAME_PROPERTY_NAME,
SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME, SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME,
@ -2160,6 +2189,22 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
properties = properties[fullSnapshotName]; properties = properties[fullSnapshotName];
driver.ctx.logger.verbose("snapshot properties: %j", properties); driver.ctx.logger.verbose("snapshot properties: %j", properties);
// TODO: properly handle use-case where datasetEnableQuotas is not turned on
if (driverZfsResourceType == "filesystem") {
// independent of detached snapshots when creating a volume from a 'snapshot'
// we could be using detached clones (ie: send/receive)
// so we must be cognizant and use the highest possible value here
// note that whatever value is returned here can/will essentially impact the refquota
// value of a derived volume
size_bytes = getLargestNumber(
properties.referenced.value,
properties.logicalreferenced.value
);
} else {
// get the size of the parent volume
size_bytes = properties.volsize.value;
}
// set this just before sending out response so we know if volume completed // set this just before sending out response so we know if volume completed
// this should give us a relatively sane way to clean up artifacts over time // this should give us a relatively sane way to clean up artifacts over time
await zb.zfs.set(fullSnapshotName, { [SUCCESS_PROPERTY_NAME]: "true" }); await zb.zfs.set(fullSnapshotName, { [SUCCESS_PROPERTY_NAME]: "true" });
@ -2173,7 +2218,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
* In that vein, I think it's best to return 0 here given the * In that vein, I think it's best to return 0 here given the
* unknowns of 'cow' implications. * unknowns of 'cow' implications.
*/ */
size_bytes: 0, size_bytes,
// remove parent dataset details // remove parent dataset details
snapshot_id: properties.name.value.replace( snapshot_id: properties.name.value.replace(

View File

@ -3,6 +3,7 @@ const { CsiBaseDriver } = require("../index");
const HttpClient = require("./http").Client; const HttpClient = require("./http").Client;
const TrueNASApiClient = require("./http/api").Api; const TrueNASApiClient = require("./http/api").Api;
const { Zetabyte } = require("../../utils/zfs"); const { Zetabyte } = require("../../utils/zfs");
const getLargestNumber = require("../../utils/general").getLargestNumber;
const sleep = require("../../utils/general").sleep; const sleep = require("../../utils/general").sleep;
const Handlebars = require("handlebars"); const Handlebars = require("handlebars");
@ -1843,6 +1844,14 @@ class FreeNASApiDriver extends CsiBaseDriver {
return client; return client;
} }
async getMinimumVolumeSize() {
const driverZfsResourceType = this.getDriverZfsResourceType();
switch (driverZfsResourceType) {
case "filesystem":
return 1073741824;
}
}
async getTrueNASHttpApiClient() { async getTrueNASHttpApiClient() {
const driver = this; const driver = this;
const httpClient = await this.getHttpClient(); const httpClient = await this.getHttpClient();
@ -2003,6 +2012,8 @@ class FreeNASApiDriver extends CsiBaseDriver {
let zvolBlocksize = this.options.zfs.zvolBlocksize || "16K"; let zvolBlocksize = this.options.zfs.zvolBlocksize || "16K";
let name = call.request.name; let name = call.request.name;
let volume_content_source = call.request.volume_content_source; let volume_content_source = call.request.volume_content_source;
let minimum_volume_size = await driver.getMinimumVolumeSize();
let default_required_bytes = 1073741824;
if (!datasetParentName) { if (!datasetParentName) {
throw new GrpcError( throw new GrpcError(
@ -2038,7 +2049,7 @@ class FreeNASApiDriver extends CsiBaseDriver {
Object.keys(call.request.capacity_range).length === 0 Object.keys(call.request.capacity_range).length === 0
) { ) {
call.request.capacity_range = { call.request.capacity_range = {
required_bytes: 1073741824, required_bytes: default_required_bytes,
}; };
} }
@ -2079,6 +2090,14 @@ class FreeNASApiDriver extends CsiBaseDriver {
); );
} }
// ensure *actual* capacity is not too small
if (minimum_volume_size > 0 && capacity_bytes < minimum_volume_size) {
throw new GrpcError(
grpc.status.OUT_OF_RANGE,
`volume capacity is smaller than the minimum: ${minimum_volume_size}`
);
}
// ensure *actual* capacity is not greater than limit // ensure *actual* capacity is not greater than limit
if ( if (
call.request.capacity_range.limit_bytes && call.request.capacity_range.limit_bytes &&
@ -2100,10 +2119,10 @@ class FreeNASApiDriver extends CsiBaseDriver {
let size; let size;
switch (driverZfsResourceType) { switch (driverZfsResourceType) {
case "volume": case "volume":
size = properties["volsize"].value; size = properties["volsize"].rawvalue;
break; break;
case "filesystem": case "filesystem":
size = properties["refquota"].value; size = properties["refquota"].rawvalue;
break; break;
default: default:
throw new Error( throw new Error(
@ -2197,10 +2216,13 @@ class FreeNASApiDriver extends CsiBaseDriver {
// zvol enables reservation by default // zvol enables reservation by default
// this implements 'sparse' zvols // this implements 'sparse' zvols
let sparse;
if (driverZfsResourceType == "volume") { if (driverZfsResourceType == "volume") {
// this is managed by the `sparse` option in the api
if (!this.options.zfs.zvolEnableReservation) { if (!this.options.zfs.zvolEnableReservation) {
volumeProperties.refreservation = 0; volumeProperties.refreservation = 0;
} }
sparse = Boolean(!this.options.zfs.zvolEnableReservation);
} }
let detachedClone = false; let detachedClone = false;
@ -2539,6 +2561,7 @@ class FreeNASApiDriver extends CsiBaseDriver {
...httpApiClient.getSystemProperties(volumeProperties), ...httpApiClient.getSystemProperties(volumeProperties),
type: driverZfsResourceType.toUpperCase(), type: driverZfsResourceType.toUpperCase(),
volsize: driverZfsResourceType == "volume" ? capacity_bytes : undefined, volsize: driverZfsResourceType == "volume" ? capacity_bytes : undefined,
sparse: driverZfsResourceType == "volume" ? sparse : undefined,
create_ancestors: true, create_ancestors: true,
user_properties: httpApiClient.getPropertiesKeyValueArray( user_properties: httpApiClient.getPropertiesKeyValueArray(
httpApiClient.getUserProperties(volumeProperties) httpApiClient.getUserProperties(volumeProperties)
@ -2913,9 +2936,10 @@ class FreeNASApiDriver extends CsiBaseDriver {
properties.volsize = capacity_bytes; properties.volsize = capacity_bytes;
setProps = true; setProps = true;
if (this.options.zfs.zvolEnableReservation) { // managed automatically for zvols
properties.refreservation = capacity_bytes; //if (this.options.zfs.zvolEnableReservation) {
} // properties.refreservation = capacity_bytes;
//}
break; break;
} }
@ -2970,8 +2994,17 @@ class FreeNASApiDriver extends CsiBaseDriver {
let properties; let properties;
properties = await httpApiClient.DatasetGet(datasetName, ["available"]); properties = await httpApiClient.DatasetGet(datasetName, ["available"]);
let minimum_volume_size = await driver.getMinimumVolumeSize();
return { available_capacity: Number(properties.available.rawvalue) }; return {
available_capacity: Number(properties.available.rawvalue),
minimum_volume_size:
minimum_volume_size > 0
? {
value: Number(minimum_volume_size),
}
: undefined,
};
} }
/** /**
@ -3326,6 +3359,9 @@ class FreeNASApiDriver extends CsiBaseDriver {
"refquota", "refquota",
"available", "available",
"used", "used",
"volsize",
"referenced",
"logicalreferenced",
VOLUME_CSI_NAME_PROPERTY_NAME, VOLUME_CSI_NAME_PROPERTY_NAME,
SNAPSHOT_CSI_NAME_PROPERTY_NAME, SNAPSHOT_CSI_NAME_PROPERTY_NAME,
MANAGED_PROPERTY_NAME, MANAGED_PROPERTY_NAME,
@ -3546,6 +3582,20 @@ class FreeNASApiDriver extends CsiBaseDriver {
return; return;
} }
// TODO: properly handle use-case where datasetEnableQuotas is not turned on
let size_bytes = 0;
if (driverZfsResourceType == "filesystem") {
// independent of detached snapshots when creating a volume from a 'snapshot'
// we could be using detached clones (ie: send/receive)
// so we must be cognizant and use the highest possible value here
// note that whatever value is returned here can/will essentially impact the refquota
// value of a derived volume
size_bytes = getLargestNumber(row.referenced, row.logicalreferenced);
} else {
// get the size of the parent volume
size_bytes = row.volsize;
}
if (source_volume_id) if (source_volume_id)
entries.push({ entries.push({
snapshot: { snapshot: {
@ -3556,7 +3606,7 @@ class FreeNASApiDriver extends CsiBaseDriver {
* In that vein, I think it's best to return 0 here given the * In that vein, I think it's best to return 0 here given the
* unknowns of 'cow' implications. * unknowns of 'cow' implications.
*/ */
size_bytes: 0, size_bytes,
// remove parent dataset details // remove parent dataset details
snapshot_id: row["name"].replace( snapshot_id: row["name"].replace(
@ -3606,6 +3656,7 @@ class FreeNASApiDriver extends CsiBaseDriver {
const httpApiClient = await this.getTrueNASHttpApiClient(); const httpApiClient = await this.getTrueNASHttpApiClient();
const zb = await this.getZetabyte(); const zb = await this.getZetabyte();
let size_bytes = 0;
let detachedSnapshot = false; let detachedSnapshot = false;
try { try {
let tmpDetachedSnapshot = JSON.parse( let tmpDetachedSnapshot = JSON.parse(
@ -3904,12 +3955,19 @@ class FreeNASApiDriver extends CsiBaseDriver {
"refquota", "refquota",
"available", "available",
"used", "used",
"volsize",
"referenced",
"refreservation",
"logicalused",
"logicalreferenced",
VOLUME_CSI_NAME_PROPERTY_NAME, VOLUME_CSI_NAME_PROPERTY_NAME,
SNAPSHOT_CSI_NAME_PROPERTY_NAME, SNAPSHOT_CSI_NAME_PROPERTY_NAME,
SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME, SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME,
MANAGED_PROPERTY_NAME, MANAGED_PROPERTY_NAME,
]; ];
// TODO: let things settle to ensure proper size_bytes is reported
// sysctl -d vfs.zfs.txg.timeout # vfs.zfs.txg.timeout: Max seconds worth of delta per txg
if (detachedSnapshot) { if (detachedSnapshot) {
properties = await httpApiClient.DatasetGet( properties = await httpApiClient.DatasetGet(
fullSnapshotName, fullSnapshotName,
@ -3924,6 +3982,22 @@ class FreeNASApiDriver extends CsiBaseDriver {
driver.ctx.logger.verbose("snapshot properties: %j", properties); driver.ctx.logger.verbose("snapshot properties: %j", properties);
// TODO: properly handle use-case where datasetEnableQuotas is not turned on
if (driverZfsResourceType == "filesystem") {
// independent of detached snapshots when creating a volume from a 'snapshot'
// we could be using detached clones (ie: send/receive)
// so we must be cognizant and use the highest possible value here
// note that whatever value is returned here can/will essentially impact the refquota
// value of a derived volume
size_bytes = getLargestNumber(
properties.referenced.rawvalue,
properties.logicalreferenced.rawvalue
);
} else {
// get the size of the parent volume
size_bytes = properties.volsize.rawvalue;
}
// set this just before sending out response so we know if volume completed // set this just before sending out response so we know if volume completed
// this should give us a relatively sane way to clean up artifacts over time // this should give us a relatively sane way to clean up artifacts over time
//await zb.zfs.set(fullSnapshotName, { [SUCCESS_PROPERTY_NAME]: "true" }); //await zb.zfs.set(fullSnapshotName, { [SUCCESS_PROPERTY_NAME]: "true" });
@ -3946,7 +4020,7 @@ class FreeNASApiDriver extends CsiBaseDriver {
* In that vein, I think it's best to return 0 here given the * In that vein, I think it's best to return 0 here given the
* unknowns of 'cow' implications. * unknowns of 'cow' implications.
*/ */
size_bytes: 0, size_bytes,
// remove parent dataset details // remove parent dataset details
snapshot_id: properties.name.value.replace( snapshot_id: properties.name.value.replace(

View File

@ -14,6 +14,20 @@ class Client {
} }
getBaseURL() { getBaseURL() {
const server = this.options; const server = this.options;
if (!server.protocol) {
if (server.port) {
if (String(server.port).includes("80")) {
server.protocol = "http";
}
if (String(server.port).includes("443")) {
server.protocol = "https";
}
}
}
if (!server.protocol) {
server.protocol = "http";
}
const options = { const options = {
scheme: server.protocol, scheme: server.protocol,
host: server.host, host: server.host,

View File

@ -1054,6 +1054,15 @@ class CsiBaseDriver {
normalized_staging_path = staging_target_path; normalized_staging_path = staging_target_path;
} }
// sanity check to ensure the staged path is actually mounted
result = await mount.pathIsMounted(normalized_staging_path);
if (!result) {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`staging path is not mounted: ${normalized_staging_path}`
);
}
result = await mount.pathIsMounted(target_path); result = await mount.pathIsMounted(target_path);
// if not mounted, mount // if not mounted, mount
if (!result) { if (!result) {

View File

@ -34,5 +34,23 @@ function lockKeysFromRequest(call, serviceMethodName) {
} }
} }
function getLargestNumber() {
let number;
for (let i = 0; i < arguments.length; i++) {
value = Number(arguments[i]);
if (isNaN(value)) {
continue;
}
if (isNaN(number)) {
number = value;
continue;
}
number = value > number ? value : number;
}
return number;
}
module.exports.sleep = sleep; module.exports.sleep = sleep;
module.exports.lockKeysFromRequest = lockKeysFromRequest; module.exports.lockKeysFromRequest = lockKeysFromRequest;
module.exports.getLargestNumber = getLargestNumber;

View File

@ -10,7 +10,7 @@ FINDMNT_COMMON_OPTIONS = [
"--nofsroot", // prevents unwanted behavior with cifs volumes "--nofsroot", // prevents unwanted behavior with cifs volumes
]; ];
DEFAUT_TIMEOUT = 30000; DEFAUT_TIMEOUT = process.env.MOUNT_DEFAULT_TIMEOUT || 30000;
class Mount { class Mount {
constructor(options = {}) { constructor(options = {}) {

View File

@ -33,25 +33,28 @@ class SshClient {
var conn = new Client(); var conn = new Client();
if (client.options.connection.debug == true) { if (client.options.connection.debug == true) {
client.options.connection.debug = function(msg) { client.options.connection.debug = function (msg) {
client.debug(msg); client.debug(msg);
}; };
} }
conn conn
.on("error", function(err) { .on("error", function (err) {
client.debug("Client :: error"); client.debug("Client :: error");
reject(err); reject(err);
}) })
.on("ready", function() { .on("ready", function () {
client.debug("Client :: ready"); client.debug("Client :: ready");
conn.exec(command, options, function(err, stream) { //options.pty = true;
//options.env = {
// TERM: "",
//};
conn.exec(command, options, function (err, stream) {
if (err) reject(err); if (err) reject(err);
let stderr; let stderr;
let stdout; let stdout;
stream stream
.on("close", function(code, signal) { .on("close", function (code, signal) {
client.debug( client.debug(
"Stream :: close :: code: " + code + ", signal: " + signal "Stream :: close :: code: " + code + ", signal: " + signal
); );
@ -61,7 +64,7 @@ class SshClient {
resolve({ stderr, stdout, code, signal }); resolve({ stderr, stdout, code, signal });
conn.end(); conn.end();
}) })
.on("data", function(data) { .on("data", function (data) {
client.debug("STDOUT: " + data); client.debug("STDOUT: " + data);
if (stream_proxy) { if (stream_proxy) {
stream_proxy.stdout.emit("data", ...arguments); stream_proxy.stdout.emit("data", ...arguments);
@ -71,7 +74,7 @@ class SshClient {
} }
stdout = stdout.concat(data); stdout = stdout.concat(data);
}) })
.stderr.on("data", function(data) { .stderr.on("data", function (data) {
client.debug("STDERR: " + data); client.debug("STDERR: " + data);
if (stream_proxy) { if (stream_proxy) {
stream_proxy.stderr.emit("data", ...arguments); stream_proxy.stderr.emit("data", ...arguments);
@ -86,7 +89,7 @@ class SshClient {
.connect(client.options.connection); .connect(client.options.connection);
if (stream_proxy) { if (stream_proxy) {
stream_proxy.on("kill", signal => { stream_proxy.on("kill", (signal) => {
conn.end(); conn.end();
}); });
} }